When it comes to working with JSON data in Flutter, there are two primary approaches: Manual serialization and Automated serialization.
Manual serialization requires us to manually convert our Dart objects to JSON and vice versa. This can be time-consuming and error-prone, especially when dealing with complex data structures.
Automated Serialization, on the other hand, is a much more efficient and less error-prone approach.
In this article, we will dive deep into the various options available in the JsonSerializable
constructor and how we can use them to customize the serialization and deserialization of our JSON data.
Positive thinking leads to positive outcomes. Try out Justly, build good habits, and start thinking positively today!
The json_serializable package is a code generation library for Dart that simplifies the process of serializing and deserializing JSON data. With json_serializable, you can annotate your model classes with the json_annotation package, which tells the library which classes to generate serialization and deserialization code for.
To generate the code, you simply need to run the code generator using the build_runner package. This generates the Dart files necessary for the serialization and deserialization process.
In Pubspec.yaml
add the following dependencies.
dependencies:
flutter:
sdk: flutter
json_annotation: ^4.8.0
dev_dependencies:
flutter_test:
sdk: flutter
json_serializable:
build_runner:
We added json_annotation as a development dependency and json_serializable and build_runner packages in dev-dependencies as these packages are only used during development to generate the serialization code.
To implement automatic serialization and deserialization in Dart using the json_serialiazable
package, we need to set up our model class and generate a serialization code.
First, add the necessary imports and annotations at the beginning of your dart file.
import 'package:json_annotation/json_annotation.dart';
part 'movie.g.dart';
@JsonSerializable()
class Movie{
final String name;
final DateTime dateOfRelease;
Movie({
required this.name,
required this.dateOfRelease,
});
factory Movie.fomJson(Map<String,dynamic> json)=>_$MovieFromJson(json);
Map<String, dynamic> toJson(Movie movie)=>_$MovieToJson(this);
}
The @JsonSerializable()
annotation is used to indicate that the Movie
class should have a serialization code generated for it. Note that we also include part directives to indicate that this file is part of a generated file.
In addition, we need to define two methods: fomJson
and toJson
. These methods are used for serialization and deserialization, respectively.
To generate the serialization code, we need to run the build_runner command in the terminal: dart run build_runner build
. This command generates the necessary serialization code and saves it to a new file with the name <class_name>.g.dart. After running the command, you’ll see a new file named movie.g.dart generated next to the movie.dart file.
After making any changes to the
json_serializable
annotations, it is essential to run the code generator to generate the updated serialization and deserialization code.
JsonSerializable
constructor parameters.const JsonSerializable({
this.anyMap,
this.checked,
this.constructor,
this.createFieldMap,
this.createFactory,
this.createToJson,
this.disallowUnrecognizedKeys,
this.explicitToJson,
this.fieldRename,
this.ignoreUnannotated,
this.includeIfNull,
this.converters,
this.genericArgumentFactories,
this.createPerFieldToJson,
});
The anyMap
parameter of the @JsonSerializable
annotation is used to handle JSON objects that contain Map
types with non-String keys. By default, anyMap
is set to false
, which means that Map
types are expected to have String
keys. However, in situations where JSON data may contain maps with different key-value types, we can set anyMap
to true.
For example, let’s say we have a Map movieReview
like this,
final data={
'name':'XXX',
'dateOfRelease':DateTime.now().toString(),
'movieReview':
{
1:50,
'review':'YYY'
}
};
It will throw Unhandled Exception: type ‘_Map<Object, Object>’ is not a subtype of type ‘Map<String, dynamic>’ in type cast.
To deserialize JSON objects with maps that have non-string keys, we can set anyMap
to true.
@JsonSerializable(anyMap: true)
class Movie {
...
final Map movieReview;
const Movie({
...
required this.movieReview
});
factory Movie.fomJson(Map<String,dynamic json)=>_$MovieFromJson(json);
Map<String, dynamic> toJson()=>_$MovieToJson(this);
}
It is important to note that if anyMap
is set to true, the generated serialization method will use Map<dynamic, dynamic>
to serialize an object, which may not be ideal in some cases. In those situations, you can consider writing a custom toJSon
method that explicitly converts the anyMap
to a Map<String, dynamic>.
By default, checked
is set to false. By setting checked
to true, the generated code will validate the input JSON data matches the expected data types, and if there’s a mismatch, it will throw a CheckedFromJsonException
with the details of the error.
When we try to parse a JSON map that has a dateOfRelease
value of type int instead of DateTime,
var movieData = {
'name': 'XXX',
'dateOfRelease': 2
};
Movie movie = Movie.fromJson(movieData);
It will throw a CheckedFromJsonException
with the following message,
Unhandled Exception: CheckedFromJsonException
Could not create `Movie`.
There is a problem with "dateOfRelease".
type 'int' is not a subtype of type 'String' in type cast
Isn’t that cool? It is generally a good idea to set the checked
to true during development to catch any potential errors early, and then set it to false in production for better performance. No more hunting-down type errors during runtime!
The constructor
parameter allows us to specify a named constructor to target when creating the fromJSon
function. If a named constructor is specified, the fromJSon
function will use that instead of the default constructor.
Let’s say we have a Movie
class with a private constructor,
@JsonSerializable(constructor: '_')
class Movie {
final String name;
final DateTime dateOfRelease;
Movie({
required this.name,
required this.dateOfRelease,
});
Movie._(this.name, this.dateOfRelease);
factory Movie.fromJson(Map<String,dynamic> json)=>_$MovieFromJson(json);
Map<String, dynamic> toJson()=>_$MovieToJson(this);
}
In this case, the private constructor _
is used to create a new instance of the Movie class from the JSON data instead of the default constructor. This can be useful when you need to perform some additional logic or validation before creating an object from the JSON data.
The default value is true, and a private, static method called _$MovieFromJson
is generated in the part file. This is responsible for creating a new instance of the class from a JSON Map.
However, there may be cases where we want to perform some additional validation on the input data before creating the instance of the class, In such cases, we can set createFactory
to false and write down our own factory constructor.
Let’s add some validation for Movie
class.
@JsonSerializable(createFactory: false)
class Movie {
final String name;
final DateTime dateOfRelease;
Movie({
required this.name,
required this.dateOfRelease,
});
factory Movie.fromJson(Map<String, dynamic> json) {
if (json['name'] == '') {
throw ArgumentError('Name can\'t be a empty String');
}
return Movie(name: json['name'], dateOfRelease: json['dateOfRelease']);
}
Map<String, dynamic> toJson() => {'name': name, 'dateOfRelease': dateOfRelease};
}
Disabling createFactory
can be a good option if you need more fine-grained control over the deserialization process. However, keep in mind that you will need to write your own factory constructor, which may require extra work.
By default, its value is set as false, When set to true, it generates a constant map that maps the class field names to the corresponding JSON keys. For instance, if we set createFieldMap
to true for our Movie
class, the generated file has the following method,
const _$MovieFieldMap = <String, String>{
'name': 'name',
'dateOfRelease': 'dateOfRelease',
};
This constant map is useful when using other code generators to provide features such as field renaming. However, it’s not necessary for basic serialization and deserialization using the generated methods.
Its default value is false. If set to true, it generates a private, static abstract class in the generated part file. This class contains static methods that correspond to each field in the annotated class, and are responsible for encoding only that field into JSON.
It generates _$MoviePerFieldToJson
for Movie
class like this,
// ignore: unused_element
abstract class _$MoviePerFieldToJson {
// ignore: unused_element
static Object? name(String instance) => instance;
// ignore: unused_element
static Object? dateOfRelease(DateTime instance) => instance.toIso8601String();
}
We can use this method to encode only a single property of the Movie
object like this,
final nameJson = _$MoviePerFieldToJson.name('XXX');
This can be especially useful if you have a large object with many fields, but only need to encode a subset of those fields at a time.
By default, the value of createToJson
is true and is used to specify whether a top-level function should be created to generate a JSON representation of the object. However, you can set it to false, if you don’t want the library to generate the corresponding function in the generated file.
Let’s set it to false for Movie
class
part of 'movie.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Movie _$MovieFromJson(Map<String, dynamic> json) => Movie(
name: json['name'] as String,
dateOfRelease: DateTime.parse(json['dateOfRelease'] as String),
);
When createToJson
is set to false, the library won’t generate the _$MovieToJson
function in the generated file.
By default, this option is set to false, which means that any unrecognized keys in the JSON map will simply be ignored by the generated fromJson
method. This is often useful when working with APIs that may send additional data that your application does not need to use.
However, if set to true, any unrecognized keys in the JSON map will cause an UnrecognizedKeysException
to be thrown.
Let’s set disAllowUnrecognisedKeys
to true for Movie
class and try to deserialize JSON data that contains an extra director
field like this,
final data={
'name':'XXX',
'dateOfRelease':DateTime.now().toString(),
'director':'RobertoS'
};
Movie movie= Movie.fromJson(data);
It will throw UnrecognizedKeysException,
Unhandled Exception: UnrecognizedKeysException: Unrecognized keys: [director]; supported keys: [name, dateOfRelease]
This can be useful in cases where you want to ensure that the data being deserialized adheres strictly to a defined schema.
By default, this option is set to false, which means that generated toJson method will omit the toJson call when encoding nested objects.
If we have Movie
class with a nested object Review
and want to ensure that toJson
method of Review
is always called when encoding a Movie, the explicitToJson
can be set as true.
fieldRename
determines how class field names are automatically converted to JSON map keys. By default, if no value is provided, the FielsRename.none
is used, which means that the name of the field is used without modification.
When we want to store keys in a snake case, we can set fieldRename
to FieldRename.snake
.
@JsonSerializable(fieldRename: FieldRename.snake)
class Movie {
....
}
//generated file
Movie _$MovieFromJson(Map<String, dynamic> json) => Movie(
name: json['name'] as String,
dateOfRelease: DateTime.parse(json['date_of_release'] as String),
);
Map<String, dynamic> _$MovieToJson(Movie instance) => <String, dynamic>{
'name': instance.name,
'date_of_release': instance.dateOfRelease.toIso8601String(),
};
The generated JSON Keys are in the snake case. Other available options include FieldRename.snake
, FieldRename.kebab
, FieldRename.pascal,
and FieldRename.screamingSnake
.
By default, its value is set to false, which means that all fields in your class will have code generated. But if we set it to true, only the fields that are explicitly annotated with JsonKey
will have code generated, and the Unannotated fields will be ignored.
If we only want to include the name
field in the JSON code,
@JsonSerializable(ignoreUnannotated: true)
class Movie {
@JsonKey()
final String name;
final DateTime? dateOfRelease;
....
}
And just like that, the dateOfRelease
field will be ignored when generating JSON code,
Movie _$MovieFromJson(Map<String, dynamic> json) => Movie(
name: json['name'] as String,
);
Map<String, dynamic> _$MovieToJson(Movie instance) => <String, dynamic>{
'name': instance.name,
};
Great!! 👍. It saves you from the hassle of having to add JSonKey(includeToJson: false, includeFromJson: false)
to all of the unneeded fields.
By default, its value is set to true, which means all fields, even if they are null, will be included in the serialized output. But If we set it to false, then any nullable fields with a value of null
will be omitted from the serialized output.
If we deserialize a JSON map with a null
value for dateOfRelease
.
final data={
'name':'XXX',
'dateOfRelease':null,
};
Movie movie= Movie.fromJson(data);
print(movie.toJson());
//Output
{name: XXX,dateOfRelease: null}
And if we set includeIfNull
to false, then the dateOfRelease
will be omitted.
final data={
'name':'XXX',
'dateOfRelease':null,
};
Movie movie= Movie.fromJson(data);
print(movie.toJson());
//Output
{name: XXX}
So, what do you think? Do you want all the fields to be included in the JSON output even if they are null or would you prefer a more concise output that only includes non-null values? Yes, it is useful if you want to be more selective and leave some things behind to make your output cleaner and more organized.
What if you want to convert dateOfRealease
to and from int
representation to DateTime
? In this scenario, you can use converters
property to specify a list of JsoConverters
objects. JsonConverter
is convenient if you want to use the same conversion logic on many fields.
Let’s create a custom DateConverter
class that implements the JSonConverter
to convert DateTime
object and an int
representation of the date.
@JsonSerializable(converters: [DateConverter()])
class Movie {
....
}
class DateConverter implements JsonConverter<DateTime,int>{
const DateConverter();
@override
DateTime fromJson(int json) {
return DateUtils.dateOnly(DateTime.fromMillisecondsSinceEpoch(json));
}
@override
int toJson(DateTime dateTime) {
return DateUtils.dateOnly(dateTime).millisecondsSinceEpoch;
}
}
Okay, Let’s test it.
void main() {
final data={
'name':'Sultan',
'dateOfRelease':123,
};
Movie movie= Movie.fromJson(data);
print(movie.dateOfRelease);
}
//Output:
1970-01-01 00:00:00.000
The main benefit of using Jsonconverters
is that converters allow for the reusability of the same conversion logic across multiple fields or classes.
The genericArgumentFactories
is a helpful little guy that generates extra parameters for fromJson
and toJson
to support serializing values of generic types.
To enable this option, simply set it to true.
@JsonSerializable(genericArgumentFactories: true)
class Movie<T> {
final DateTime dateOfRelease;
final T value;
Movie({
required this.value,
required this.dateOfRelease,
});
factory Movie.fromJson(Map<String, dynamic> json, T Function(Object? json) fromJsonT) => _$MovieFromJson(json,fromJsonT);
Map<String, dynamic> toJson(Movie<T> movie,Object Function(T value) toJsonT) => _$MovieToJson(movie,toJsonT);
}
It generates the additional helper parameter FromJsonT
for the fromJson
and toJsonT
for the toJson
method. This helper parameter allows us to deserialize and serialize generic types.
void main() {
final data={
'value':'XXX',
'dateOfRelease':DateTime.now().toString(),
};
Movie movie= Movie.fromJson(data,(json)=>json as String);
}
And that’s a wrap!
In this article, we have covered the basics of how to use json_serializable, including the use-case of each parameter of JsonSerializable
annotation and how it can be useful to customize serialization and deserialization.
I hope you found this article helpful in understanding how to use json_serializable effectively.
Thank you for reading, Happy coding! 👋