In the previous article, we have learned how to parse JSON into type-safe model classes in Dart.
But writing all the JSON parsing code by hand becomes time-consuming and error-prone if we have a lot of model classes. Luckily, we can use code generation tools such as json_serializable and Freezed to automate the process.
In this article, we'll learn how to parse JSON data with code generation using the Freezed package. And I'll also share a VS Code extension that makes the process even easier. 🙂
Ready? Let's go!
Installing the codegen dependencies
To get things working we're going to need a few dependencies in our pubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
freezed_annotation: ^0.14.2
json_annotation: ^4.0.1
dev_dependencies:
build_runner: ^2.0.6
freezed: ^0.14.2
json_serializable: ^4.1.4
5 new dependencies? Really?
Yes. Here's what they do:
json_serializable
: provides the Dart Build System with some builders for handling JSONjson_annotation
: defines the annotations used byjson_serializable
freezed
: a powerful code generator that can handle complex use-cases with a simple APIfreezed_annotation
: defines the annotations used byfreezed
build_runner
: this is a standalone build package that can generate Dart files for us
Sounds complicated? Don't worry: As long as you import all the required packages, it will be fine. 👍
You can generate the JSON parsing code with
json_serializable
alone (withoutfreezed
). However,freezed
is more powerful and can handle complex use-cases with a simple API.
A sample JSON document
To keep things in line with the previous article, we'll reuse the same sample JSON:
{
"name": "Pizza da Mario",
"cuisine": "Italian",
"year_opened": 1990,
"reviews": [
{
"score": 4.5,
"review": "The pizza was amazing!"
},
{
"score": 5.0,
"review": "Very friendly staff, excellent service!"
}
]
}
For reference, here are the Restaurant
and Review
model classes we had written before:
class Restaurant {
Restaurant({
required this.name,
required this.cuisine,
this.yearOpened,
required this.reviews,
});
final String name;
final String cuisine;
final int? yearOpened;
final List<Review> reviews;
factory Restaurant.fromMap(Map<String, dynamic> data) {
final name = data['name'] as String;
final cuisine = data['cuisine'] as String;
final yearOpened = data['year_opened'] as int?;
final reviewsData = data['reviews'] as List<dynamic>?;
final reviews = reviewsData != null
? reviewsData.map((reviewData) => Review.fromMap(reviewData)).toList()
: <Review>[];
return Restaurant(
name: name,
cuisine: cuisine,
yearOpened: yearOpened,
reviews: reviews,
);
}
Map<String, dynamic> toMap() {
return {
'name': name,
'cuisine': cuisine,
if (yearOpened != null) 'year_opened': yearOpened,
'reviews': reviews.map((review) => review.toMap()).toList(),
};
}
}
class Review {
Review({required this.score, this.review});
final double score;
// nullable - assuming the review may be missing
final String? review;
factory Review.fromMap(Map<String, dynamic> data) {
final score = data['score'] as double;
final review = data['review'] as String?;
return Review(score: score, review: review);
}
Map<String, dynamic> toMap() {
return {
'score': score,
if (review != null) 'review': review,
};
}
}
As we can see, this is a lot of code and this approach doesn't scale if we have many different models.
Model classes with Freezed
To make our life easier, let's use Freezed to define our Restaurant
and Review
model classes.
Since Restaurant
depends on Review
, let's start with the Review
class:
// review.dart
// 1. import freezed_annotation
import 'package:freezed_annotation/freezed_annotation.dart';
// 2. add 'part' files
part 'review.freezed.dart';
part 'review.g.dart';
// 3. add @freezed annotation
@freezed
// 4. define a class with a mixin
class Review with _$Review {
// 5. define a factory constructor
factory Review({
// 6. list all the arguments/properties
required double score,
String? review,
// 7. assign it with the `_Review` class constructor
}) = _Review;
// 8. define another factory constructor to parse from json
factory Review.fromJson(Map<String, dynamic> json) => _$ReviewFromJson(json);
}
It's very important to use the correct syntax here. If we miss something or add a typo, our code generator will produce some errors.
Let's do the same for the Restaurant
class:
// restaurant.dart
import 'package:freezed_annotation/freezed_annotation.dart';
// import any other models we depend on
import 'review.dart';
part 'restaurant.freezed.dart';
part 'restaurant.g.dart';
@freezed
class Restaurant with _$Restaurant {
factory Restaurant({
required String name,
required String cuisine,
// note: using a JsonKey to map our JSON key that uses
// *snake_case* to our Dart variable that uses *camelCase*
@JsonKey(name: 'year_opened') int? yearOpened,
// note: using an empty list as a default value
@Default([]) List<Review> reviews,
}) = _Restaurant;
factory Restaurant.fromJson(Map<String, dynamic> json) =>
_$RestaurantFromJson(json);
}
Note how both the Restaurant
and Review
classes have a factory constructor listing all the arguments we need, but we haven't declared the corresponding properties.
In fact our code is incomplete and will produce errors like these:
Target of URI doesn't exist: 'restaurant.freezed.dart'.
Try creating the file referenced by the URI, or Try using a URI for a file that does exist.
The name '_Restaurant' isn't a type and can't be used in a redirected constructor.
Try redirecting to a different constructor.
The method '_$RestaurantFromJson' isn't defined for the type 'Restaurant'.
Try correcting the name to the name of an existing method, or defining a method named '_$RestaurantFromJson'.
Let's take care of this.
Running the code generator
To generate the missing code, we can run this on the console:
dart run build_runner build -d
This will produce the following output:
[INFO] Generating build script...
[INFO] Generating build script completed, took 419ms
[INFO] Initializing inputs
[INFO] Reading cached asset graph...
[INFO] Reading cached asset graph completed, took 55ms
[INFO] Checking for updates since last build...
[INFO] Checking for updates since last build completed, took 428ms
[INFO] Running build...
[INFO] 1.3s elapsed, 0/2 actions completed.
[INFO] Running build completed, took 2.1s
[INFO] Caching finalized dependency graph...
[INFO] Caching finalized dependency graph completed, took 27ms
[INFO] Succeeded after 2.1s with 5 outputs (5 actions)
And if we look in the project explorer, we can find some new files:
restaurant.dart
restaurant.freezed.dart
restaurant.g.dart
review.dart
review.freezed.dart
review.g.dart
The .freezed.dart
files contain a lot of code. If you want to see all the generated code, you can check this gist.
What's important is that for each model class, the code generator has added:
- all the stored properties that we need (and made them
final
) - the
toString()
method - the
==
operator - the
hashCode
getter variable - the
copyWith()
method - the
toJson()
method
Quite handy!
And if we ever need to modify any of the properties in our model classes, we just need to update their factory constructors:
@freezed
class Review with _$Review {
factory Review({
// update any properties as needed
required double score,
String? review,
}) = _Review;
factory Review.fromJson(Map<String, dynamic> json) => _$ReviewFromJson(json);
}
@freezed
class Restaurant with _$Restaurant {
factory Restaurant({
// update any properties as needed
required String name,
required String cuisine,
@JsonKey(name: 'year_opened') int? yearOpened,
@Default([]) List<Review> reviews,
}) = _Restaurant;
factory Restaurant.fromJson(Map<String, dynamic> json) =>
_$RestaurantFromJson(json);
}
Then we can run the code generator again and Freezed will take care of the rest:
dart run build_runner build -d
Voilà! We can now define type-safe, immutable model classes in few lines of code, and generate all the JSON serialization code by running a single command.
Basic JSON annotations
Freezed supports many annotations that let us customize how the code generator processes our models.
The most useful ones are @JsonKey
and @Default
.
Here's an example of how I'm using them on my Movie App on GitHub:
@freezed
class TMDBMovieBasic with _$TMDBMovieBasic {
factory TMDBMovieBasic({
@JsonKey(name: 'vote_count') int? voteCount,
required int id,
@Default(false) bool video,
@JsonKey(name: 'vote_average') double? voteAverage,
required String title,
double? popularity,
@JsonKey(name: 'poster_path') required String posterPath,
@JsonKey(name: 'original_language') String? originalLanguage,
@JsonKey(name: 'original_title') String? originalTitle,
@JsonKey(name: 'genre_ids') List<int>? genreIds,
@JsonKey(name: 'backdrop_path') String? backdropPath,
bool? adult,
String? overview,
@JsonKey(name: 'release_date') String? releaseDate,
}) = _TMDBMovieBasic;
factory TMDBMovieBasic.fromJson(Map<String, dynamic> json) =>
_$TMDBMovieBasicFromJson(json);
}
In this case, the keys in the JSON response use a snake_case
naming convention, and we can use the @JsonKey
annotation to tell Freezed which keys are mapped to which properties.
And we can use the @Default
annotation if we want to specify a default value for a given non-nullable property.
Non-nullable arguments need either a
required
keyword or a default value. If a@Default
annotation is specified, its value will be used if the corresponding JSON key-value pair is missing.
Advanced JSON Serialization features with Freezed
What we have covered so far is enough to handle JSON serialization in most cases.
But Freezed is a powerful package and we can do cool things such as:
- generate union types by specifying multiple constructors
- specify custom JSON converters
To learn about the most advanced features, read this section on the documentation:
Code Generation Drawbacks
Code generation has some clear benefits and it's the way to go if you have many model classes.
But there are some drawbacks too:
A lot of extra code
Our Restaurant
and Review
model classes are very simple, but the generated code already takes up 450 lines of code. This quickly adds up if you have many model classes.
Code generation is slow
Code generation in Dart is quite slow.
Even though there are ways to mitigate this, codegen can significantly slow down your development workflow on big projects.
Should generated files be added to git?
If you work in a team and you commit the generated files to git, pull requests become harder to review.
But if you don't, the project is not in a runnable state by default and:
- every team member needs to remember to run the codegen step (potentially leading to inconsistencies)
- a custom CI build step is required to build the app
And according to this poll, there isn't even a consensus on whether generated files should be added to git.
Dart Language Limitations
Code generation can help, but it's not a silver bullet.
The underlying problem is that Dart doesn't (yet) have any language features that make JSON serialization easier.
The introduction of data classes - and more broadly static metaprogramming in Dart - has the potential to solve these problems. So we can hope that in the future JSON serialization will become much easier in Dart.
But given the current language limitations, code generation with Freezed is our best option.
VSCode Extension: Json to Dart Model
As we have learned, we can declare some model classes with their factory constructors, and let the code generator take care of everything else.
But wouldn't it be cool if we could generate everything directly from sample JSON documents?
Well, the Json to Dart Model VSCode extension does exactly this.
Having tried it, I can say that it does the job quite well and it supports Freezed under the hood. So if you want to save some extra time, consider using it.
There is also an online tool called QuickType that can generate type-safe model classes in any language. But it doesn't support Dart Null Safety just yet.
Conclusion
We have now explored various options for JSON serialization.
Here are some guidelines to help you choose the best approach:
- if you have a few, small model classes, you can write the JSON parsing code by hand.
- if you have many model classes, Freezed can do the heavy-lifting for you.
- to speed things up even more, consider using the Json to Dart Model extension for VS Code.
Want to get some practice with JSON parsing?
Then you can choose a public free API from this massive list and build a Flutter app with it. As always, practice is the best way to learn. 👍
Happy coding!