flutterdart dart
npx skills add dhruvanbhalara/skills --skill flutter-implement-json-serialization

Contents

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:

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

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:

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

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

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

import 'package:flutter_test/flutter_test.dart';

void main() {
  group('$User', () {
    test('fromJson creates valid User', () {
      final json = {'id': 1, 'name': 'Alice', 'email': 'alice@test.com'};
      final user = User.fromJson(json);

      expect(user.id, 1);
      expect(user.name, 'Alice');
      expect(user.email, 'alice@test.com');
    });

    test('toJson returns valid map', () {
      const user = User(id: 1, name: 'Alice', email: 'alice@test.com');
      final json = user.toJson();

      expect(json['id'], 1);
      expect(json['name'], 'Alice');
    });

    test('round-trip serialization', () {
      const original = User(id: 1, name: 'Alice', email: 'alice@test.com');
      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);
    });
  });
}