Tuesday, October 13, 2020

Serialization with JSON in Dart

Whenever one needs to package up an object and send it over a network or save it to a file, one needs serialization. This post is a brief overview and example of how do to so using the Dart language. 

A de facto standard serialization language that has emerged in recent years is JSON, the JavaScript Object Notation. The Dart language has adapted it as its own serialization standard, and it is the approach that we will employ here. 

The appeal and utility of JSON comes in how it nicely mirrors the structure of an object definition. Basic values (e.g. strings, numbers) are represented as strings. Sequences of values are also representable. Most pertinent to representing objects, each object is represented by a dictionary. The keys in the dictionary are the names of the object's instance variables, and the values associated with each key are in turn represented in JSON. Constructing a JSON representation of an object, then, is a recursive process in which one creates a dictionary by creating an entry for each instance variable. 

Here is a simple Dart class representing a User with two strings: a name and title. It creates a JSON object by returning a Dart Map containing those variables and their values. It rebuilds a User object from a JSON object by looking up the instance variable values from the JSON map and assigning them accordingly.

class User {
  String _name;
  String _title;

  User(this._name, this._title);

  bool operator==(Object other) =>
      other is User && other.name == name && other.title == title;

  String get name => _name;
  String get title => _title;

  Map<String,dynamic> toJson() => {
    'name': _name,
    'title': _title,
  };

  User.fromJson(Map<String, dynamic> json)
    : _name = json['name'], _title = json['title'];
}

The Map object containing the JSON representation has a value type of dynamic to avoid overdetermining the data types of the instance variables.

This next class contains a User object, and helps illustrate how JSON can be built recursively:

class UserStampedMessage {
  String _message;
  User _user;

  UserStampedMessage(this._message, this._user);

  bool operator==(Object other) =>
      other is UserStampedMessage && other.message == message && other.user == user;

  String get message => _message;
  User get user => _user;

  Map<String, dynamic> toJson() => {
    'message': _message,
    'user': _user.toJson()
  };

  UserStampedMessage.fromJson(Map<String, dynamic> json)
    : _message = json['message'], _user = User.fromJson(json['user']);
}

A UserStampedMessage contains a message and an associated user. When converting to JSON, the value for the User object in the returned map is itself another JSON object. When rebuilding the object from JSON, the fromJson method for the User class is invoked to rebuild the instance variable's value.

To send a UserStampedMessage over a network, then, involves the following steps:

  • Convert the object to JSON.
  • Convert the JSON to a string (use the jsonEncode() function from the dart:convert package).
  • Send the string into a socket.
Retrieving a UserStampedMessage from a socket requires the following complementary steps:
  • The value coming from the socket will be a Uint8List (i.e., a byte sequence). Use String.fromCharCodes() to convert it to a string.
  • Convert the string into a JSON map using the jsonDecode() function from the dart:convert package.
  • Rebuild the object from the JSON map using the fromJson method.
The following code fragment shows the encoding process:
    UserStampedMessage msg = UserStampedMessage("This is a test", User("Gabriel Ferrer", "Professor"));
    Map<String, dynamic> msgJson = msg.toJson();
    String msgStr = jsonEncode(msgJson);
    // Send over a socket, save to a file, or whatever else you have in mind...
Here are the pieces assembled for the decoding process:
    String msgStr = /* retrieve from socket, file, or wherever... */;
    Map<String, dynamic> decodedMap = jsonDecode(msgStr);
    UserStampedMessage recovered = UserStampedMessage.fromJson(decodedMap);

Happy coding!