flutter-implement-json-serialization
Create model classes with fromJson/toJson using dart:convert and Dart 3 pattern matching. Use when manually mapping JSON to classes, parsing HTTP responses, or choosing between manual and code-generated serialization.
What this skill does
## Contents
- [Core Guidelines](#core-guidelines)
- [Dart 3 Pattern Matching in fromJson](#dart-3-pattern-matching-in-fromjson)
- [Background Parsing](#background-parsing)
- [Manual vs Code-Gen Decision](#manual-vs-code-gen-decision)
- [Workflow: Implementing a Serializable Model](#workflow-implementing-a-serializable-model)
- [Workflow: Fetching and Parsing JSON](#workflow-fetching-and-parsing-json)
- [Examples](#examples)
## Core Guidelines
- **Import `dart:convert`**: Use `jsonEncode()` and `jsonDecode()` for manual serialization.
- **Type Safety**: Always cast `jsonDecode()` result to `Map<String, dynamic>` (objects) or `List<dynamic>` (arrays). Never work with raw `dynamic`.
- **Encapsulation**: Define `fromJson` factory constructor and `toJson` method within the model class.
- **Background Parsing**: Offload to a separate isolate via `compute()` if parsing takes > 16ms (large JSON payloads).
- **Error Handling**: Throw `FormatException` on invalid JSON. Never return `null` from `fromJson`.
## Dart 3 Pattern Matching in fromJson
Use `switch` expressions with destructuring for type-safe, concise deserialization:
```dart
import 'dart:convert';
class User {
final int id;
final String name;
final String email;
const User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'id': final int id,
'name': final String name,
'email': final String email,
} =>
User(id: id, name: name, email: email),
_ => throw const FormatException('Invalid User JSON'),
};
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
};
}
```
**Benefits over manual casting**:
- Compile-time type checking in destructuring patterns
- Single expression handles both extraction and validation
- `FormatException` thrown automatically on type mismatch
### Nested Objects
```dart
class Post {
final int id;
final String title;
final User author;
const Post({required this.id, required this.title, required this.author});
factory Post.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'id': final int id,
'title': final String title,
'author': final Map<String, dynamic> authorJson,
} =>
Post(id: id, title: title, author: User.fromJson(authorJson)),
_ => throw const FormatException('Invalid Post JSON'),
};
}
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'author': author.toJson(),
};
}
```
## Background Parsing
For large JSON payloads (thousands of objects), offload parsing to a background isolate:
```dart
import 'dart:convert';
import 'package:flutter/foundation.dart';
// MUST be a top-level function (not a method or closure)
List<User> parseUsers(String responseBody) {
final parsed = (jsonDecode(responseBody) as List<dynamic>)
.cast<Map<String, dynamic>>();
return parsed.map<User>((json) => User.fromJson(json)).toList();
}
Future<List<User>> fetchUsers(http.Client client) async {
final response = await client.get(Uri.parse('https://api.example.com/users'));
if (response.statusCode == 200) {
return compute(parseUsers, response.body);
} else {
throw Exception('Failed to load users: ${response.statusCode}');
}
}
```
**Rules**:
- The parsing function MUST be top-level or static (not an instance method).
- Use `compute()` for simple isolate tasks. For complex scenarios, use `Isolate.run()`.
- Threshold: parse > 10,000 items or > 1MB payload → use background isolate.
## Manual vs Code-Gen Decision
| Criteria | Manual (`dart:convert`) | Code-Gen (`json_serializable` / `freezed`) |
|---|---|---|
| **Model count** | < 5 models | > 5 models |
| **Nesting depth** | Shallow (1-2 levels) | Deep / complex hierarchies |
| **Dev dependency** | None | `build_runner`, `json_annotation` |
| **Type safety** | Dart 3 pattern matching | Generated code |
| **Boilerplate** | Manual per model | Auto-generated |
| **Build time** | None | Adds `build_runner` step |
| **Flexibility** | Full control over parsing | Constrained by annotations |
**Recommendation**: Start with manual serialization for prototypes and small models. Migrate to `json_serializable` or `freezed` when the model count exceeds 5 or nesting becomes complex. See `flutter-code-gen` skill for code generation workflows.
## Workflow: Implementing a Serializable Model
### Task Progress
- [ ] **Step 1**: Define model class with `final` properties and `const` constructor.
- [ ] **Step 2**: Implement `factory Model.fromJson(Map<String, dynamic> json)` using pattern matching.
- [ ] **Step 3**: Implement `Map<String, dynamic> toJson()` method.
- [ ] **Step 4**: Write unit tests for serialization round-trip (`fromJson(toJson(model)) == model`).
- [ ] **Step 5**: Run tests — `dart test` or `flutter test`.
- [ ] **Step 6**: Feedback Loop — fix type mismatch errors → re-run until green.
## Workflow: Fetching and Parsing JSON
### Task Progress
- [ ] **Step 1**: Execute HTTP request.
- [ ] **Step 2**: Validate response status code (200 → proceed, else → throw).
- [ ] **Step 3**: Determine parsing strategy:
- Small payload → parse synchronously on main thread.
- Large payload → use `compute(parseFunction, response.body)`.
- [ ] **Step 4**: Decode and map JSON to model via `fromJson`.
## Examples
### Synchronous HTTP Fetch + Parse
```dart
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<User> fetchUser(http.Client client, int userId) async {
final response = await client.get(
Uri.parse('https://api.example.com/users/$userId'),
headers: {'Accept': 'application/json'},
);
if (response.statusCode == 200) {
final Map<String, dynamic> jsonMap =
jsonDecode(response.body) as Map<String, dynamic>;
return User.fromJson(jsonMap);
} else {
throw Exception('Failed to load user: ${response.statusCode}');
}
}
```
### List Parsing with Type Safety
```dart
Future<List<User>> fetchAllUsers(http.Client client) async {
final response = await client.get(
Uri.parse('https://api.example.com/users'),
);
if (response.statusCode == 200) {
final List<dynamic> jsonList = jsonDecode(response.body) as List<dynamic>;
return jsonList
.map((e) => User.fromJson(e as Map<String, dynamic>))
.toList();
} else {
throw Exception('Failed to load users');
}
}
```
### Unit Test for Serialization
```dart
import 'package:flutter_test/flutter_test.dart';
void main() {
group('$User', () {
test('fromJson creates valid User', () {
final json = {'id': 1, 'name': 'Alice', 'email': '[email protected]'};
final user = User.fromJson(json);
expect(user.id, 1);
expect(user.name, 'Alice');
expect(user.email, '[email protected]');
});
test('toJson returns valid map', () {
const user = User(id: 1, name: 'Alice', email: '[email protected]');
final json = user.toJson();
expect(json['id'], 1);
expect(json['name'], 'Alice');
});
test('round-trip serialization', () {
const original = User(id: 1, name: 'Alice', email: '[email protected]');
final json = original.toJson();
final restored = User.fromJson(json);
expect(restored.id, original.id);
expect(restored.name, original.name);
expect(restored.email, original.email);
});
test('throws FormatException on invalid JSON', () {
final invalidJson = {'id': 'not_an_int', 'name': 'Alice'};
expect(() => User.fromJson(invalidJson), throwsFormatException);
});
});
}
```
Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.