
Using json_serializable to Serialise Dart/Flutter Models
In this article we're going to be looking at how we can use the json_serializable
package to parse from/convert to json model instances within Flutter. We'll also look at how to use JsonConverter<X, Y>
to write custom conversions for non-primitive types.
Project Setup
As always, we'll be starting with a blank Flutter project. Run the following in your terminal:
$ flutter create ds_json
$ cd ds_json
$ code . (or your favourite editor)
You'll then need to add the following packages to pubspec.yaml
:
dependencies:
flutter:
sdk: flutter
json_annotation: ^3.1.1
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^1.10.9
json_serializable: ^3.5.1
You can now run your application on the emulator or device.
Contact List Application
Imagine an application that allows us to store a list of contacts. As it turns out, we're interested in knowing the following information about our contact:
- Name
- Date of Birth
- Favourite Colour
This can be expressed as a Contact
model like so:
@immutable
class Contact {
final String name;
final DateTime dateOfBirth;
final Color favouriteColour;
const Contact({
@required this.name,
@required this.dateOfBirth,
@required this.favouriteColour,
});
/// Below here is used for equality comparison
///
/// See the freezed article for a good way to automate this
/// https://developer.school/how-to-use-freezed-with-flutter/
@override
bool operator ==(Object other) =>
other is Contact &&
other.name == name &&
other.dateOfBirth == dateOfBirth &&
other.favouriteColour == favouriteColour;
@override
int get hashCode => hashValues(name, dateOfBirth, favouriteColour);
}
Ideally, we'd like to be able to call contact.toJson()
on a Contact
instance and a Contact.fromJson(json)
factory. You'll find the need to do this when you're saving or retrieving data in JSON format locally or across a network.
Without json_serializable
Without the use of this library we might write toJson/fromJson
serialisation code like this:
factory Contact.fromJson(Map<String, dynamic> json) => Contact(
name: json['name'] as String,
dateOfBirth: DateTime.parse(json['dateOfBirth'] as String),
favouriteColour: Color(json['favouriteColour'] as int),
);
Map<String, dynamic> toJson() => {
'name': name,
'dateOfBirth': dateOfBirth.toIso8601String(),
'favouriteColour': favouriteColour.value,
};
The major issue with this is that the process is painfully manual. Not only do we have to update our serialisation method(s) every time we add new properties to this class, but we have to do this for every class.
It doesn't scale well. This is where json_serializable
comes in handy.
Using json_serializable
Update your Contact
model to contain the following methods:
factory Contact.fromJson(Map<String, dynamic> json) =>
_$ContactFromJson(json);
Map<String, dynamic> toJson() => _$ContactToJson(this);
This funky looking code is generated from json_serializable
when we run the build_runner
command. However, it won't generate our model yet until we add the JsonSerializable
annotation and define the part
that contains the generated code.
import 'package:flutter/widgets.dart';
import 'package:json_annotation/json_annotation.dart';
// 1
part 'contact_model.g.dart';
@immutable
@JsonSerializable()
class Contact {
final String name;
final DateTime dateOfBirth;
final Color favouriteColour;
const Contact({
@required this.name,
@required this.dateOfBirth,
@required this.favouriteColour,
});
// 2
factory Contact.fromJson(Map<String, dynamic> json) =>
_$ContactFromJson(json);
Map<String, dynamic> toJson() => _$ContactToJson(this);
}
build_runner
We can now run the generator by typing the following inside of our terminal:
$ flutter pub run build_runner watch --delete-conflicting-outputs
This starts the build_runner
which acts as a task runner for the json_serializable
package. As we've added the annotation and part, it'll attempt to generate the serialisation for our Contact
.
Uh oh. You'll notice that we have an error.
[SEVERE] json_serializable:json_serializable on lib/src/contacts/domain/models/contact_model.dart:
Could not generate `fromJson` code for `favouriteColour`.
In order to make this work we'll need to make a custom JsonConverter
that is able to perform the .toJson
and .fromJson
for the Color
field. This is because json_serializable
only contains relevant converters for primitive types and isn't able to automatically infer this.
Using JsonConverter
Let's make a a JsonConverter
for our Color
class. The questions we need to ask ourselves at this point are:
- What's the best primitive data type to use when serialising this model?
In the FlutterColor
type, we can convert this to anint
by using thevalue
getter.
You could also store this as aString
if you wanted. The "best" here is based on a per-project/requirement basis andint
works best for us. - How can we create a new model from the serialised data?
We can create a newColor
from anint
.
This can be implemented like so:
import 'package:flutter/widgets.dart';
import 'package:json_annotation/json_annotation.dart';
class ColorSerialiser implements JsonConverter<Color, int> {
const ColorSerialiser();
@override
Color fromJson(int json) => Color(json);
@override
int toJson(Color color) => color.value;
}
Finally, we can add the @ColorSerialiser()
annotation to our favouriteColour
and run build_runner
again if needed.
@JsonSerializable()
class Contact {
// Redacted for example
@ColorSerialiser()
final Color favouriteColour;
}
Serialising/Deserialising a Contact
Instead of building our a user interface for our Contact serialisation example, we'll write a quick set of unit tests that confirm it works as expected.
Create a file in the tests
directory named contact_test.dart
:
import 'package:ds_json/src/contacts/domain/models/contact_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group("Serialisation", () {
test('Contact is serialised to json', () async {
final actual = Contact(
name: "Paul",
dateOfBirth: DateTime(2020, 1, 1),
favouriteColour: Color(0xff9b59b6),
).toJson();
final matcher = {
"name": "Paul",
"dateOfBirth": "2020-01-01T00:00:00.000",
"favouriteColour": 4288371126
};
expect(actual, matcher);
});
test('Contact is serialised from json', () async {
final Map<String, dynamic> json = {
"name": "Paul",
"dateOfBirth": "2020-01-01T00:00:00.000",
"favouriteColour": 4288371126
};
final actual = Contact.fromJson(json);
final matcher = Contact(
name: "Paul",
dateOfBirth: DateTime(2020, 1, 1),
favouriteColour: Color(0xff9b59b6),
);
expect(actual, matcher);
});
});
}
We can then run this test by typing the following inside of our terminal:
$ flutter test
Running "flutter pub get" in ds_json... 564ms
00:02 +2: All tests passed!
This proves that our ColorSerialiser
worked as intended. It converted the favouriteColor
to an int
on toJson
serialisation and created a Color
object upon fromJson
deserialisation.
Summary
In this article we looked at using json_serializable
to easily write the necessary JSON serialisation code for us. This saved us a significant amount of time in comparison to writing this manually.
You can find the code for this article here: