JSON and Serialization in Flutter
Part 2: Serialization using Code generation and Code for nested classes
Serialization using Code generation
It means using external libraries to generate the encoding boilerplate. Libraries like json_serializable and built_value. This can be used for medium to large-scale projects, but it has a drawback. Code generation requires some initial setup and the generated source files might produce visual clutter in your project navigator.
Setting up json_serializable in a project
For setting up json_serializable package, you need one regular dependency and two dev dependencies. The code should look something like this, with the <latest_version> replaced with the latest version of the package.
pubspec.yamldependencies:
# Your other regular dependencies here
json_annotation: <latest_version>dev_dependencies:
# Your other dev_dependencies here
build_runner: <latest_version>
json_serializable: <latest_version>
Run the fultter pub get after this change in pubspec.yaml
Creating model classes the json_serializable way
user.dartimport 'package:json_annotation/json_annotation.dart';/// This allows the `User` class to access private members in
/// the generated file. The value for this is *.g.dart, where
/// the star denotes the source file name.
part 'user.g.dart';/// An annotation for the code generator to know that this class needs the
/// JSON serialization logic to be generated.
@JsonSerializable()
class User {
User(this.name, this.email);String name;
String email;/// A necessary factory constructor for creating a new User instance
/// from a map. Pass the map to the generated `_$UserFromJson()` constructor.
/// The constructor is named after the source class, in this case, User.
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);/// `toJson` is the convention for a class to declare support for serialization
/// to JSON. The implementation simply calls the private, generated
/// helper method `_$UserToJson`.
Map<String, dynamic> toJson() => _$UserToJson(this);
}
If needed, it is also easy to customize the naming strategy. For example, if the API returns objects with snake_case, and you want to use lowerCamelCase in your models, you can use the @JsonKey
annotation with a name parameter:
/// Tell json_serializable that "registration_date_millis" should be
/// mapped to this property.
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;
It’s best if both server and client follow the same naming strategy.@JsonSerializable()
provides fieldRename
enum for totally converting dart fields into JSON keys.
Modifying @JsonSerializable(fieldRename: FieldRename.snake)
is equivalent to adding @JsonKey(name: '<snake_case>')
to each field.
Sometimes server data is uncertain, so it is necessary to verify and protect data on the client.
Other commonly used @JsonKey
annotations include:
/// Tell json_serializable to use "defaultValue" if the JSON doesn't
/// contain this key or if the value is `null`.
@JsonKey(defaultValue: false)
final bool isAdult;/// When `true` tell json_serializable that JSON must contain the key,
/// If the key doesn't exist, an exception is thrown.
@JsonKey(required: true)
final String id;/// When `true` tell json_serializable that generated code should
/// ignore this field completely.
@JsonKey(ignore: true)
final String verificationCode;
Code generation utility
- One time code generation-
By running flutter pub run build_runner build you can generate JSON serialization code for your models whenever they are needed. - Continuous code generation-
A watcher makes our source code generation process more convenient. It watches changes in our project files and automatically builds the necessary files when needed. Start the watcher by running flutter pub run build_runner watch in the project root.
Consuming json_serializable models
decoding
Map<String, dynamic> userMap = jsonDecode(jsonString);
var user = User.fromJson(userMap);encoding
String json = jsonEncode(user);
The source code generator creates a file called user.g.dart, which has all the necessary serialization logic. We no longer have to write automated tests to ensure that the serialization works—it’s now the library’s responsibility to make sure the serialization works appropriately.
Generating code for nested classes
Why this is a different topic, you may ask? And this question is valid too. If we have two model classes, one using another in it, you will see unexpected behavior. The model will not work as expected, denoting that the member which we used with our custom type is just an instance of that type. But, we don’t want this to happen. To make this work, pass explicitToJson: true
in the @JsonSerializable()
annotation over the class declaration. The User
class now looks as follows:
import 'package:json_annotation/json_annotation.dart';import 'address.dart';part 'user.g.dart';@JsonSerializable(explicitToJson: true)
class User {
User(this.name, this.address);String name;
Address address;factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}