Work with JSON in Flutter - Part 2: json_serializable

7 min read, 20 July, 2018

From Part 1 of using dart:convert to work with JSON in Flutter, we can see that using dart:convert requires a large amount of hand-written code for the mapping between JSON data and model class objects. If we take a close look at the code, we can clearly see the pattern of how the mapping is done. Values are extracted from the JSON object and assigned to the object's properties with the same name. This means we can generate the mapping code automatically. There is already a library json_serializable does this, so we can just use it.

This article uses json_serializable version 0.5.2, json_annotation version 0.2.4 and build_runner version 0.8.3. The latest version 1.0.0 of json_serializable and json_annotation are not compatible with Dart SDK 2.0.0-dev.58.0.flutter-f981f09760 bundled in current Flutter 0.5.1 release. Version 1.0.0 requires at least Dart SDK 2.0.0-dev.65.

json_serializable generates code based on annotations. There is no GSON/Jackson equivalent in Flutter. This is because these libraries require using runtime reflection, which is disabled in Flutter. Runtime reflection interferes with tree shaking. With tree shaking, we can "shake off" unused code from release builds. This optimizes the app's size significantly. Since reflection makes all code implicitly used by default, it makes tree shaking difficult.

Setup

To use json_serializable, we need to include it and two other dev dependencies in the project. Update project's pubspec.yaml file to include these dependencies as shown below. Run the command flutter packages get or use "Packages get" in the IDE to install these dependencies in the project.

dependencies:
  json_annotation: ^0.2.3

dev_dependencies:
  build_runner: ^0.8.0
  json_serializable: ^0.5.0

Usage

We still use the user JSON data and classes User and Address in Part 1 as the example. We can now remove the factory constructors fromJson and replace them with generated code.

The annotation JsonSerializable is added to classes that need to generate JSON serialization code. Here we add to classes User and Address. user.g.dart is the name of generated file. For the class User, _$UserSerializerMixin and _$UserFromJson are both generated.

import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

()
class User extends Object with _$UserSerializerMixin {
  int id;
  String firstName, lastName, email;
  bool vip;
  String dateOfBirth;
  List<Address> shippingAddresses;

  User({
    this.id,
    this.firstName,
    this.lastName,
    this.email,
    this.vip,
    this.dateOfBirth,
    this.shippingAddresses,
  });

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

()
class Address extends Object with _$AddressSerializerMixin {
  String address, city, state, country, zipcode;

  Address({
    this.address,
    this.city,
    this.state,
    this.country,
    this.zipcode,
  });

  factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
}

Below is the content of generated file user.g.dart. _$UserSerializerMixin is the abstract class with includes the method toJson. _$UserFromJson is the function to create User objects from JSON data.

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'user.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

User _$UserFromJson(Map<String, dynamic> json) => new User(
    id: json['id'] as int,
    firstName: json['firstName'] as String,
    lastName: json['lastName'] as String,
    email: json['email'] as String,
    vip: json['vip'] as bool,
    dateOfBirth: json['dateOfBirth'] as String,
    shippingAddresses: (json['shippingAddresses'] as List)
        ?.map((e) =>
            e == null ? null : new Address.fromJson(e as Map<String, dynamic>))
        ?.toList());

abstract class _$UserSerializerMixin {
  int get id;
  String get firstName;
  String get lastName;
  String get email;
  bool get vip;
  String get dateOfBirth;
  List<Address> get shippingAddresses;
  Map<String, dynamic> toJson() => <String, dynamic>{
        'id': id,
        'firstName': firstName,
        'lastName': lastName,
        'email': email,
        'vip': vip,
        'dateOfBirth': dateOfBirth,
        'shippingAddresses': shippingAddresses
      };
}

Address _$AddressFromJson(Map<String, dynamic> json) => new Address(
    address: json['address'] as String,
    city: json['city'] as String,
    state: json['state'] as String,
    country: json['country'] as String,
    zipcode: json['zipcode'] as String);

abstract class _$AddressSerializerMixin {
  String get address;
  String get city;
  String get state;
  String get country;
  String get zipcode;
  Map<String, dynamic> toJson() => <String, dynamic>{
        'address': address,
        'city': city,
        'state': state,
        'country': country,
        'zipcode': zipcode
      };
}

With the generated code, the code we need to write is largely simplified.

Build

The code generation can be triggered manually or automatically.

