A common task when building application nowadays consists of communicating with a distant server through REST APIs. These APIs often support JSON as a data format for communication through HTTP protocol. I found the task of deserializing data not so obvious in Dart strong mode (used by Flutter), so I decided to present you how we can achieve this in a few steps.
Creating a Map
object from a JSON content is pretty straightforward and integrated to the SDK with the dart:convert
package.
import 'dart:convert';
final json = '{ "hello" : "world" }';
final map = JSON.decode(json);
final hello = map["hello"];
print(hello); // "world""
Since in strong mode we can’t rely on mirrors to analyze objects at runtime, we will use code generation with the help of the json_serializable package.
First, you must declare the dependencies in your pubspec.yaml
file:
dependencies:
# ...
json_annotation:
dev_dependencies:
# ...
build_runner:
json_serializable:
Now we have to describe our data model in a library. For example, for a dart/model.dart
file, declared as a model
library :
{
"firstname" : "John",
"lastname" : "Doe",
"company" :
{
"name": "Awesome Inc."
}
}
library example;
import 'package:json_annotation/json_annotation.dart';
part 'model.g.dart';
@JsonSerializable()
class Customer extends Object with _$CustomerSerializerMixin {
final String firstname;
final String lastname;
@JsonKey(nullable: false)
Company company;
Customer(this.firstname, this.lastname, this.company);
// Boilerplate code needed to wire-up generated code
factory Customer.fromJson(Map<String, dynamic> json) => _$CustomerFromJson(json);
}
@JsonSerializable()
class Company extends Object with _$CompanySerializerMixin {
Company(this.name);
String name;
// Boilerplate code needed to wire-up generated code
factory Company.fromJson(Map<String, dynamic> json) => _$CompanyFromJson(json);
}
This will not compile, and it is perfectly normal since the serialization part hasn’t been generated yet!
You must create a tool/build.dart
file in your project. This will be the program executed to generate serialization code.
import 'dart:async';
import 'package:build_runner/build_runner.dart';
import 'package:json_serializable/src/json_part_builder.dart';
import 'package:source_gen/source_gen.dart';
Future main(List<String> args) async {
await build([
new BuildAction(
jsonPartBuilder(),
'<YOUR PROJECT ID>',
inputs: const ['lib/*.dart'])
], deleteFilesByDefault: true);
}
To generate the *.g.dart
files, run the dart tool/build.dart
command.
To deserialize your JSON from a json string, just use the <Model>.fromJson(<json>)
constructor to instantiate you object from you decoded JSON.
final json = JSON.decode('{ "firstname" : "John", "lastname" : "Doe", "company" : { "name": "Awesome Inc." } }');
Customer customer = new Customer.fromJson(map);
You can use a second package called built_value for more advanced serialization. It will not only deserialize objects from JSON, but gives you other formats and functionalities like immutability, validation, hashCode
, equals
, toString
First, you must declare the dependencies in your pubspec.yaml
file:
dependencies:
# ...
built_value:
dev_dependencies:
# ...
build:
build_runner:
source_gen:
built_value_generator:
The source_gen
, build_runner
, build
are needed for adding code generation steps to the process.
Now we have to describe our data model in a library. For example, for a dart/model.dart
file, declared as a model
library :
{
"firstname" : "John",
"lastname" : "Doe",
"company" :
{
"name": "Awesome Inc."
}
}
library model; // name aligned with filename
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
part 'model.g.dart'; // The imported generate code (<name>.g.dart)
abstract class Customer implements Built<Customer, CustomerBuilder> {
// Various fields
String get firstname;
String get lastname;
Company get company; // This is a nested Built<..,..>
// Boilerplate code needed to wire-up generated code
Customer._();
factory Customer([updates(CustomerBuilder b)]) = _$Customer;
static Serializer<Customer> get serializer => _$customerSerializer;
}
abstract class Company implements Built<Company, CompanyBuilder> {
// Various fields
String get name;
// Boilerplate code needed to wire-up generated code
Company._();
factory Company([updates(CompanyBuilder b)]) = _$Company;
static Serializer<Company> get serializer => _$companySerializer;
}
You also need to declare a serializer.dart
library.
library serializers;
import 'package:built_collection/built_collection.dart';
import 'package:built_value/serializer.dart';
import 'model.dart';
part 'serializers.g.dart';
@SerializersFor(const [
Customer,
Company,
])
final Serializers serializers = _$serializers;
This will not compile, and it is perfectly normal since the serialization part hasn’t been generated yet!
You must create a tool/build.dart
file in your project. This will be the program executed to generate serialization code.
import 'dart:async';
import 'package:build_runner/build_runner.dart';
import 'package:built_value_generator/built_value_generator.dart';
import 'package:source_gen/source_gen.dart';
Future main(List<String> args) async {
await build([
new BuildAction(
new PartBuilder([
new BuiltValueGenerator(),
]),
'<YOUR PROJECT ID>',
inputs: const ['lib/*.dart'])
], deleteFilesByDefault: true);
}
To generate the *.g.dart
files, run the dart tool/build.dart
command.
To deserialize your JSON from your string, we need to plug the StandardJsonPlugin
to serializers first. Use the standardSerializers.deserializeWith
method to instantiate you object from you decoded JSON.
import 'package:<YOUR PROJECT ID>/model.dart';
import 'package:<YOUR PROJECT ID>/serializers.dart';
import 'package:built_value/standard_json_plugin.dart';
final standardSerializers = (serializers.toBuilder()..addPlugin(new StandardJsonPlugin())).build();
final json = JSON.decode('{ "firstname" : "John", "lastname" : "Doe", "company" : { "name": "Awesome Inc." } }');
Customer customer = standardSerializers.deserializeWith(Customer.serializer, json);
If your platform is compatible with Dart 2 (not yet available with Flutter), you can take advantage of a most recent version of the built_runner package that provides a way to trigger build steps without the hassle of having to add a dart build program.
pub run build_runner <watch|build>
The whole process could seem heavy compared to reflection based solutions, but it is a real benefit from a performance point of view. If you adopt the most complex option (2, with build_value
), you will also have data validation, hash codes and other functionalities that go far beyond serialization (though it is not always needed).
As suggested on built_value documentation, you may want to add a code snippet to your editor for a quicker type declaration.
You can also create a watch.dart
file to add continuous generation (every time the source file changes).
Look at the documentation articles for more advanced features (polymorphism, enums, …).