Office Time
Photo by Daniel Fazio / Unsplash

"Flutter is an open source framework by Google for building beautiful, natively compiled, multi-platform applications from a single codebase." as Flutter official site says. I started to study Flutter a few months ago and have been participated in several projects at MDG. There are tons of packages which developers can use to make their apps look fine. Today I want to share the way of JSON serializing in Flutter with json_serializable package.

Basic way to fetch JSON data in Flutter

Using flutter to develop mobile and web apps, developers need to connect the app with servers. To use data from servers, JSON type data is the good old type to use.

In the official docs, they separate into two methods for JSON serialization, one for smaller projects and the other, for bigger projects. For bigger size projects, docs recommend using an automatic code generator with external libraries. If the project is small to handle manually, here won’t critical problems. Built-in JSON decoder in dart:convert can work it out for you. However, when we type decoding logics for bigger and more complicated projects, it can derive errors from typos or unmatched type errors easily. Docs mentioned two flutter packages which are json_serializable and built_value. I used json_serializable for its higher likes point from pub.dev site.

Why use this package?

  • prevent small errors such as typos when your Flutter project’s scale is heavier and more complicated.
  • Don’t need to type every class and method every time to avoid making typo errors when the models become complicated and also when we need to create many models.

Let's understand with real examples

This is a simple example for User model.

{“name” : “user name”, “email” : “user@email.com“}

But in Flutter you can’t approach the data directly. You need to convert it as json format with dart:convert. You can write down the model to handle the data such as the following example manually written.

 final String name;
 final String email;

 User(this.name, this.email);

 User.fromJson(Map<String, dynamic> json)
   : name = json['name'],
    email = json['email'];

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

So with User.fromJson, User data can be constructed from pure map data. On the contrary, with toJson, User instance can be converted to the Map data.  As I already mentioned at the beginning of this blog, we can still create a model to take care of our data manually. However let’s imagine that we need to make a class with several different types of data, and even not a single class. Several different classes to handle, you can type them down.


Still, I feel you how much pain and how many hours you(and I) have been through to take care of an error which for real occurred for just one single typo. We all have this kind of experiences. We post a question on communities, StackOverFlow, or Twitter to solve this problem but eventually, it can be this one single typo.

To prevent this unnecessary energy consumption, at least when it comes to JSON serializing, we can ask the json_serializable package some help to take our burdens!

I would like to start to implement json_serializable package in the project.
To do so, you can add pubspec.yaml in dependencies dev_dependencies

dependencies:
 # Your other regular dependencies here
 json_annotation: <latest_version>

dev_dependencies:
 # Your other dev_dependencies here
 build_runner: <latest_version>
 json_serializable: <latest_version>

Then run flutter pub get to make these dependencies available in your project. After you implement packages successfully, now we need to make a proper model class but in the json_serializable way.

To make a similar example from a manually written User class, let’s make another model called User. When you make a new file for User class you can name it as user.dart (or whatever you want ), then import json_annotation in the file.

import 'package:json_annotation/json_annotation.dart';

Then you can write down this code in the following below line.

Part ‘user.g.dart’;

If you named this file in a different way, this line should be <your_filename>.g.dart. You only need to add a letter g before .dart of this current file. This is the way you let the package know which file should be generated and matched with this class of the file.

Write down an annotation for the code generator.

@JsonSerializable()
class User {
 User(this.name, this.email);

 String name;
 String email;

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

  Map<String, dynamic> toJson() => _$UserToJson(this);
}

The first look of simply written model is like this. Not so different from the manually written down version.
The most obvious different part from manual version is

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

  Map<String, dynamic> toJson() => _$UserToJson(this);

<Model>from.Json , _$<Model>FromJson(json)and _$<Model>ToJson(this) are the most different part from manual version. And you will see the error warning indicator from the editor or viewer since the fromJson and toJson functions don’t exist. Now we can create those functions!

Once you are done with writing this part, you run flutter pub run build_runner build command in terminal, Json serialization code will be generated with the file name <filename>.g.dart in the same directory of <filename>.dart. When the creating process is done, you can check error warning signs are gone. Now it’s time to check the newly created file!

You can check _$<Model>.ToJson and ‘.fromJson’ functions are created with all the proper types needed for the original model. user.g.dart file probably look like this.

User _$UserFromJson(Map <String, dynamic> json) =>
User(
 Name: json[‘name’] as String,
 Email : json[‘email’] as String,
) 

Map<String, dynamic> _$UserToJson(User instance) => 
<String, dynamic> {
‘name’ : instance.name, ‘email’: instance.email
}

At this level, you might think it’s still not so much better than writing down the model and Json handling functions manually.
However, let’s have a look into an example more complicated.

import 'package:json_annotation/json_annotation.dart';
part ‘complicated_model.g.dart';

@JsonSerializable(fieldRename: FieldRename.snake)
class ComplicatedModel {
 final int id;
 final int tabletId;
 final int StreetId;
 final int anno;
 final DateTime validDateData;
 final String validTimeData;
  final int protocolNumber;
 final int ValidStatus;
 final String vehicleType;
 final String vehicleModel;
 final String vehicleVersion;
 final String createdAt;
 final String updatedAt;
 @JsonKey(defaultValue: [])
 final List<Map> users;
 @JsonKey(defaultValue: [])
 final List<Map> fines;
 @JsonKey(defaultValue: [])
 final List<Map> violations;
 @JsonKey(defaultValue: {})
 final Map<String, dynamic>? street;
 @JsonKey(defaultValue: {})
 final Map<String, dynamic> state;
 final dynamic serialNumber;

ComplicatedModel(
{required this.id,
.
.
.
//for other required fields..
})

 factory ComplicatedModel.fromJson(Map<String, dynamic> data) =>
   _$ComplicatedModelFromJson(data);

 Map<String, dynamic> toJson() => _$ComplicatedModelToJson(this);
}

This is roughly written down, though, from this you can easily imagine what would be if you have to write FromJson and ToJson with each right data type without any typo. json_serializable package will do this for you. You just need to build the class structure!

You may notice @JsonKey() from this example using properties (fileName, includeIfNull, etc), json annotation (Json_Key). I assume you are already familiar with some basic Dart grammar. The most representative one could be in Flutter you use ‘camelCase’ for the key names.If the returned API data is written in snake_case, you can modify as @JsonSerializable(fieldRename: FieldRename.snake), it will cover all the fields corresponding to the snake_case field from API side. Writing @JsonKey(name: ‘<snake_case>) above each needed field works in the same way. You can choose between covering every field from the begging of the class or setting for each field as it’s needed.


There are a few more useful annotations,

@JsonKey(defaultValue: <defaultValue>)
//You can set a default value for each field for the case JSON doesn’t have a value or null value.

@JsonKey(required : true)
//When required is true, if the key doesn’t exist, an exception will be thrown.

@JsonKey(ignore : true)
//You can let json_serializable ignore this field when generates the code

Conclusion

We have a look through the usage of json_serializable package to generate more complicated Models in Flutter. Even manually the code can be written still, I suggest you explore the way of using this package and be free from making unwanted errors by typing complicated models.
You can put the setting as the way you want to handle the data depending on their writing cases, handling null or required value with @Jsonkey annotations. Next time, we will take a step to implement an automatically generated class instance to Dropdownsearch package.

•References in this article
https://flutter.dev/docs/development/data-and-backend/json
https://pub.dev/packages/json_annotation
https://pub.dev/packages/json_serializable