Flutter - json_serializableを理解するメモ

1. はじめに

Flutterでアプリ作ろうと思ったら多くの場合、サーバ側のWebAPIと通信することが想定されます。

HTTP越しの通信でデータの受け渡しをする、つまりjsonデータのやり取りが必要→アプリではjson文字列ではなくモデルクラスのようなオブジェクトでデータを扱いたい→サーバに送り返すときにはモデルオブジェクトを再びjsonに変換して送りたい。。。

つまり json <-> object の serialize / deserialize ですね。
公式ページだと「json_serializable」と「built_value」が紹介されていて、特別に新しい技術でもないので、既に色々な方がブログっております。

が、まあ自分用という意味でもメモブログしておきます。

今回使った環境は、、、
OS: Windows 10 x64
Flutterバージョンは以下の通り。

$ flutter --version

Flutter 0.9.4 • channel beta • https://github.com/flutter/flutter.git
Framework • revision f37c235c32 (5 weeks ago) • 2018-09-25 17:45:40 -0400
Engine • revision 74625aed32
Tools • Dart 2.1.0-dev.5.0.flutter-a2eb050044

2. json_serializableとは

Flutter(というかDart)で json文字列<->オブジェクト の serialize / deserialize を楽に実装するためのライブラリです。
では早速、使ってみようかと。

3. 使ってみよう

以下のようなロジックを実装してみます。

上記ロジックをFlutterアプリ内で実装し、print()関数でコンソールに出力します。
(なのでFlutterアプリですが、UI無視のサンプルとします)

3.1. flutterプロジェクトを作成

以下のコマンドで「json_example」という名前のFlutterプロジェクトを作成します。

$ flutter create json_example

3.2. 依存パッケージ追加

json_serializableを使うために「json_serializable」「json_annotation」「build_runner」の3つのパッケージを「pubspec.yaml」定義に追加します。

...省略...

dependencies:
  flutter:
    sdk: flutter
  json_annotation: ^2.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  json_serializable: ^2.0.0
  build_runner: ^1.0.0

...省略...

各パッケージの概要は以下の通り:
json_serializable : json serialize / deserializeを行うパッケージ
json_annotation: json_serializableで使用されるアノテーションを定義するパッケージ
build_runner: 「json serialize / deserializeを行うためのコード」の自動生成を行うためのビルドランナー

以下のコマンドでパッケージを取得します。(VS Code+pluginやandroid Studio環境を使っていればIDEが自動で実行)

$ flutter packages get

3.3. モデルクラス作成

Employeeクラスを作成します。

// ./lib/employee.dart ファイル

import 'package:json_annotation/json_annotation.dart';

part 'employee.g.dart';

@JsonSerializable()
class Employee {
  Employee(this.id, this.firstName, this.lastName);

  int id;
  String firstName;
  String lastName;
  
  factory Employee.fromJson(Map<String, dynamic> json) => _$EmployeeFromJson(json);
  Map<String, dynamic> toJson() => _$EmployeeToJson(this);
}

モデルクラスは以下のような定型フォーマットで記述します。上記Employeeもこのフォーマットに従っています。
※xyzおよび【】部分が作成するモデルクラス名。
※別途メソッド定義を追加するのは自由。
※サンプルでは扱っていないが、アノテーションなども適時付けられる。

// ./lib/【xyz】.dart ファイル

import 'package:json_annotation/json_annotation.dart';

part '【xyz】.g.dart';

@JsonSerializable()
class 【Xyz】 {
  【Xyz】(【プロパティ定義を並べる】);

  【プロパティ定義を並べる】
  ...
  
  factory 【Xyz】.fromJson(Map<String, dynamic> json) => _$【Xyz】FromJson(json);
  Map<String, dynamic> toJson() => _$【Xyz】ToJson(this);
}

3.4. serialize / deserializeコードを自動生成

以下のコマンドを実行するとモデルクラスに対応した自動生成コードが生成されます。

$ flutter packages pub run build_runner build

※ 「flutter packages pub run build_runner watch」とすると
モデルクラスの変更を監視してファイル編集の度に自動生成が実行されます。

本サンプルでは「employee.dart」に対応した「employee.g.dart」ファイルが自動生成されます。
何に対して自動生成コードが作成されるかというと「@JsonSerializable()」アノテーションが付与されているクラスに対して生成されます。
自動生成された ./lib/employee.g.dart は以下の通りです。

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'employee.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

