昨日の記事の以下の疑問へのアンサーブログです(笑)
そして、今回の対応で最後までよくわからなかったのが、jsonDecode関数のよってJSONデータをDartデータに変換する処理です。
結局、以下の通り、愚直にやるととりあえずうまくいきましたが、どう見ても冗長でうまくありません。
アプリ開発していれば、こんなケースは頻出中の頻出だと思うのですが、ググってもイマイチ定番の書き方がわかりませんでした。。
改めて、手動でJSONデコード書くのは面倒すぎると思って調べてみると、やっぱり自動化ツールがありました。「json_serializable」です。
仕組みとしては、モデルクラスを定義、コードジェネレータ「json_serializable」によって、そのクラスにJSONのデコード処理(fromJson)、エンコード処理(toJson)のメソッドを自動生成する感じです。
早速、試してみたのでご紹介します。
準備
本命の「json_serializable」と「json_annotation」「build_runner」を追加します。
# pubspec.yaml
dependencies:
json_annotation: ^4.0.1 # これを追加
dev_dependencies:
build_runner: ^2.0.6 # これを追加
json_serializable: ^4.1.3 # これを追加
デコードするJSONデータ
昨日のブログで手動でJSONデコードしたものと同じものを用意しました。
const jsonString = '''
{
"properties": {
"genres": {
"id": ":NbD",
"type": "multi_select",
"multi_select": {
"options": [
{
"id": "a1883558-276c-4094-a720-b895694a9271",
"name": "異世界転生",
"color": "brown"
}
]
}
}
}
}
''';
モデルクラスの作成
JSONをデコードした結果を、Dartで操作するために、モデルクラスを作成します。
また、このモデルクラスは、コードジェネレータがJSONデータのデコード処理のソースを自動生成するためのテンプレート(雛形)となります。
モデルクラスは、JSONのハッシュ(DartのMap)、配列(DartのList)単位ごとに、ひとつずつ作る必要があります。
つまり、JSONが複雑であればあるほど、ここの定義は多くなりますが、デコード処理をひとつずつ書くのに比べれば大したことないと思います。
(例)JSONハッシュのDartモデルクラス
JSONハッシュをDartモデルクラスに落とし込んだ例です。
「@JsonSerializable()」は、このクラスが、コードジェネレート対象であることを示すアノテーションです。
この時点では、コードジェネレータ実行前なので、*FromJson、*ToJson、「part ‘mode.g.dart」がコンパイルエラーとなりますが気にしなくてよいです。
// JSONハッシュ
{
"id": "a1883558-276c-4094-a720-b895694a9271",
"name": "異世界転生",
"color": "brown"
}
// lib/model.dart
// Dartモデルクラス
import 'package:json_annotation/json_annotation.dart';
part 'model.g.dart';
@JsonSerializable()
class Option {
Option({
required this.id,
required this.name,
required this.color,
});
factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json);
String id;
String name;
String color;
Map<String, dynamic> toJson() => _$OptionToJson(this);
}
(例)JSON配列のDartモデルクラス
JSON配列をDartモデルクラスに落とし込んだ例です。
「@JsonSerializable(explicitToJson: true)」は、JSONがネストしていることを示すアノテーションです。
「List<Options> options;」の部分がネスト部分です。
↑で作ったJSONハッシュのモデルクラスを配列として持っています。
// JSON配列
{
"properties": {
"multi_select": {
"options": [
{
"id": "a1883558-276c-4094-a720-b895694a9271",
"name": "異世界転生",
"color": "brown"
}
]
}
}
// lib/model.dart
// Dartモデルクラス
@JsonSerializable(explicitToJson: true)
class MultiSelect {
MultiSelect({
required this.options,
});
factory MultiSelect.fromJson(Map<String, dynamic> json) =>
_$MultiSelectFromJson(json);
List<Option> options;
Map<String, dynamic> toJson() => _$MultiSelectToJson(this);
}
JSONのキーとモデルクラスのメンバ変数の名前を変えたい場合は、「@JsonKey」アノテーションをつけることで対応できます。
今回の例では、「multi_select」を「multiSelect」にしたかったので以下のようにしました。
@JsonSerializable(explicitToJson: true)
class Genres {
Genres({
required this.id,
required this.type,
required this.multiSelect,
});
factory Genres.fromJson(Map<String, dynamic> json) => _$GenresFromJson(json);
String id;
String type;
@JsonKey(name: 'multi_select') // @JsonKeyアノテーション:multi_select -> multiSelect
MultiSelect multiSelect;
Map<String, dynamic> toJson() => _$GenresToJson(this);
}
コードジェネレータでJSONデコード用のソースを自動生成
モデルクラスが出来たのでいよいよコードジェネレータを実行します。
コマンドラインで以下を実行すると、コードジェネレータがモデルクラスファイル(lib/model.dart)からデコード処理のためのファイル(lib/model.g.dart)を自動生成します。
デコードメソッド(FromJson)とエンコードメソッド(ToJson)が生成されます。
>flutter packages pub run build_runner build
実装を見てみると、私が昨日の記事でやったのと同様、「Map<String, dynamic>」にcastしながら地道にデコードしていっているのがわかります。
やり方、間違っていなかったんだと少々嬉しくなりました(笑)
// lib/model.g.dart(抜粋)
// Optionモデルクラス用
Option _$OptionFromJson(Map<String, dynamic> json) {
return Option(
id: json['id'] as String,
name: json['name'] as String,
color: json['color'] as String,
);
}
Map<String, dynamic> _$OptionToJson(Option instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'color': instance.color,
};
// MultiSelectモデルクラス用
MultiSelect _$MultiSelectFromJson(Map<String, dynamic> json) {
return MultiSelect(
options: (json['options'] as List<dynamic>)
.map((e) => Option.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
Map<String, dynamic> _$MultiSelectToJson(MultiSelect instance) =>
<String, dynamic>{
'options': instance.options.map((e) => e.toJson()).toList(),
};
ちなみに、buildをwatchにすることで、モデルクラスの変更を検知して、コードジェネレータを自動実行することもできます。
>flutter packages pub run build_runner watch
使ってみる
これで準備は完了です。では、実際に使ってみます。
import 'model.dart';
final model = Model.fromJson(jsonDecode(jsonString) as Map<String, dynamic>);
debugPrint(model.toJson().toString()); // (1)
debugPrint(model.properties.genres.multiSelect.options[0].id); // (2)
debugPrint(model.properties.genres.multiSelect.options[0].name); // (3)
debugPrint(model.properties.genres.multiSelect.options[0].color); // (4)
JSONの各要素に、Dart変数としてアクセス出来るのがわかると思います。
これはめちゃくちゃ便利です。
>flutter run
(1) flutter: {properties: {genres: {id: :NbD, type: multi_select, multi_select: {options: [{id: a1883558-276c-4094-a720-b895694a9271, name: 異世界転生, color: brown}]}}}}
(2) flutter: a1883558-276c-4094-a720-b895694a9271
(3) flutter: 異世界転生
(4) flutter: brown
複雑なJSONデータをコードジェネレータ「json_serializable」を使ってデコードを自動化する方法を見てきました。
JSONが複雑になると手動でデコード処理を書くのは やってられないので、このパッケージはかなり便利だと思います。
「来期アニメ」アプリを作る前はこれを知らなかったので手動でデコード処理を書きましたが、次にJSONデコードが必要になった際には、絶対に使うと思います。
以上、私と同じようにFlutterのJSONデコードに苦労している方の参考になれば幸いです。
参考にした記事
素晴らしいパッケージ、記事、ありがとうございます。





最後に宣伝
自作アプリ作ってます。ダウンロード伸びなくてテンション下がり気味なのでインストールお願いします!
ソース全文も載せておきます。
// lib/main.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'model.dart';
void main() {
const jsonString = '''
{
"properties": {
"genres": {
"id": ":NbD",
"type": "multi_select",
"multi_select": {
"options": [
{
"id": "a1883558-276c-4094-a720-b895694a9271",
"name": "異世界転生",
"color": "brown"
}
]
}
}
}
}
''';
final model = Model.fromJson(jsonDecode(jsonString) as Map<String, dynamic>);
debugPrint(model.toJson().toString());
debugPrint(model.properties.genres.multiSelect.options[0].id);
debugPrint(model.properties.genres.multiSelect.options[0].name);
debugPrint(model.properties.genres.multiSelect.options[0].color);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
// lib/model.dart
import 'package:json_annotation/json_annotation.dart';
part 'model.g.dart';
@JsonSerializable()
class Option {
Option({
required this.id,
required this.name,
required this.color,
});
factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json);
String id;
String name;
String color;
Map<String, dynamic> toJson() => _$OptionToJson(this);
}
@JsonSerializable(explicitToJson: true)
class MultiSelect {
MultiSelect({
required this.options,
});
factory MultiSelect.fromJson(Map<String, dynamic> json) =>
_$MultiSelectFromJson(json);
List<Option> options;
Map<String, dynamic> toJson() => _$MultiSelectToJson(this);
}
@JsonSerializable(explicitToJson: true)
class Genres {
Genres({
required this.id,
required this.type,
required this.multiSelect,
});
factory Genres.fromJson(Map<String, dynamic> json) => _$GenresFromJson(json);
String id;
String type;
@JsonKey(name: 'multi_select')
MultiSelect multiSelect;
Map<String, dynamic> toJson() => _$GenresToJson(this);
}
@JsonSerializable(explicitToJson: true)
class Properties {
Properties({
required this.genres,
});
factory Properties.fromJson(Map<String, dynamic> json) =>
_$PropertiesFromJson(json);
Genres genres;
Map<String, dynamic> toJson() => _$PropertiesToJson(this);
}
@JsonSerializable(explicitToJson: true)
class Model {
Model({
required this.properties,
});
factory Model.fromJson(Map<String, dynamic> json) => _$ModelFromJson(json);
Properties properties;
Map<String, dynamic> toJson() => _$ModelToJson(this);
}
// lib/model.g.dart(自動生成されるファイル)
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Option _$OptionFromJson(Map<String, dynamic> json) {
return Option(
id: json['id'] as String,
name: json['name'] as String,
color: json['color'] as String,
);
}
Map<String, dynamic> _$OptionToJson(Option instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'color': instance.color,
};
MultiSelect _$MultiSelectFromJson(Map<String, dynamic> json) {
return MultiSelect(
options: (json['options'] as List<dynamic>)
.map((e) => Option.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
Map<String, dynamic> _$MultiSelectToJson(MultiSelect instance) =>
<String, dynamic>{
'options': instance.options.map((e) => e.toJson()).toList(),
};
Genres _$GenresFromJson(Map<String, dynamic> json) {
return Genres(
id: json['id'] as String,
type: json['type'] as String,
multiSelect:
MultiSelect.fromJson(json['multi_select'] as Map<String, dynamic>),
);
}
Map<String, dynamic> _$GenresToJson(Genres instance) => <String, dynamic>{
'id': instance.id,
'type': instance.type,
'multi_select': instance.multiSelect.toJson(),
};
Properties _$PropertiesFromJson(Map<String, dynamic> json) {
return Properties(
genres: Genres.fromJson(json['genres'] as Map<String, dynamic>),
);
}
Map<String, dynamic> _$PropertiesToJson(Properties instance) =>
<String, dynamic>{
'genres': instance.genres.toJson(),
};
Model _$ModelFromJson(Map<String, dynamic> json) {
return Model(
properties: Properties.fromJson(json['properties'] as Map<String, dynamic>),
);
}
Map<String, dynamic> _$ModelToJson(Model instance) => <String, dynamic>{
'properties': instance.properties.toJson(),
};
コメント