| 1 | --- |
| 2 | name: flutter-implement-json-serialization |
| 3 | description: Create model classes with `fromJson` and `toJson` methods using `dart:convert`. Use when manually mapping JSON keys to class properties for simple data structures. |
| 4 | metadata: |
| 5 | model: models/gemini-3.1-pro-preview |
| 6 | last_modified: Tue, 21 Apr 2026 21:44:50 GMT |
| 7 | --- |
| 8 | # Serializing JSON Manually in Flutter |
| 9 | |
| 10 | ## Contents |
| 11 | - [Core Guidelines](#core-guidelines) |
| 12 | - [Workflow: Implementing a Serializable Model](#workflow-implementing-a-serializable-model) |
| 13 | - [Workflow: Fetching and Parsing JSON](#workflow-fetching-and-parsing-json) |
| 14 | - [Examples](#examples) |
| 15 | |
| 16 | ## Core Guidelines |
| 17 | |
| 18 | - **Import `dart:convert`**: Utilize Flutter's built-in `dart:convert` library for manual JSON encoding (`jsonEncode`) and decoding (`jsonDecode`). |
| 19 | - **Enforce Type Safety**: Always cast the `dynamic` result of `jsonDecode()` to the expected type, typically `Map<String, dynamic>` for objects or `List<dynamic>` for arrays. |
| 20 | - **Encapsulate Serialization Logic**: Define plain model classes containing properties corresponding to the JSON structure. Implement a `fromJson` factory constructor and a `toJson` method within the model. |
| 21 | - **Handle Background Parsing**: If parsing large JSON documents (execution time > 16ms), offload the parsing logic to a separate isolate using Flutter's `compute()` function to prevent UI jank. |
| 22 | - **Throw Exceptions on Failure**: When handling HTTP responses, throw an exception if the status code is not successful (e.g., not 200 OK or 201 Created). Do not return `null`. |
| 23 | |
| 24 | ## Workflow: Implementing a Serializable Model |
| 25 | |
| 26 | Use this checklist to implement manual JSON serialization for a data model. |
| 27 | |
| 28 | **Task Progress:** |
| 29 | - [ ] Define the plain model class with `final` properties. |
| 30 | - [ ] Implement the `factory Model.fromJson(Map<String, dynamic> json)` constructor. |
| 31 | - [ ] Implement the `Map<String, dynamic> toJson()` method. |
| 32 | - [ ] Write unit tests for both serialization methods. |
| 33 | - [ ] Run validator -> review type mismatch errors -> fix casting logic. |
| 34 | |
| 35 | 1. **Define the Model**: Create a class with properties matching the JSON keys. |
| 36 | 2. **Implement `fromJson`**: Extract values from the `Map` and cast them to the appropriate Dart types. Use pattern matching or explicit casting. |
| 37 | 3. **Implement `toJson`**: Return a `Map<String, dynamic>` mapping the class properties back to their JSON string keys. |
| 38 | 4. **Validate**: Execute unit tests to ensure type safety, autocompletion, and compile-time exception handling function correctly. |
| 39 | |
| 40 | ## Workflow: Fetching and Parsing JSON |
| 41 | |
| 42 | Use this conditional workflow when retrieving and parsing JSON from a network request. |
| 43 | |
| 44 | **Task Progress:** |
| 45 | - [ ] Execute the HTTP request. |
| 46 | - [ ] Validate the response status code. |
| 47 | - [ ] Determine parsing strategy (Synchronous vs. Isolate). |
| 48 | - [ ] Decode and map the JSON to the model. |
| 49 | |
| 50 | 1. **Execute Request**: Use the `http` package to perform the network call. |
| 51 | 2. **Validate Response**: |
| 52 | - If `response.statusCode == 200` (or 201 for POST), proceed to parsing. |
| 53 | - If the status code indicates failure, throw an `Exception`. |
| 54 | 3. **Determine Parsing Strategy**: |
| 55 | - If parsing a **small payload** (e.g., a single object), parse synchronously on the main thread. |
| 56 | - If parsing a **large payload** (e.g., an array of thousands of objects), use `compute(parseFunction, response.body)` to parse in a background isolate. |
| 57 | 4. **Decode and Map**: Pass the decoded JSON to your model's `fromJson` constructor. |
| 58 | |
| 59 | ## Examples |
| 60 | |
| 61 | ### High-Fidelity Model Implementation |
| 62 | |
| 63 | ```dart |
| 64 | import 'dart:convert'; |
| 65 | |
| 66 | class User { |
| 67 | final int id; |
| 68 | final String name; |
| 69 | final String email; |
| 70 | |
| 71 | const User({ |
| 72 | required this.id, |
| 73 | required this.name, |
| 74 | required this.email, |
| 75 | }); |
| 76 | |
| 77 | // Factory constructor for deserialization |
| 78 | factory User.fromJson(Map<String, dynamic> json) { |
| 79 | return switch (json) { |
| 80 | { |
| 81 | 'id': int id, |
| 82 | 'name': String name, |
| 83 | 'email': String email, |
| 84 | } => |
| 85 | User( |
| 86 | id: id, |
| 87 | name: name, |
| 88 | email: email, |
| 89 | ), |
| 90 | _ => throw const FormatException('Failed to load User.'), |
| 91 | }; |
| 92 | } |
| 93 | |
| 94 | // Method for serialization |
| 95 | Map<String, dynamic> toJson() { |
| 96 | return { |
| 97 | 'id': id, |
| 98 | 'name': name, |
| 99 | 'email': email, |
| 100 | }; |
| 101 | } |
| 102 | } |
| 103 | ``` |
| 104 | |
| 105 | ### Synchronous Parsing (Small Payload) |
| 106 | |
| 107 | ```dart |
| 108 | import 'dart:convert'; |
| 109 | import 'package:http/http.dart' as http; |
| 110 | |
| 111 | Future<User> fetchUser(http.Client client, int userId) async { |
| 112 | final response = await client.get( |
| 113 | Uri.parse('https://api.example.com/users/$userId'), |
| 114 | headers: {'Accept': 'application/json'}, |
| 115 | ); |
| 116 | |
| 117 | if (response.statusCode == 200) { |
| 118 | // Decode returns dynamic, cast to Map<String, dynamic> |
| 119 | final Map<String, dynamic> jsonMap = jsonDecode(response.body) as Map<String, dynamic>; |
| 120 | return User.fromJson(jsonMap); |
| 121 | } else { |
| 122 | throw Exception('Failed to load user'); |
| 123 | } |
| 124 | } |
| 125 | ``` |
| 126 | |
| 127 | ### Background Parsing (Large Payload) |
| 128 | |
| 129 | ```dart |
| 130 | import 'dart:convert'; |
| 131 | import 'package:flutter/foundation.dart'; |
| 132 | import 'package:http/http.dart' as http; |
| 133 | |
| 134 | // Top-level function required for compute() |
| 135 | List<User> parseUsers(String responseBody) { |
| 136 | final parsed = (jsonDecode(responseBody) as List<dynamic>).cast<Map<String, dynamic>>(); |
| 137 | return parsed.map<User>((json) => User.fromJson(json)).toList(); |
| 138 | } |
| 139 | |
| 140 | Future<List<User>> fetchUsers(http.Client client) async { |
| 141 | final response = await client.get( |
| 142 | Uri.parse('https://api.example.com/users'), |
| 143 | headers: {'Accept': 'application/json'}, |
| 144 | ); |
| 145 | |
| 146 | if (response.statusCode == 200) { |
| 147 | // Offload expensive parsing to a background isolate |
| 148 | return compute(parseUsers, response.body); |
| 149 | } else { |
| 150 | throw Exception('Failed to load users'); |
| 151 | } |
| 152 | } |
| 153 | ``` |