'How to convert an object containing DateTime fields to JSON in Dart?

I try to convert an object to JSON.

  var obj = { "dt": new DateTime.now() };
  var s = stringify(obj);

The runtime throws an exception: "Calling toJson method on object failed."

That's expected since DateTime class doesn't have toJson method. But what should I do in this case?

Javascript's JSON.stringify function has an optional argument replacer which allows me to provide my own way of serialization of any object even if the object has no toJson method. Is there any similar facility in Dart or maybe I can extend somehow DateTime class with my own toJson method?



Solution 1:[1]

JSON conversion only works with maps, lists, strings, numbers, booleans, or null. So what if your object contains another type like DateTime?

DateTime ? JSON

Let's start with the following object:

class Person {
  Person(this.name, this.birthdate);
  String name;
  DateTime birthdate;
}

You can convert it to a map like this:

final person = Person('Bob', DateTime(2020, 2, 25));

Map<String, dynamic> map = {
 'name': person.name,
 'birthdate': person.birthdate,
};

If you tried to encode this right now with jsonEncode (or json.encode), you would get an error because the DateTime is not directly serializeable. There are two solutions.

Solution 1

You could serialize it yourself first like this:

Map<String, dynamic> map = {
  'name': person.name,
  'birthdate': person.birthdate.toIso8601String(),
};

final jsonString = json.encode(map);

Note:

Here is the difference between toString and toIso8601String:

2020-02-25 14:44:28.534 // toString()
2020-02-25T14:44:28.534 // toIso8601String()

The toIso8601String doesn't have any spaces so that makes it nicer for conversions and sending over APIs that might not deal with spaces well.

Solution 2

You could use the optional toEncodable function parameter on jsonEncode.

import 'dart:convert';

void main() {
  final person = Person('Bob', DateTime(2020, 2, 25));

  Map<String, dynamic> map = {
    'name': person.name,
    'birthdate': person.birthdate,
  };

  final toJson = json.encode(map, toEncodable: myDateSerializer);
}

dynamic myDateSerializer(dynamic object) {
  if (object is DateTime) {
    return object.toIso8601String();
  }
  return object;
}

The toEncodable function just converts the input to a string or something that jsonEncode can covert to a string.

JSON ? DateTime

There is nothing special here. You just have to parse the string into the type that you need. In the case of DateTime you can use its parse or tryParse methods.

final myMap= json.decode(jsonString);
final name = myMap['name'];
final birthdateString = myMap['birthdate'];
final birthdate = DateTime.parse(birthdateString);
final decodedPerson = Person(name, birthdate);

Note that parse will throw an exception if the format of the string cannot be parsed into a DateTime object.

As a model class

class Person {

  Person(this.name, this.birthdate);

  String name;
  DateTime birthdate;

  Person.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        birthdate = DateTime.tryParse(json['birthdate']),

  Map<String, dynamic> toJson() {
    return {
      'name': name,
      'birthdate': birthdate.toIso8601String(),
    };
  }
}

This will not throw an exception is the date is malformatted, but birthdate would be null.

Notes

  • See my fuller answer here.
  • Thanks to this answer for pointing me in the right direction.

Solution 2:[2]

Zdeslav Vojkovic's answer is outdated now.

The JSON.encode() method in dart:convert has an optional toEncodable method that is invoked for objects that are not natively serializable to JSON. It's then up to the user to provide a closure that returns an appropriate serialization of the DateTime.

Solution 3:[3]

IMO, it's a flaw in dart:json library that stringify doesn't support additional callback to serialize types lacking the implementation of toJson. Strangely enough, parse function does support reviver argument to customize the deserialization process. Probably the reason was along the lines that user can add toJson for their on types, and core library will take care of 'native' types, but DateTime was omitted (maybe because date serialization in JSON is not really a straightforward story).

Maybe the goal of the team was to use custom types instead of Maps (which is nice), but the drawback here is that if your custom type contains 10 other properties and 1 which is of DateTime type, you need to implement toJson which serializes all of them, even integers and strings. Hopefully, once when Mirror API is ready, there will be libraries that implement serialization 'out-of-band' by reading the reflected info on type, see lower for an example. The promise of Dart is that I would be able to use my types both on server and client, but if I have to manually implement serialization/deserialization for all my models then it is too awkward to be usable.

it also doesn't help that DateTime itself is not very flexible with formatting, as there are no other methods besides toString which returns the format useless for serialization.

So here are some options:

  • wrap (or derive from) DateTime in your own type which provides toJson
  • patch json.stringifyJsonValue and submit the change or at least submit an issue :)
  • use some 3-rd party library like json-object (just an example, it also doesn't support DateTime serialization, AFAIK

I am not really happy with any of them :)

Solution 4:[4]

I've added a new package to Dart pub.dev that allows json serialization of objects within a structure. This package Jsonize serialize and deserialize custom classes, and handles DateTime out of the box:

  List<dynamic> myList = [1, "Hello!", DateTime.now()];
  var jsonRep = Jsonize.toJson(myList);
  var myDeserializedList = Jsonize.fromJson(jsonRep);

So will do with your example

  var obj = { "dt": new DateTime.now() };
  var s = Jsonize.toJson(obj);
  var obj2 = Jsonize.fromJson(s);

but can do also this

  var obj = DateTime.now();
  var s = Jsonize.toJson(obj);
  var dt = Jsonize.fromJson(s);

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Florian Loitsch
Solution 3 Zdeslav Vojkovic
Solution 4 cabbi