mcpField
F
flutter-implement-json-serialization

flutter-implement-json-serialization

github.com/dhruvanbhalaraDevelopment3050 installsUpdated 10w ago
Repository

About flutter-implement-json-serialization

A JSON serialization guide for Dart and Flutter developers. It explains type-safe model conversion, background parsing, and how to choose between manual serialization and code generation.

Key features

  1. 01Implement fromJson and toJson
  2. 02Parse JSON with pattern matching
  3. 03Parse large payloads in background isolates
  4. 04Compare manual and generated serialization
  5. 05Validate serialization with workflows

Use cases

  1. 01Add JSON conversion methods to models
  2. 02Parse nested JSON objects
  3. 03Move large response parsing to background
  4. 04Choose manual or generated serialization

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/dhruvanbhalara/skills --skill flutter-implement-json-serialization

After installing, type/flutter-implement-json-serializationin Claude Code to confirm it loaded.

Details

From the project README

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

CriteriaManual (dart:convert)Code-Gen (json_serializable / freezed)
Model count< 5 models> 5 models
Nesting depthShallow (1-2 levels)Deep / complex hierarchies
Dev dependencyNonebuild_runner, json_annotation
Type safetyDart 3 pattern matchingGenerated code
BoilerplateManual per modelAuto-generated
Build timeNoneAdds build_runner step
FlexibilityFull control over parsingConstrained 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': '[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);
    });
  });
}

Comments

Markdown supported