Automated JSON Serialization with json_serializable
Automated JSON Serialization with json_serializable
Writing fromJson and toJson methods by hand is tedious and error-prone. The json_serializable package solves this by generating that boilerplate automatically. You annotate your Dart model class, run the build_runner code generator, and the package produces a .g.dart file containing the complete serialization logic. The result is type-safe, zero-maintenance JSON handling.
json_serializable is a dev dependency — it is only needed during development to generate code. The generated .g.dart files are committed to source control and shipped with the app. At runtime, no code-generation tooling is present.Setting Up the Dependencies
Add the required packages to pubspec.yaml. json_annotation provides the annotations you use in your model. json_serializable and build_runner do the code generation at development time.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
json_annotation: ^4.9.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.4.0
json_serializable: ^6.8.0
Run flutter pub get to install the packages before annotating your models.
Annotating a Model Class
To make a class serializable, you need three things:
- Import
package:json_annotation/json_annotation.dart - Add the
partdirective referencing the generated file - Annotate the class with
@JsonSerializable()
user.dart — Annotated model
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart'; // generated file will be placed here
@JsonSerializable()
class User {
final int id;
final String name;
final String email;
@JsonKey(name: 'avatar_url') // map snake_case JSON key to camelCase field
final String avatarUrl;
@JsonKey(name: 'created_at')
final DateTime createdAt;
const User({
required this.id,
required this.name,
required this.email,
required this.avatarUrl,
required this.createdAt,
});
// Factory and method delegate to the generated code:
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
Running the Code Generator
Once your model is annotated, run one of the following commands from the project root:
- One-shot build:
dart run build_runner build --delete-conflicting-outputs - Watch mode:
dart run build_runner watch --delete-conflicting-outputs
The generator creates user.g.dart alongside user.dart. You should never edit this file manually — it will be overwritten on the next build.
.g.dart files instantly whenever you save a change to an annotated class, so you never need to remember to re-run the build manually.What the Generated File Contains
After running the generator, user.g.dart will contain two top-level functions that the model delegates to:
user.g.dart — Generated output (do not edit)
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
User _$UserFromJson(Map<String, dynamic> json) => User(
id: (json['id'] as num).toInt(),
name: json['name'] as String,
email: json['email'] as String,
avatarUrl: json['avatar_url'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
);
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'email': instance.email,
'avatar_url': instance.avatarUrl,
'created_at': instance.createdAt.toIso8601String(),
};
Useful @JsonSerializable Options
The @JsonSerializable annotation accepts optional parameters to customize behaviour:
fieldRename: FieldRename.snake— automatically maps allcamelCasefields tosnake_caseJSON keys without needing a@JsonKeyon every field.explicitToJson: true— required when a field is itself a serializable object; otherwise nested objects are not converted to maps.includeIfNull: false— omits fields withnullvalues from the serialized output.createToJson: false— skip generatingtoJsonwhen you only need deserialization (e.g., read-only API responses).
Nested objects with explicitToJson
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
class Post {
final int id;
final String title;
final User author; // nested serializable object
const Post({required this.id, required this.title, required this.author});
factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
Map<String, dynamic> toJson() => _$PostToJson(this);
}
explicitToJson: true when a model contains nested serializable fields, toJson() will include the nested object as a Dart instance rather than a Map. This typically causes a runtime error or incorrect JSON output when encoding.Handling Lists and Nullable Fields
The generator handles List<T>, Map<K, V>, and nullable types (T?) without extra configuration. Declare the types accurately and the generated code will cast and null-check correctly.
Model with list and nullable fields
@JsonSerializable(explicitToJson: true)
class ApiResponse {
final bool success;
final String? message; // nullable — may be absent in JSON
final List<User> data; // list of nested objects
const ApiResponse({
required this.success,
this.message,
required this.data,
});
factory ApiResponse.fromJson(Map<String, dynamic> json) =>
_$ApiResponseFromJson(json);
Map<String, dynamic> toJson() => _$ApiResponseToJson(this);
}
Summary
The json_serializable workflow keeps your model classes clean and declarative. You describe the shape of your data once using annotations, run the generator, and get battle-tested serialization code for free. Key points to remember:
- Add
json_annotationas a runtime dependency andbuild_runner+json_serializableas dev dependencies. - Annotate the class with
@JsonSerializable()and add thepartdirective. - Run
dart run build_runner build --delete-conflicting-outputsto generate the.g.dartfile. - Use
@JsonKey(name: ...)for custom key mappings andexplicitToJson: truefor nested objects. - Commit generated
.g.dartfiles — they are part of the compiled app.