Best Practices for JSON Serialization and Deserialization in Flutter Using json_serializable
This article explains the fundamentals of JSON serialization and deserialization in Flutter, compares manual and automated approaches, introduces the json_serializable library and its annotations, demonstrates generic handling, custom converters, and tooling to streamline model generation for robust mobile app development.
1. Introduction
In a previous article the author noted that the JSON serialization/deserialization and network request sections were simplistic and not suitable for real projects, so this section presents the author’s best practices for handling JSON in Flutter, inviting readers to share alternative methods.
QuickType + json_serializable can generate model classes from backend JSON, use generics to reduce duplicate models, configure build.yaml to avoid repetitive property settings, and customize field serialization as needed.
2. Basics
2.1 JSON Serialization vs. Deserialization
Serialization : Object → JSON string, used when sending a request.
Deserialization : JSON string → Object, used when parsing a response.
2.2 Flutter Support for JSON
Flutter provides the dart:convert library with jsonEncode and jsonDecode methods. A simple example:
import 'dart:convert';
void main() {
String jsonString = '{"name": "Jay", "age": 30}';
// Decode (deserialization)
Map
userMap = jsonDecode(jsonString);
print(userMap); // {name: Jay, age: 30}
// Encode (serialization)
Map
userMap2 = {'name': 'Jay', 'age': 30};
String jsonString2 = jsonEncode(userMap2);
print(jsonString2); // {"name":"Jay","age":30}
}In practice developers create model classes with fromJson() and toJson() methods, typically named as such by convention.
When json.encode() is called, Dart automatically looks for a toJson() method on the object, as defined by JsonEncoder .
2.3 Why JSON Handling in Flutter Can Be Cumbersome
Manual field mapping leads to repetitive work and potential errors. Unlike Java’s runtime reflection (Gson/Jackson), Flutter disables reflection to preserve tree‑shaking, so code generation is the recommended solution.
The official recommendation is to use the json_serializable code‑generation tool.
3. json_serializable Library
The library solves automatic generation of JSON (de)serialization code.
3.1 Adding Dependencies
json_annotation – defines annotations.
json_serializable – generates code based on annotations.
build_runner – runs the code‑generation tasks.
Two ways to add them:
# Method 1: command line
flutter pub add json_annotation dev:build_runner dev:json_serializable
# Method 2: edit pubspec.yaml
dependencies:
flutter:
sdk: flutter
json_annotation: ^4.8.1
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.4.7
json_serializable: ^6.7.13.2 Basic Usage
Annotate a model class with @JsonSerializable() and add fromJson() and toJson() factories:
import 'package:json_annotation/json_annotation.dart';
part 'banner.g.dart';
@JsonSerializable()
class Banner {
@JsonKey(name: 'id')
final int bid;
final String desc;
final String imagePath;
final int isVisible;
final int order;
final String title;
final int type;
final String url;
Banner({required this.bid, required this.desc, required this.imagePath, required this.isVisible, required this.order, required this.title, required this.type, required this.url});
factory Banner.fromJson(Map
json) => _$BannerFromJson(json);
Map
toJson() => _$BannerToJson(this);
}Run the generator:
flutter pub run build_runner build --delete-conflicting-outputsThe command creates a banner.g.dart file with the generated serialization logic.
3.3 Advanced Usage
3.3.1 Generics
Many APIs return a wrapper object with data , errorCode , and errorMsg . Using generic response classes reduces duplication.
@JsonSerializable(genericArgumentFactories: true)
class DataResponse
{
final T? data;
final int errorCode;
final String errorMsg;
DataResponse({required this.data, required this.errorCode, required this.errorMsg});
factory DataResponse.fromJson(Map
json, T Function(dynamic) fromJsonT) => _$DataResponseFromJson(json, fromJsonT);
Map
toJson(dynamic Function(T) toJsonT) => _$DataResponseToJson(this, toJsonT);
}
@JsonSerializable(genericArgumentFactories: true)
class ListResponse
{
final List
? data;
final int errorCode;
final String errorMsg;
ListResponse({required this.data, required this.errorCode, required this.errorMsg});
factory ListResponse.fromJson(Map
json, T Function(dynamic) fromJsonT) => _$ListResponseFromJson(json, fromJsonT);
Map
toJson(dynamic Function(T) toJsonT) => _$ListResponseToJson(this, toJsonT);
}Example usage with a User model demonstrates deserialization of both single objects and lists.
3.3.2 Reducing Repetitive Settings with build.yaml
Configure global options such as explicit_to_json: true and include_if_null: false to avoid adding the same annotation parameters to every model.
targets:
$default:
builders:
json_serializable:
options:
explicit_to_json: true
include_if_null: false3.3.3 Custom Field (De)Serialization
Use @JsonKey with fromJson and toJson to handle special cases, such as converting a timestamp string to DateTime and back.
@JsonSerializable()
class User {
String name;
@JsonKey(fromJson: _timeStampFromJson, toJson: _timeStampToJson)
DateTime timestamp;
User({required this.name, required this.timestamp});
factory User.fromJson(Map
json) => _$UserFromJson(json);
Map
toJson() => _$UserToJson(this);
static DateTime _timeStampFromJson(String ts) => DateTime.fromMillisecondsSinceEpoch(int.parse(ts));
static String _timeStampToJson(DateTime dt) => dt.millisecondsSinceEpoch.toString();
}3.3.4 Custom Converters (JsonConverter)
Define a JsonConverter to map JSON values to Dart enums or other types.
enum DayOfWeek { monday, tuesday, wednesday, thursday, friday, saturday, sunday }
class DayOfWeekConverter implements JsonConverter
{
const DayOfWeekConverter();
@override
DayOfWeek fromJson(String json) {
switch (json.toLowerCase()) {
case 'monday': return DayOfWeek.monday;
case 'tuesday': return DayOfWeek.tuesday;
// ... other cases ...
default: throw ArgumentError('Invalid day: $json');
}
}
@override
String toJson(DayOfWeek obj) => obj.toString().split('.').last;
}
@JsonSerializable()
class MyModel {
@DayOfWeekConverter()
DayOfWeek dayOfWeek;
MyModel(this.dayOfWeek);
factory MyModel.fromJson(Map
json) => _$MyModelFromJson(json);
Map
toJson() => _$MyModelToJson(this);
}3.4 Common Questions
3.4.1 Should .g.dart files be committed?
There is no strict rule; teams may choose to commit them for convenience or ignore them to avoid merge conflicts, since they are generated.
3.4.2 Automating Code Generation
Running flutter pub run build_runner after each model change can be tedious. Use the watch mode:
flutter packages pub run build_runner watchIDE integrations (e.g., VS Code tasks) can bind this command to a shortcut for faster workflow.
4. Tools for Generating Model Classes from JSON
Besides json_serializable , several online and IDE tools can generate Dart models:
QuickType – copy JSON and get Dart code with optional json_serializable annotations.
json2dart – similar web‑based generator.
Android Studio plugins such as JsonToDart or FlutterJsonBeanFactory.
CLI tools like fluttercandies/JsonToDart and flutterchina/json_model .
These tools speed up model creation, after which developers may fine‑tune class names, add comments, or write custom scripts for bulk generation.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.