Lesson 44 of 51 intermediate

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

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. 1. Import json_annotation for serialization annotations
  2. 2.
  3. 3. Part directive links to the generated .g.dart file
  4. 4.
  5. 5. Comment: Response DTO wraps user and tokens together
  6. 6. @JsonSerializable annotation triggers code generation
  7. 7. AuthResponseDto class with two nested objects
  8. 8. A UserDto field for user information
  9. 9. A TokensDto field for authentication tokens
  10. 10.
  11. 11. Constructor requires both fields
  12. 12.
  13. 13. Factory fromJson delegates to generated _$AuthResponseDtoFromJson function
  14. 14. toJson delegates to generated _$AuthResponseDtoToJson function
  15. 15. Closing AuthResponseDto
  16. 16.
  17. 17. Comment: User DTO maps API field names to Dart conventions
  18. 18. @JsonSerializable annotation
  19. 19. UserDto class declaration
  20. 20. The id field maps directly (same name in JSON)
  21. 21.
  22. 22. @JsonKey maps full_name in JSON to fullName in Dart
  23. 23. The fullName field for the user's display name
  24. 24.
  25. 25. The email field maps directly
  26. 26.
  27. 27. @JsonKey maps is_verified with a default of false if missing from JSON
  28. 28. The isVerified boolean field
  29. 29.
  30. 30. @JsonKey maps created_at from JSON to createdAt in Dart
  31. 31. The createdAt DateTime field
  32. 32.
  33. 33. Constructor with required and default parameters
  34. 34. Closing constructor
  35. 35.
  36. 36. Factory fromJson using generated function
  37. 37. toJson using generated function
  38. 38. Closing UserDto
  39. 39.
  40. 40. Comment: Tokens DTO for authentication data
  41. 41. @JsonSerializable annotation
  42. 42. TokensDto class declaration
  43. 43. @JsonKey maps access_token to accessToken
  44. 44. The accessToken string field
  45. 45.
  46. 46. @JsonKey maps refresh_token to refreshToken
  47. 47. The refreshToken string field
  48. 48.
  49. 49. @JsonKey maps expires_in to expiresIn
  50. 50. The expiresIn integer (seconds until expiry)
  51. 51.
  52. 52. Constructor
  53. 53. Closing constructor
  54. 54.
  55. 55. Factory fromJson and toJson methods
  56. 56. Closing TokensDto
  57. 57.
  58. 58. Comment: Domain Entity has no JSON knowledge
  59. 59. Pure Dart class with no annotations or imports
  60. 60. Simple fields: id, name, email, isVerified
  61. 61. Constructor
  62. 62. Closing User entity
  63. 63.
  64. 64. Comment: Repository maps DTO to Entity
  65. 65. Function that converts UserDto to domain User
  66. 66. Maps dto.fullName to entity name (different field names!)
  67. 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

Open interactive version (quiz + challenge) ← Back to course: Flutter & Dart