Employee _$EmployeeFromJson(Map<String, dynamic> json) {
  return Employee(json['id'] as int, json['firstName'] as String,
      json['lastName'] as String);
}

Map<String, dynamic> _$EmployeeToJson(Employee instance) => <String, dynamic>{
      'id': instance.id,
      'firstName': instance.firstName,
      'lastName': instance.lastName
    };

ポイント①$EmployeeFromJson() / $EmployeeToJson()

自動生成されたコードには、$EmployeeFromJson() / $EmployeeToJson() の2つのメソッドが定義されています。
中身をよく見ると、、、
$EmployeeFromJson()は、Mapオブジェクトから「id」「firstName」「lastName」の各要素を取り出してEmployeeオブジェクトを作成する実装(つまり、json文字列 -> Employeeの処理)。
$EmployeeToJson()は、Employeeオブジェクトから各プロパティ要素「id」「firstName」「lastName」を取り出してMapオブジェクトを作成する実装(つまり、Employee→Mapの処理)。
になっています。

ポイント②part of 'employee.dart';

自動生成コードの先頭部分を見てみると「part of 'employee.dart';」と記述されています。
「part of」はdart言語仕様のキーワードです。
簡単に言うと「自分は employee.dart の一部ですよ」と定義しています。

ポイント③もう一度 employee.dart を振り返る

前述の employee.dart を以下に再掲します。

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'employee.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

Employee _$EmployeeFromJson(Map<String, dynamic> json) {
  return Employee(json['id'] as int, json['firstName'] as String,
      json['lastName'] as String);
}

Map<String, dynamic> _$EmployeeToJson(Employee instance) => <String, dynamic>{
      'id': instance.id,
      'firstName': instance.firstName,
      'lastName': instance.lastName
    };
part 'employee.g.dart'; 定義

employee.dart では定型記述として「part 'employee.g.dart';」と定義していました。
dart言語の「part」キーワード定義であり、つまり「employee.g.dartは自分の子(パート)ですよ」という意味になります。

$EmployeeFromJson(json) / $Employee_ToJson(this)の利用

さらに、「Employee.fromJson()メソッドの定義実装で$EmployeeFromJson(json)」を呼び出し、「toJson()メソッド定義実装で$Employee_ToJson(this)」を呼び出していました。

つまり、モデルクラスは「自動生成されるコードに合わせた形」で定義していたわけです。
上記を理解するとコードのロジックがすべてクリアに見渡せるようになると思います。

3.5. Employeeクラスをserialize / deserializeしてみる

では main.dart からEmployeeクラスのserialize / deserializeを行ってみます。

// ./lib/main.dart

import 'package:flutter/material.dart';
import 'dart:convert';
import 'employee.dart';

void main() {
  String orgJson = '{"id":1000,"firstName":"ryuichi","lastName":"daigo"}';
  print('オリジナルJSON文字列: $orgJson');
  
  var mapEmployee = json.decode(orgJson);
  var employee = Employee.fromJson(mapEmployee);
  print('JSON→Employeeデシリアライズしたオブジェクト: id: ${employee.id}, firstName: ${employee.firstName}, lastName: ${employee.lastName}');

  var serializedJson = json.encode(employee.toJson());
  print('再度Employee→JSONシリアライズした文字列: $serializedJson');

  runApp(new MyApp());
}

...省略...

実行結果(コンソール出力)は以下の通り。

$ flutter run

...省略...
I/flutter (28581): オリジナルJSON文字列: {"id":1000,"firstName":"ryuichi","lastName":"daigo"}
I/flutter (28581): JSON→Employeeデシリアライズしたオブジェクト: id: 1000, firstName: ryuichi, lastName: daigo
I/flutter (28581): 再度Employee→JSONシリアライズした文字列: {"id":1000,"firstName":"ryuichi","lastName":"daigo"}
...省略...

3. まとめ

json_serializableについては、初見では、なんか良く分からん定型的な形式でモデルクラスを定義し、ジェネレータを使うといい感じに json serialize / deserialize 可能なパッケージというモヤモヤ感を持っていました。
ただ上述ように、ジェネレートされた僅かなコードを覗くことで、すべてが一本の線につながって気持ちよくなるかと思います。

最後に公式のリンクを4つ貼っておきます。

↓ Flutter公式のjson serialize説明 flutter.io

json_serializableパッケージ pub.dartlang.org

json_annotationパッケージ pub.dartlang.org

↓ build_runnerパッケージ pub.dartlang.org