flutter-implement-json-serialization について
Dart と Flutter 開発者向けの JSON シリアライズガイドです。型安全なモデル変換、バックグラウンド解析、手動実装とコード生成の選び方を説明します。
主な機能
- 01fromJson と toJson の実装
- 02パターンマッチによる JSON 解析
- 03大きなデータのバックグラウンド解析
- 04手動と生成型シリアライズの比較
- 05手順に沿った変換結果の検証
活用シーン
- 01モデルに JSON 変換メソッドを追加
- 02ネストした JSON オブジェクトの解析
- 03大きなレスポンスの解析をバックグラウンドへ移行
- 04手動方式と生成方式の選択
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ dhruvanbhalara/ skills --skill flutter-implement-json-serializationインストール後、Claude Code で/flutter-implement-json-serializationを入力して読み込まれたか確認します。
詳細
目次
- コアガイドライン
- fromJson における Dart 3 パターンマッチング
- バックグラウンド解析
- 手動 vs コード生成の判断
- ワークフロー: シリアライズ可能なモデルの実装
- ワークフロー: JSON の取得と解析
- 例
コアガイドライン
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);
});
});
}
コメント
Markdown 対応