  • To run it manually, use the command flutter packages pub run build_runner build.
  • We can also use the command flutter packages pub run build_runner watch to setup the watcher to run code generation automatically upon file changes.

You may need to pass the argument --delete-conflicting-outputs to flutter packages pub run build_runner build to remove old generated files before generating new files.

Customization

We can customize json_serializable for different requirements.

JsonSerializable

The annotation JsonSerializable supports following properties:

  • disallowUnrecognizedKeys - Should unrecognized keys be ignored in the generated factory fromJson. The default value is false. If true, any unrecognized keys will be treated as an error. Version 1.0.0 only
  • createFactory - Should factory method like _$UserFromJson be generated.
  • createToJson - Should toJson method be generated.
  • includeIfNull - Should serialized output includes null values.
  • nullable - Should handling of null values be included for serialization and deserialization. The default value is true and should be used in most cases. However, if you are sure that all values won't be null, then changing this to false can reduce the size of generated code.

JsonKey

JsonKey is added to a field in the class. It supports following properties:

  • name - The name of corresponding property in the JSON map.
  • nullable - Has the same meaning as nullable in JsonSerializable.
  • includeIfNull - Has the same meaning as includeIfNull in JsonSerializable.
  • ignore - Should the field be ignored.
  • fromJson - The function to convert the field's value from JSON data.
  • toJson - The function to serialize the field's value to JSON.
  • defaultValue - The default value when the source JSON does not contain this key or the value is null. Version 1.0.0 only
  • required - Specify whether the field is required. Version 1.0.0 only
  • disallowNullValue - Specify whether null value is disallowed for the field. Version 1.0.0 only

The code below shows some examples of using JsonSerializable and JsonKey. The JsonKey for firstName specifies the name in JSON data is first_name. The JsonKey for generatedValue specifies this field is ignored in JSON. The JsonKey for date uses custom functions to serialize a DateTime object as int in JSON.

import 'package:json_annotation/json_annotation.dart';
part 'test_json_serializable.g.dart';

()
class Example {
  (name: 'first_name')
  String firstName;

  (ignore: true)
  String generatedValue;

  (fromJson: _dateTimeFromEpochUs, toJson: _dateTimeToEpochUs)
  DateTime date;
}

DateTime _dateTimeFromEpochUs(int us) =>
    new DateTime.fromMicrosecondsSinceEpoch(us);
int _dateTimeToEpochUs(DateTime dateTime) => dateTime.microsecondsSinceEpoch;

JsonLiteral

JsonLiteral generates a private field containing the contents of a JSON file. This annotation is useful when you have static JSON files and want to parse and use them in the code.

Given the JSON file test.json with contents as below.

{
  "a": "hello",
  "b": true,
  "c": 100
}

The dart file below declares a top-level getter with the value dataJsonLiteral from test_literal.g.dart. When using JsonLiteral, we need to provide the relative path of the JSON file. The optional parameter asConst specifies whether the JSON literal should be written as a constant.

import 'package:json_annotation/json_annotation.dart';
part 'test_literal.g.dart';

('test.json', asConst: true)
Map<String, dynamic> get data => _$dataJsonLiteral;

Below is the content of generated file test_literal.g.dart.

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'test_literal.dart';

// **************************************************************************
// JsonLiteralGenerator
// **************************************************************************

const _$dataJsonLiteral = const {'a': 'hello', 'b': true, 'c': 100};

Summary

With the help of json_serializable, we can reduce the amount of hand-written JSON serialization and deserialization code. json_serializable should be used for most Flutter apps.

Source code

Source code of this article is available on GitHub.