Flutter/Dartで複雑なJSONデータをコードジェネレータ「json_serializable」を使って自動デコードする方法

昨日の記事の以下の疑問へのアンサーブログです(笑)

そして、今回の対応で最後までよくわからなかったのが、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()」は、このクラスが、コードジェネレート対象であることを示すアノテーションです。

// JSONハッシュ
{
   "id": "a1883558-276c-4094-a720-b895694a9271",
   "name": "異世界転生",
   "color": "brown"
 }
// lib/model.dart
// 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デコードに苦労している方の参考になれば幸いです。

参考にした記事

素晴らしいパッケージ、記事、ありがとうございます。

json_serializable | Dart Package
Automatically generate code for converting to and from JSON by annotating Dart classes.
json_annotation | Dart Package
Classes and helper functions that support JSON code generation via the `json_serializable` package.
build_runner | Dart Package
A build system for Dart code generation and modular compilation.
[Flutter] JSONファイルの扱い方 - Qiita
1. はじめに Dart公式のjson_annotationライブラリを利用し、Flutterアプリで簡単にJSONファイルを扱える方法についてまとめています。 利用するライブラリ一覧 json_annotation b...
【Flutter】JSONをデコードする - Qiita
この記事は Flutter Advent Calendar 2018 - Qiita の20日目の記事です。 概要 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(),
    };

コメント

タイトルとURLをコピーしました