JSONをDartモデルクラスに変換することは、Flutter開発で最も一般的なタスクの一つです。REST APIからデータを取得する場合でも、ローカル設定を読み取る場合でも、JSONデータを安全に解析して使用するためには、適切に構造化されたDartクラスが必要です。
モデルクラスが必要な理由
- 型安全性 — ランタイムではなくコンパイル時にエラーをキャッチ
- IDEの自動補完とリファクタリングサポート
- フロントエンドとバックエンド間の明確なデータ契約
- テストとメンテナンスが容易に
基本的なJSONからDartへの型マッピング
| JSON型 | Dart型 | 例 |
|---|---|---|
| string | String | "hello" → "hello" |
| number (int) | int | 42 → 42 |
| number (float) | double | 3.14 → 3.14 |
| boolean | bool | true → true |
| null | Null / dynamic | null → null |
| array | List<T> | [1,2,3] → [1,2,3] |
| object | Map / Class | {} → User() |
手動モデルクラス(fromJson / toJson)
最も簡単なアプローチは、モデルクラスを手動で書くことです。完全な例:
// JSON input
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"isActive": true,
"score": 95.5
}
// Dart model class
class User {
final int id;
final String name;
final String email;
final bool isActive;
final double score;
User({
required this.id,
required this.name,
required this.email,
required this.isActive,
required this.score,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
isActive: json['isActive'] as bool,
score: (json['score'] as num).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'isActive': isActive,
'score': score,
};
}
}ネストされたオブジェクトの処理
JSONにネストされたオブジェクトが含まれる場合、各レベルに個別のDartクラスを作成します:
// JSON with nested object
{
"id": 1,
"name": "John",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zipCode": "62701"
}
}
// Dart classes
class Address {
final String street;
final String city;
final String zipCode;
Address({required this.street, required this.city, required this.zipCode});
factory Address.fromJson(Map<String, dynamic> json) {
return Address(
street: json['street'] as String,
city: json['city'] as String,
zipCode: json['zipCode'] as String,
);
}
Map<String, dynamic> toJson() => {
'street': street, 'city': city, 'zipCode': zipCode,
};
}
class User {
final int id;
final String name;
final Address address;
User({required this.id, required this.name, required this.address});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
address: Address.fromJson(json['address'] as Map<String, dynamic>),
);
}
Map<String, dynamic> toJson() => {
'id': id, 'name': name, 'address': address.toJson(),
};
}リストと配列の処理
JSON配列はDartのList型にマッピングされます:
// JSON with arrays
{
"name": "John",
"hobbies": ["coding", "reading", "gaming"],
"orders": [
{"id": 1, "product": "Laptop", "price": 999.99},
{"id": 2, "product": "Mouse", "price": 29.99}
]
}
// Dart
class User {
final String name;
final List<String> hobbies;
final List<Order> orders;
User({required this.name, required this.hobbies, required this.orders});
factory User.fromJson(Map<String, dynamic> json) {
return User(
name: json['name'] as String,
hobbies: List<String>.from(json['hobbies'] as List),
orders: (json['orders'] as List)
.map((e) => Order.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
}Null Safetyのベストプラクティス
Dartのサウンドnull safetyでは、nullable フィールドを適切に処理する必要があります:
- 必ず存在するフィールドには `required` キーワードを使用
- nullable型には `?` サフィックスを使用(例: `String?`)
- fromJsonで `??` 演算子でデフォルト値を提供
- 値が初期化されることが確実な場合のみ `late` を使用
class User {
final int id;
final String name;
final String? bio; // nullable
final String avatarUrl; // with default
User({
required this.id,
required this.name,
this.bio,
this.avatarUrl = 'https://example.com/default-avatar.png',
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
bio: json['bio'] as String?,
avatarUrl: json['avatarUrl'] as String? ?? 'https://example.com/default-avatar.png',
);
}
}json_serializableパッケージの使用
大規模プロジェクトでは、json_serializableパッケージがボイラープレートを自動化します:
ステップ1: 依存関係を追加
# pubspec.yaml
dependencies:
json_annotation: ^4.8.1
dev_dependencies:
json_serializable: ^6.7.1
build_runner: ^2.4.6ステップ2: アノテーション付きモデルを作成
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
final int id;
final String name;
final String email;
@JsonKey(name: 'is_active')
final bool isActive;
@JsonKey(defaultValue: 0.0)
final double score;
User({
required this.id,
required this.name,
required this.email,
required this.isActive,
required this.score,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}ステップ3: Build Runnerを実行
dart run build_runner build --delete-conflicting-outputsFreezedで不変モデルを作成
freezedパッケージはcopyWith、等価性、JSON シリアライゼーション付きの不変クラスを生成します:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
required int id,
required String name,
required String email,
@Default(false) bool isActive,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}よくある間違いと修正方法
| 間違い | 修正 |
|---|---|
| nullチェックなしで `json['key']` を使用 | `json['key'] as String? ?? ''` を使用するかnullを処理 |
| ネストされたオブジェクトの変換を忘れる | オブジェクトには `NestedClass.fromJson(json['key'])` を呼ぶ |
| すべての数値に `int` を使用 | `num` を使い変換: `(json['price'] as num).toDouble()` |
| 欠けているキーを処理しない | `json.containsKey('key')` を使うかデフォルト値を提供 |
| toJsonで誤った型を返す | ネストされたオブジェクトが自身の `.toJson()` を呼ぶことを確認 |
よくある質問
JSONをDartクラスに変換する最良の方法は?
小規模プロジェクトでは手動変換が有効です。大規模プロジェクトではjson_serializableまたはfreezedパッケージでボイラープレートコードを自動生成します。
Dartで動的なJSONキーをどう処理しますか?
未知のキーを持つオブジェクトにはMap<String, dynamic>を使用します。
json_serializableとfreezedのどちらを使うべき?
シンプルなJSONシリアライゼーションにはjson_serializable。不変性、copyWith、union型も必要ならfreezedを使います。
JSON to Dart変換で日付をどう処理しますか?
DateTime.parse(json['date'])でISO 8601文字列を解析します。toJsonではdateTime.toIso8601String()を使います。