JSON Serialization in Flutter with json_serializable
json_serializable 6.14.1 generates type-safe to/from JSON code, enums, custom converters, and even JSON Schema — no hand-written maps.
Published on • October 7, 2026
AI Assistant

Hand-writing json['key'] as String casts is the most common source of runtime surprises in Flutter apps. One missing field, one null that shouldn’t be null, and the crash happens three layers away from the API call that caused it.
json_serializable eliminates that class of bugs by generating the conversion code at build time. The current release is 6.14.1, published by the Google verified publisher, with 3.9k likes and 3.64M downloads.
The Setup
Three dependencies and one build command:
dependencies:
json_annotation: ^4.12.0
dev_dependencies:
build_runner: ^2.4.0
json_serializable: ^6.14.1
dart run build_runner build
Annotate your class, declare the generated part file, and connect the generated functions:
import 'package:json_annotation/json_annotation.dart';
part 'person.g.dart';
@JsonSerializable(createJsonSchema: true)
class Person {
final String firstName, lastName;
final DateTime? dateOfBirth;
Person({required this.firstName, required this.lastName, this.dateOfBirth});
factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
Map<String, dynamic> toJson() => _$PersonToJson(this);
static const jsonSchema = _$PersonJsonSchema;
}
Building creates person.g.dart:
Person _$PersonFromJson(Map<String, dynamic> json) => Person(
firstName: json['firstName'] as String,
lastName: json['lastName'] as String,
dateOfBirth: json['dateOfBirth'] == null
? null
: DateTime.parse(json['dateOfBirth'] as String),
);
Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
'firstName': instance.firstName,
'lastName': instance.lastName,
'dateOfBirth': instance.dateOfBirth?.toIso8601String(),
};
The generator only looks for a fromJson factory and a toJson method — it doesn’t care how they were produced. That becomes important for custom types.
Three Levels of Configuration
You can tune codegen at three layers, each taking precedence over the next:
@JsonKeyon a field — per-field overrides like renaming, defaults, custom converters.@JsonSerializableon the class — per-class settings (createJsonSchema,fieldRename,includeIfNull, etc.).build.yaml— project-wide defaults for every@JsonSerializableclass.
Every JsonSerializable field is configurable in build.yaml, which is the right place for house style:
targets:
$default:
builders:
json_serializable:
options:
field_rename: snake
include_if_null: false
explicit_to_json: true
Useful defaults from the docs: any_map: false, checked: false, create_factory: true, create_to_json: true, date_time_utc: false, disallow_unrecognized_keys: false, generic_argument_factories: false, ignore_unannotated: false.
Enums Without Boilerplate
Enums get their own annotations. @JsonEnum sets the rename policy for all values; @JsonValue maps individual values:
@JsonEnum(fieldRename: FieldRename.kebab)
enum Status {
@JsonValue('in-progress')
inProgress,
done,
}
For enhanced enums, valueField pulls the serialized form from a field:
@JsonEnum(valueField: 'code')
enum StatusCodeEnhanced {
success(200),
movedPermanently(301),
found(302),
internalServerError(500);
const StatusCodeEnhanced(this.code);
final int code;
}
Supported Types
Out of the box: BigInt, bool, DateTime, double, Duration, Enum, int, Iterable, List, Map, num, Object, Record, Set, String, Uri — and collections of those. Map keys can be BigInt, DateTime, Enum, int, Object, String, or Uri.
Custom Types: Three Escape Hatches
1. Own the type. If you control it, just give it fromJson/toJson — the generator finds them without any annotation on the type itself:
class Sample2 {
factory Sample2.fromJson(int value) => Sample2(value);
final int value;
int toJson() => value;
}
2. Per-field functions. JsonKey(toJson:, fromJson:) with top-level or static functions:
@JsonKey(toJson: _toJson, fromJson: _fromJson)
final DateTime value;
static int _toJson(DateTime value) => value.millisecondsSinceEpoch;
static DateTime _fromJson(int ms) => DateTime.fromMillisecondsSinceEpoch(ms);
3. JsonConverter. Best when the same conversion applies to many fields — and crucially, it works inside collections:
class EpochDateTimeConverter implements JsonConverter<DateTime, int> {
const EpochDateTimeConverter();
@override
DateTime fromJson(int json) => DateTime.fromMillisecondsSinceEpoch(json);
@override
int toJson(DateTime object) => object.millisecondsSinceEpoch;
}
JSON Schema Generation
Set createJsonSchema: true and the generator emits a static const schema per class — useful for validation, docs, or defining an API contract:
const _$PersonJsonSchema = {
r'$schema': 'https://json-schema.org/draft/2020-12/schema',
'type': 'object',
'properties': {
'firstName': {'type': 'string'},
'lastName': {'type': 'string'},
'dateOfBirth': {'type': 'string', 'format': 'date-time'},
},
'required': ['firstName', 'lastName'],
};
Type mapping follows intuition: int → integer, DateTime → string + format: date-time, List/Set → array, Map → object, nested objects via $ref. Doc comments (///) become description fields, and JsonKey.defaultValue is reflected in the schema.
Gotchas in Practice
- Commit your
.g.dartfiles or regenerate in CI. A stale part file fails at compile time with an unhelpful “undefined” error. explicit_to_json: truematters when you have nested serializable objects — without it, nestedtoJson()isn’t called and you getInstance of 'Foo'in the map.build_runneris slow on large projects. Usedart run build_runner build --delete-conflicting-outputsin CI and watch-mode (watch) locally.Recordsupport (Dart 3 tuples) is included in 6.14.x — no wrapper class needed for simple pairs.