JSON Models & Serialization
Turning JSON into Dart Objects
Open interactive version (quiz + challenge)Real-world analogy
Imagine you receive a letter written in French (JSON from the server). You cannot use it directly in your English (Dart) conversation. You need a translator -- fromJson translates French to English (JSON to Dart object), and toJson translates English back to French (Dart object to JSON). json_serializable is like hiring a professional translation agency that handles everything automatically!
What is it?
JSON serialization converts between Dart objects and JSON data. The json_serializable package auto-generates fromJson/toJson code using annotations and build_runner, eliminating manual mapping errors. In team_mvp_kit, DTOs in the data layer handle all JSON serialization while domain entities stay pure Dart with no serialization knowledge.
Real-world relevance
Every API call in team_mvp_kit involves serialization. When you log in, the server sends JSON with tokens and user data. The LoginResponseDto deserializes it. When you update your profile, your data is serialized to JSON and sent to the server. DTOs act as the translation layer between your API's JSON format and your app's Dart types.
Key points
- What is Serialization? — Serialization is converting a Dart object into JSON (a Map or String) so it can be sent over the network or saved to storage. Deserialization is the reverse -- converting JSON back into a Dart object. Together they are the bridge between your app's typed Dart world and the untyped JSON world of APIs.
- Manual fromJson and toJson — The simplest approach is writing factory fromJson constructors and toJson methods by hand. fromJson takes a Map and extracts fields. toJson returns a Map with string keys. This works but is tedious and error-prone for large models.
- json_serializable Package — json_serializable generates fromJson and toJson code automatically using build_runner. Annotate your class with @JsonSerializable() and add the part directive. Run build_runner and the .g.dart file is generated with all the mapping code. This eliminates manual errors and saves time.
- @JsonKey for Field Customization — Use @JsonKey to customize how individual fields are serialized. Rename fields when the API uses snake_case but your Dart code uses camelCase. Set default values, mark fields as nullable, or ignore them entirely during serialization.
- Nested Objects and Lists — json_serializable handles nested objects and lists of objects automatically. If UserDto has a ProfileDto field or a List of AddressDto, the generated code calls their fromJson/toJson too. Just make sure all nested classes also have @JsonSerializable.
- DTOs vs Domain Entities — In team_mvp_kit's Clean Architecture, DTOs (Data Transfer Objects) live in the data layer and handle JSON serialization. Domain entities live in the domain layer and have NO serialization logic. The repository maps DTOs to entities, keeping your domain pure and independent of the API format.
- Using DTOs in Repositories — The repository calls the API, receives raw JSON, deserializes it to a DTO using fromJson, then maps the DTO to a domain entity. This three-step pattern (API call -> DTO -> Entity) is used throughout team_mvp_kit for every data operation.
- Enum Serialization — json_serializable can serialize enums to and from strings. Use @JsonEnum with valueField to control how enum values map to JSON strings. This is common for status fields, user roles, and any field with a fixed set of values.
- Running Code Generation — After creating or modifying any @JsonSerializable class, run build_runner to generate the .g.dart files. Use --delete-conflicting-outputs to clean up stale files. Use watch mode during development for automatic regeneration on save.
Code example
import 'package:json_annotation/json_annotation.dart';
part 'auth_response_dto.g.dart';
// 1. Response DTO with nested objects
@JsonSerializable()
class AuthResponseDto {
final UserDto user;
final TokensDto tokens;
AuthResponseDto({required this.user, required this.tokens});
factory AuthResponseDto.fromJson(Map<String, dynamic> json) =>
_$AuthResponseDtoFromJson(json);
Map<String, dynamic> toJson() => _$AuthResponseDtoToJson(this);
}
// 2. User DTO with field name mapping
@JsonSerializable()
class UserDto {
final int id;
@JsonKey(name: 'full_name')
final String fullName;
final String email;
@JsonKey(name: 'is_verified', defaultValue: false)
final bool isVerified;
@JsonKey(name: 'created_at')
final DateTime createdAt;
UserDto({
required this.id,
required this.fullName,
required this.email,
this.isVerified = false,
required this.createdAt,
});
factory UserDto.fromJson(Map<String, dynamic> json) =>
_$UserDtoFromJson(json);
Map<String, dynamic> toJson() => _$UserDtoToJson(this);
}
// 3. Tokens DTO
@JsonSerializable()
class TokensDto {
@JsonKey(name: 'access_token')
final String accessToken;
@JsonKey(name: 'refresh_token')
final String refreshToken;
@JsonKey(name: 'expires_in')
final int expiresIn;
TokensDto({
required this.accessToken,
required this.refreshToken,
required this.expiresIn,
});
factory TokensDto.fromJson(Map<String, dynamic> json) =>
_$TokensDtoFromJson(json);
Map<String, dynamic> toJson() => _$TokensDtoToJson(this);
}
// 4. Domain Entity (no JSON, no annotations)
class User {
final int id;
final String name;
final String email;
final bool isVerified;
User({
required this.id,
required this.name,
required this.email,
required this.isVerified,
});
}
// 5. Repository maps DTO -> Entity
User mapToEntity(UserDto dto) => User(
id: dto.id,
name: dto.fullName,
email: dto.email,
isVerified: dto.isVerified,
);Line-by-line walkthrough
- 1. Import json_annotation for serialization annotations
- 2.
- 3. Part directive links to the generated .g.dart file
- 4.
- 5. Comment: Response DTO wraps user and tokens together
- 6. @JsonSerializable annotation triggers code generation
- 7. AuthResponseDto class with two nested objects
- 8. A UserDto field for user information
- 9. A TokensDto field for authentication tokens
- 10.
- 11. Constructor requires both fields
- 12.
- 13. Factory fromJson delegates to generated _$AuthResponseDtoFromJson function
- 14. toJson delegates to generated _$AuthResponseDtoToJson function
- 15. Closing AuthResponseDto
- 16.
- 17. Comment: User DTO maps API field names to Dart conventions
- 18. @JsonSerializable annotation
- 19. UserDto class declaration
- 20. The id field maps directly (same name in JSON)
- 21.
- 22. @JsonKey maps full_name in JSON to fullName in Dart
- 23. The fullName field for the user's display name
- 24.
- 25. The email field maps directly
- 26.
- 27. @JsonKey maps is_verified with a default of false if missing from JSON
- 28. The isVerified boolean field
- 29.
- 30. @JsonKey maps created_at from JSON to createdAt in Dart
- 31. The createdAt DateTime field
- 32.
- 33. Constructor with required and default parameters
- 34. Closing constructor
- 35.
- 36. Factory fromJson using generated function
- 37. toJson using generated function
- 38. Closing UserDto
- 39.
- 40. Comment: Tokens DTO for authentication data
- 41. @JsonSerializable annotation
- 42. TokensDto class declaration
- 43. @JsonKey maps access_token to accessToken
- 44. The accessToken string field
- 45.
- 46. @JsonKey maps refresh_token to refreshToken
- 47. The refreshToken string field
- 48.
- 49. @JsonKey maps expires_in to expiresIn
- 50. The expiresIn integer (seconds until expiry)
- 51.
- 52. Constructor
- 53. Closing constructor
- 54.
- 55. Factory fromJson and toJson methods
- 56. Closing TokensDto
- 57.
- 58. Comment: Domain Entity has no JSON knowledge
- 59. Pure Dart class with no annotations or imports
- 60. Simple fields: id, name, email, isVerified
- 61. Constructor
- 62. Closing User entity
- 63.
- 64. Comment: Repository maps DTO to Entity
- 65. Function that converts UserDto to domain User
- 66. Maps dto.fullName to entity name (different field names!)
- 67. Maps all other fields directly
Spot the bug
@JsonSerializable()
class UserDto {
final int id;
@JsonKey(name: 'full_name')
final String fullName;
UserDto({required this.id, required this.fullName});
factory UserDto.fromJson(Map<String, dynamic> json) =>
_$UserDtoFromJson(json);
}Need a hint?
Compare what methods you need versus what is defined...
Show answer
The toJson() method is missing. While the code compiles, you cannot serialize UserDto back to JSON without it. Add: Map<String, dynamic> toJson() => _$UserDtoToJson(this); Also, the 'part' directive is missing at the top. Add: part 'user_dto.g.dart'; Without the part directive, the generated _$UserDtoFromJson function will not be found.
Explain like I'm 5
Imagine you have a toy box (JSON from the internet) full of loose LEGO bricks with labels like 'color: red' and 'size: big'. fromJson is like following instructions to build those loose bricks into a proper LEGO house (Dart object). toJson is like carefully taking the house apart and putting each brick back in the box with its label. json_serializable is like having a robot that reads the building instructions and writes the assembly steps for you!
Fun fact
The json_serializable package has been downloaded over 100 million times on pub.dev! Before code generation became popular, Flutter developers had to write hundreds of lines of manual JSON mapping code. One large project reported saving over 3000 lines of hand-written serialization code by switching to json_serializable.
Hands-on challenge
Create a PostDto class with fields: id (int), title (String), body (String), userId (int, mapped from 'user_id'), and createdAt (DateTime, mapped from 'created_at'). Use @JsonSerializable and @JsonKey. Then create a pure Post domain entity and a mapper function. Run build_runner to generate the .g.dart file.
More resources
- json_serializable Package (pub.dev)
- JSON and Serialization in Flutter (Flutter Official)
- json_annotation Package (pub.dev)