mcpField
F
flutter-implement-json-serialization

flutter-implement-json-serialization

github.com/dhruvanbhalara開発3050 回インストール10 週間前に更新
リポジトリ

flutter-implement-json-serialization について

Dart と Flutter 開発者向けの JSON シリアライズガイドです。型安全なモデル変換、バックグラウンド解析、手動実装とコード生成の選び方を説明します。

主な機能

  1. 01fromJson と toJson の実装
  2. 02パターンマッチによる JSON 解析
  3. 03大きなデータのバックグラウンド解析
  4. 04手動と生成型シリアライズの比較
  5. 05手順に沿った変換結果の検証

活用シーン

  1. 01モデルに JSON 変換メソッドを追加
  2. 02ネストした JSON オブジェクトの解析
  3. 03大きなレスポンスの解析をバックグラウンドへ移行
  4. 04手動方式と生成方式の選択

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/flutter-implement-json-serializationを入力して読み込まれたか確認します。

詳細

目次

コアガイドライン

  • dart:convert をインポート: 手動シリアライズには jsonEncode() と jsonDecode() を使用します。
  • 型安全性: jsonDecode() の結果は常に Map<String, dynamic> (オブジェクト) または List<dynamic> (配列) にキャストしてください。生の dynamic を扱わないでください。
  • カプセル化: fromJson ファクトリコンストラクタと toJson メソッドをモデルクラス内に定義します。
  • バックグラウンド解析: 解析に 16ms を超える場合 (大きな JSON ペイロード)、compute() を介して別のアイソレートにオフロードします。
  • エラー処理: 不正な JSON の場合は FormatException をスローします。fromJson から null を返さないでください。

fromJson における Dart 3 パターンマッチング

型安全で簡潔なデシリアライズには、分解を伴う switch 式を使用します:

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,
  };
}

手動キャストに対する利点:

  • 分解パターンにおけるコンパイル時の型チェック
  • 単一の式で抽出と検証の両方を処理
  • 型の不一致時に FormatException が自動的にスローされる
ネストされたオブジェクト
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(),
  };
}

バックグラウンド解析

大きな JSON ペイロード (数千のオブジェクト) の場合、解析をバックグラウンドアイソレートにオフロードします:

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}');
  }
}

ルール:

  • 解析関数はトップレベルまたは静的でなければなりません (インスタンスメソッドは不可)。
  • 単純なアイソレートタスクには compute() を使用します。複雑なシナリオでは Isolate.run() を使用します。
  • しきい値: 10,000 件を超えるアイテム、または 1MB を超えるペイロードを解析 → バックグラウンドアイソレートを使用。

手動 vs コード生成の判断

基準手動 (dart:convert)コード生成 (json_serializable / freezed)
モデル数5 モデル未満5 モデル超
ネストの深さ浅い (1〜2 レベル)深い / 複雑な階層
開発依存関係なしbuild_runner、json_annotation
型安全性Dart 3 パターンマッチング生成されたコード
ボイラープレートモデルごとに手動自動生成
ビルド時間なしbuild_runner ステップが追加される
柔軟性解析を完全に制御アノテーションによる制約

推奨: プロトタイプや小さなモデルでは手動シリアライズから始めてください。モデル数が 5 を超えるか、ネストが複雑になった場合は json_serializable または freezed に移行してください。コード生成のワークフローについては flutter-code-gen スキルを参照してください。

ワークフロー: シリアライズ可能なモデルの実装

タスクの進捗
  • ステップ 1: final プロパティと const コンストラクタを持つモデルクラスを定義します。
  • ステップ 2: パターンマッチングを使用して factory Model.fromJson(Map<String, dynamic> json) を実装します。
  • ステップ 3: Map<String, dynamic> toJson() メソッドを実装します。
  • ステップ 4: シリアライズのラウンドトリップ (fromJson(toJson(model)) == model) のユニットテストを作成します。
  • ステップ 5: テストを実行します: dart test または flutter test。
  • ステップ 6: フィードバックループ: 型の不一致エラーを修正 → グリーンになるまで再実行。

ワークフロー: JSON の取得と解析

タスクの進捗
  • ステップ 1: HTTP リクエストを実行します。
  • ステップ 2: レスポンスのステータスコードを検証します (200 → 続行、それ以外 → スロー)。
  • ステップ 3: 解析戦略を決定します:
    • 小さなペイロード → メインスレッドで同期的に解析します。
    • 大きなペイロード → compute(parseFunction, response.body) を使用します。
  • ステップ 4: JSON をデコードし、fromJson を介してモデルにマッピングします。

例

同期的な HTTP 取得 + 解析
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}');
  }
}
型安全なリストの解析
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');
  }
}
シリアライズのユニットテスト
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);
    });
  });
}

コメント