(当頁はdocs.flutter.dev(https://docs.flutter.dev/data-and-backend/serialization/json)のリファレンスブログです。)
1.概要
現在のモバイルアプリ開発では、ウェブサーバとの通信や構造化データの保存が必要になることがほとんどです。
ネットワーク接続されたアプリを作る場合、いずれは JSON(JavaScript Object Notation)データを扱う必要が出てくる可能性が高いです。
ここでは、Flutter を使った JSON の活用方法について説明しています。
また、異なるシナリオでどの JSON ソリューションを使用すべきか、そしてその理由についても解説しています。
データ構造 ===(エンコーディング、 シリアライズ)⇒ (JSON形式等の)文字列
データ構造 ⇐(デコーディング、 デシリアライズ)=== (JSON形式等の)文字列
2.適切な JSON シリアル化について
ここでは、JSONを操作する為の2つの一般的な戦略について触れています。
- 手動シリアル化
- コード生成を使用した自動シリアル化
(1)小規模プロジェクトでは、手動シリアル化を使用する
Dartの標準ライブラリ dart:convert を利用して、手動でJSONデコードを行う方法を指します。
jsonDecode() 関数を使用し、JSON文字列を Map 型に変換します。
その後、このマップから必要な値を直接参照します。
import 'dart:convert';
void main() {
String jsonString = '{"name": "John", "age": 30}';
Map<String, dynamic> jsonData = jsonDecode(jsonString);
print(jsonData['name']); // John
print(jsonData['age']); // 30
}
(参考)
(2)中~大規模プロジェクトでは、コード生成を使用する
コード生成を利用することで、JSONシリアライズに必要なボイラープレートコード(定型的なコード)を外部ライブラリが自動的に生成します。開発者はモデルクラスを定義し、ツールがそれに基づいてシリアライズ/デシリアライズ処理を生成します。
- 代表的なライブラリ
3.FlutterにはGSON/Jackson/Moshiに相当するライブラリはありません
4.dart:convert 使用による手動のJSONシリアライズ
Flutter では、dart:convert ライブラリを使用して、シンプルに JSON データのシリアライズ(データを文字列に変換)やデシリアライズ(文字列をデータに変換)ができます。
以下の例では、簡単な JSON モデル(ユーザーモデル)を使用して、手動で JSON を扱う方法について説明しています。
{
"name": "John Smith",
"email": "john@example.com"
}
4.1 インラインでの JSONシリアライズ( JsonDecode() 関数 )
dart:convert の jsonDecode() 関数を使用して、(上記の様な)JSON 文字列をデコードします。
final user = jsonDecode(jsonString) as Map<String, dynamic>;
print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');
(課題)
- 動的型(dynamic): jsonDecode() は dynamic 型を返します。
- データの型が実行時までわからない。
- 型安全性、オートコンプリート、コンパイル時のエラー検出ができない。
4.2 モデルクラスを使用した JSONシリアライズ(例:User クラス)
この問題を解決するために、まず JSON データを扱うモデルクラスを作成します。
(以下は、User クラスの例です。)
(1)JSON データを扱うモデルクラスの作成
class User {
final String name;
final String email;
User(this.name, this.email);
//JSON を User オブジェクトにデコードするコンストラクタ
User.fromJson(Map<String, dynamic> json)
: name = json['name'] as String,
email = json['email'] as String;
//User オブジェクトを JSON にエンコードするメソッド
Map<String, dynamic> toJson() => {
'name': name,
'email': email,
};
}
(2)上記モデルクラスの使用例
//JSON をデコードして User オブジェクトを作成
final userMap = jsonDecode(jsonString) as Map<String, dynamic>;
final user = User.fromJson(userMap);
print('Howdy, ${user.name}!');
print('We sent the verification link to ${user.email}.');
//User オブジェクトを JSON にエンコード
String json = jsonEncode(user);
print(json); // {"name":"John Smith","email":"john@example.com"}
(注意点)
- モデルクラスへの責任
- デコード (fromJson) やエンコード (toJson) のロジックはモデルクラス内に実装します。
- 呼び出し側のコードは、JSON 処理を意識する必要がなくなります。
- ユニットテストの重要性
- 実運用のアプリでは、User.fromJson() や User.toJson() の正確な動作を保証するために、ユニットテストを作成する必要があります。
(まとめ)
Flutter では、JSON の内容を想定して事前にモデルクラスを定義する必要があります。
このアプローチにより、型安全性が向上し、コンパイル時のエラー検出が可能となりますが、開発初期の作業量が増えるというトレードオフがあります。
これに対し、GSON/Jackson/Moshi のような動的なライブラリは開発の迅速化に寄与しますが、型安全性が低く、ランタイムエラーの可能性があるため、アプリの規模や性質によって適切な方法を選ぶ必要があります。
5.コード生成ライブラリを使用した JSON のシリアライズ
JSON のシリアライズ処理を自動化するために、コード生成ライブラリを使用できます。
ここでは、json_serializable というライブラリを使用しています。
このライブラリは、JSON シリアライズのボイラープレート(繰り返し書く必要のあるコード)を自動生成します。
(Flutter で JSON シリアライズのコードを生成するために使用される主な2つのライブラリ)
- json_serializable
- 通常の Dart クラスをアノテーション(@ を使ったメタ情報)を付与するだけでシリアライズ可能にします。
- シンプルで学習コストが低い。
- 必要な設定だけ行えば、既存のコードに最小限の変更で適用可能です。
- built_value
- 不変(Immutable)な値クラスを定義するための、より高機能な方法を提供します。
- クラスが不変であるため、コードの堅牢性が向上します。
- JSON のシリアライズ/デシリアライズにも対応していますが、セットアップがやや複雑です。
コード生成ライブラリ json_serializable によるシリアライズ手順:
5.1 json_serializable の設定
flutter pub add json_annotation
flutter pub add dev:build_runner
flutter pub add dev:json_serializable
5.2 json_serializable を使用したモデルクラスの作成
json_serializable を使ってモデルクラスを作成する方法を示します。
(1)モデルクラス作成
この例では、シンプルな JSON モデル(User クラス)を使用しています。
import 'package:json_annotation/json_annotation.dart';
//`part` ディレクティブ:
//( 生成されたコードファイルをリンク。
// ファイル名は元の Dart ファイル名に対応し、`.g.dart` が付与されます。)
part 'user.g.dart';
//`@JsonSerializable()`:
//( クラスが JSON シリアライズに対応していることを示します。
// このアノテーションを付けることで、build_runner によって必要な
// シリアライズ/デシリアライズコードが自動生成されます。)
@JsonSerializable()
class User {
User(this.name, this.email);
String name;
String email;
//User.fromJson() ファクトリコンストラクタ:
//( JSON マップ(Map<String, dynamic>)から User インスタンスを作成します。
// 生成された _UserFromJson() メソッドを呼び出します。)
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
//`toJson()` メソッド:
//( User インスタンスを JSON に変換します。
// 生成された _UserToJson() メソッドを呼び出します。)
Map<String, dynamic> toJson() => _$UserToJson(this);
}
(2)カスタマイズ例
(ⅰ)API の JSON フィールド名と Dart モデルのフィールド名が異なる場合
以下のように @JsonKey を使用して、JSON フィールドとモデルフィールドをマッピングします。
//API のフィールド "registration_date_millis" を
//Dart の "registrationDateMillis" にマッピング。
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;
(ⅱ)クラス全体のフィールド変換を一括でカスタマイズ
例えば、全ての Dart フィールドをスネークケース(snake_case)に変換する場合:
@JsonSerializable(fieldRename: FieldRename.snake)
class User {
final String userName;
final int registrationDateMillis;
}
上記設定により、userName は JSON の “user_name” に、registrationDateMillis は “registration_date_millis” に変換されます。
(3)よく使われる @JsonKey アノテーション
(ⅰ)デフォルト値の設定
JSON にキーが存在しない場合や値が null の場合、デフォルト値として false を使用。
@JsonKey(defaultValue: false)
final bool isAdult;
(ⅱ)必須フィールドの指定
JSON に指定されたキーが存在しない場合、例外をスローする。
@JsonKey(required: true)
final String id;
(ⅲ)フィールドを無視
生成されるシリアライズ/デシリアライズコードでこのフィールドを完全に無視。
@JsonKey(ignore: true)
final String verificationCode;
5.3 コード生成ユーティリティの実行
(上記の)part ‘user.g.dart’ の部分で、
” Target of URI hasn’t been generated: ‘user.g.dart’. “
というエラーが発生することがありますが、これは、以下のコード生成ユーティリティを動作させることで解消します。
(1)1度だけコードを生成する
- これにより、コード生成ユーティリティが user.g.dart のような必要なファイルを生成します。
- コマンドは一度だけ実行され、すべてのモデルクラスのボイラープレートコードが生成されます。
flutter pub run build_runner build
(2)継続的にコードを生成する
- watch モードでは、ソースコードに変更が加えられるたびに自動的にコード生成が行われます。
- 開発中に便利なモードで、モデルクラスの更新に応じて即座に生成されたコードを更新します。
flutter pub run build_runner watch
5.4 json_serializable モデルの使用
(1)JSONデコード(変換)
- json_serializable を使った JSON デコードは、これまで手動で行ったコードと同じように動作します。
- モデルクラスに手動でシリアライズ/デシリアライズのロジックを記述する必要がなくなり、json_serializable が自動生成したコードを利用することでデコードが簡単に行えます。
//jsonDecode:
//( JSON文字列をMapに変換 )
final userMap = jsonDecode(jsonString) as Map<String, dynamic>;
//User.fromJson(userMap):
//( MapからUserオブジェクトを作成 )
final user = User.fromJson(userMap);
(2)JSONエンコード(変換)
- JSON エンコードのコードも手動で行う場合と同じように利用可能。
- json_serializable を使うことで、エンコードのロジックは .g.dart ファイルに自動生成され、開発者が記述する必要はありません。
//jsonEncode:
//( UserオブジェクトをJSON文字列に変換。
// json_serializable が生成した _$UserToJson メソッドが内部的に利用されます。)
String json = jsonEncode(user);
6.入れ子クラスのコード生成
クラスの中に別のクラス(入れ子クラス)が含まれている場合、JSON形式でそのクラスを渡そうとするとエラーが発生することがあります。(例として、Address クラスを User クラスに含めた場合を考えます。)
(例1)Addressクラスの定義
import 'package:json_annotation/json_annotation.dart';
part 'address.g.dart';
@JsonSerializable()
class Address {
String street; //通りの名称
String city; //都市の名称
Address(this.street, this.city);
//JSONから生成
factory Address.fromJson(Map<String, dynamic> json) =>
_$AddressFromJson(json);
//JSONへ変換
Map<String, dynamic> toJson() => _$AddressToJson(this);
}
(例2)User クラスに入れ子の形で Address クラスを使用
import 'package:json_annotation/json_annotation.dart';
import 'address.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
User(this.name, this.address);
String name; //ユーザー名
Address address; //アドレス(入れ子クラス)
//JSONから生成
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
//JSONへ変換
Map<String, dynamic> toJson() => _$UserToJson(this);
}
(問題の発生)
上記コードで 、” dart run build_runner build –delete-conflicting-outputs ” を実行すると、
” .g.dart ” ファイルが生成されます。
しかし、自動生成された _$UserToJson メソッドを見ると以下の様になります。
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'name': instance.name,
'address': instance.address, //Addressクラスがそのまま出力される
};
この状態で、以下のように toJson を呼び出すと・・・
Address address = Address('My st.', 'New York');
User user = User('John', address);
print(user.toJson());
出力結果は以下の様になり、不完全です。
{name: John, address: Instance of 'Address'}
期待する出力形式は以下のような、ネストされたJSONデータです。
{name: John, address: {street: My st., city: New York}}
これを実現するには、@JsonSerializable() アノテーションに explicitToJson: true を追加します。
import 'package:json_annotation/json_annotation.dart';
import 'address.dart';
part 'user.g.dart';
@JsonSerializable(explicitToJson: true)
class User {
User(this.name, this.address);
String name; //ユーザー名
Address address; //アドレス(入れ子クラス)
//JSONから生成
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
//JSONへ変換
Map<String, dynamic> toJson() => _$UserToJson(this);
(explicitToJson の効果)
explicitToJson: true を指定すると、_$UserToJson メソッドが以下のように修正されます
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'name': instance.name,
//AddressクラスのtoJsonを呼び出す
'address': instance.address.toJson(),
};
これにより、Address クラスの toJson メソッドが自動的に呼び出され、期待するネストされたJSON形式が得られます。
(最終的な結果)
修正版コードを使うと以下のように正しく出力されます。
{name: John, address: {street: My st., city: New York}}
(補足)
- explicitToJson を使う理由
- 入れ子構造を持つクラスで、子クラスの toJson メソッドを明示的に呼び出すため。
- デフォルトでは、入れ子クラスが直接シリアライズされず、Instance of ‘ClassName’ のように表示される。
- さらに詳しい情報:
- explicitToJson の詳細は、json_annotation パッケージのドキュメント(JsonSerializable クラス)を参照。
これで、入れ子構造を持つクラスのJSONシリアライズに対応できます。