FlutterにはGSONが無い、リフレクションが無いから

長くAndroidアプリエンジニアをやってきて最近Flutterを始めました。その中でJSONパースしてをオブジェクトに変換するやりかたについてAndroidと大きく違うと思ったので紹介します。内容としてはほぼFlutter公式ドキュメントの[JSON and serialization](https://flutter.dev/docs/development/data-and-backend/json)ページのものになります。

# Androidの場合

このような名刺情報のJSONがあったとして
（乱数で作成した疑似個人情報です）

```json
{
  "id": 1,
  "name": "小林 美穂",
  "company": "田中繊維株式会社",
  "division": "営業部",
  "title": "係長"
}
```

Kotlinで同じフィールド名と型を持つクラスを作成すると

```kotlin
data class Card(val id: Int, val name: String, val company: String,
    val division: String, val title: String)
```

GSONでパースできます。

```kotlin
val card = gson.fromJson(json, Card::class.java)
```

Javaでもやりかたは同様です。

# FlutterにはGSONと同等の物が無い

FlutterでもGSON相当の物があると思っていましたが無かったです。

FlutterでのJSONデコードのやりかたはFlutter公式ドキュメントの[JSON and serialization](https://flutter.dev/docs/development/data-and-backend/json)ページに載っていました。親切にも[Is there a GSON/Jackson/Moshi equivalent in Flutter?](https://flutter.dev/docs/development/data-and-backend/json#is-there-a-gsonjacksonmoshi-equivalent-in-flutter)節があり、`The simple answer is no.`と私と同じことを考えていた人を適切に導いています。

## Flutterにはリフレクションが無い

その節ではGSON/Jackson/Moshiと同等の物が無い理由としてリフレクションを無効化しているからと説明しています。リフレクションとは実行時にオブジェクトの構造を得ることです。例えばこのようにクラスのフィールド一覧を実行時に得ることができます。

```kotlin
Card::class.java.declaredFields.map {
   println(it.name)
}
```

Flutterはリリースビルド時に不要コード除去を行いアプリサイズを最適化します。しかしリフレクションは不要コード除去に邪魔になる機能です。リフレクションはすべてのコードが存在することを意図しているため、どのコードが不要なのか分からなくなってしまいます。
そうならば、Androidの[ProGuard](https://developer.android.com/studio/build/shrink-code?hl=JA#keep-code)みたくリフレクションの対象になっているクラスを明示的に指定すれば良いのではと思うのですが、そういう設計思想では無いようです。

# FlutterでのJSONデコードのやりかた

FlutterでJSONデコードを行う方法は2つあります。

1. Map<String, dynamic> クラスのインスタンスから手動でパースする
2. ソースコード生成を行う

## 手動でパースする

```dart
// convertライブラリをインポートする
import 'dart:convert';
```

```dart
// JSON文字列をMap<String, dynamic>に変換
final map = json.decode(jsonString);
final card = Card();
card.id = map['id'];
card.name = map['name'];
card.company = map['company'];
card.division = map['division'];
card.title = map['title'];
```

一見面倒ですが、これで良いと思います。
サーバー側から取得したデータはデータベースのスキーマに基づいていて画面表示用には使いにくいケースがあり、その場合は画面表示用の使いやすい形に手動で変換します。手動で変換するならば元がオブジェクトでもマップでも、記述量はそれほど変わらないと思います。

## ソースコード生成を行う

pubspec.yamlファイルを編集して必要なライブラリを定義します。

```pubspec.yaml
dependencies:
  json_annotation: 2.4.0

dev_dependencies:
  build_runner: 1.5.2
  json_serializable: 3.0.0
```

このようにクラスを定義します。

```card.dart
// json_annotationライブラリを使う
import 'package:json_annotation/json_annotation.dart';

// 自動生成されたメソッドにこのクラスからアクセスできるようにする。
part 'card.g.dart';

// アノテーションをつける
@JsonSerializable()
class Card {
  Card(this.id, this.name, this.company, this.division, this.title);
  int id;
  String name;
  String company;
  String division;
  String title;
  // MapからCardへの変換
  factory Card.fromJson(Map<String, dynamic> json) =>
      _$CardFromJson(json) /*自動生成される*/;
  // CardからMapへの変換
  Map<String, dynamic> toJson() => _$CardToJson(this) /*自動生成される*/;
}

```

コマンドラインで以下のコマンドを実行します。

```sh
flutter pub run build_runner build
```

このようなコードが生成されます。


```card.g.dart
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'card.dart';

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

Card _$CardFromJson(Map<String, dynamic> json) {
  return Card(
      json['id'] as int,
      json['name'] as String,
      json['company'] as String,
      json['division'] as String,
      json['title'] as String);
}

Map<String, dynamic> _$CardToJson(Card instance) => <String, dynamic>{
      'id': instance.id,
      'name': instance.name,
      'company': instance.company,
      'division': instance.division,
      'title': instance.title
    };
```

このように使うことができます。

```dart
final map = json.decode(jsonString);
final card = Card.fromJson(map);
```

# 感想

Flutterを使うときはAndroidのやりかたに固執してはいけないと思いました。