Open6

freezedを利用したJSONパースの実装方法

koji-1009koji-1009

FlutterでJSONのパース処理を行う必要は、まず大抵のアプリケーション発生します。
そんなわけで、公式ドキュメントに実装例が紹介されています。

https://docs.flutter.dev/cookbook/networking/background-parsing

ただ、Flutterの開発経験が長い方だと「せめてjson_serializableは使ったほうが……」という気持ちになると思いますし、「freezedでやった方がもっと便利でしょ」とも思うのではないでしょうか。
諸事情により聞き取りなどを行なったところ、案外この辺りの対応の手引きが少ないようでした。なので、参考になればと、自分なりの理解と実装方法を記載しておきます。

koji-1009koji-1009

https://docs.flutter.dev/development/data-and-backend/json

JSONのパース処理の解説については、実は個別にドキュメントが用意されています。
量もそこまでないので、ざっと流し見てもらうと、「Dart(Flutter)の環境でどのようにJSONをDartのオブジェクトにするか」が議論されているかがわかります。


Dartを始め、JavaやKotlin、そしてSwiftにおいてJSONは「ただの文字列(String)」として扱われます。HTTP GETなどを行う処理の中では、文字列ではなくバイト列として扱われているかもしれません。

StringはStringであるため、JSONの構造をDartやKotlin、Swiftのクラスに転写しなければなりません。
この処理が言語機能に組み込まれているケース(SwiftのCodable)や、準公式のツール(KotlinのKotlin serialization)、そしてデファクトスタンダード的なOSS(JavaのGsonMoshi)などがあります。

Dartにおいては、Dartチームが提供しているJSONをDartのオブジェクトに変換する仕組みはありません。
代わりに紹介されているのがjson_serializableです。


Dartはさまざまな面でJavaに似ていると言われますが、reflectionについては明確に異なります。
Dartでreflectionを利用できないわけではない(らしい)のですが、ほとんどのケースで利用しません。このため、Java/Kotlinにおけるreflectionを利用するライブラリのようなライブラリは、あまり採用されません。
代わりに、build_runnerを利用したコードの自動生成が採用されます。(Dartにstatic meta programmingの機能が入ると、変化する可能性があります)

koji-1009koji-1009

json_serializableを利用すると、「JSONの内部構造をDartのクラスとして書き下すだけ」で、JSONの文字列(を jsonDecode して得られるMap<String, dynamic>)から、Dartのオブジェクトを生成できるようになります。

https://docs.flutter.dev/development/data-and-backend/json#creating-model-classes-the-json_serializable-way

この時、自動的に変換が可能な型は、ドキュメントに記載されています。
さりげなくDateTimeEnumもサポートされているので、ほとんど標準設定で対応できます。

https://pub.dev/packages/json_serializable#supported-types

FirestoreのTimestamp型など、標準で用意されていない場合には、コンバーターを書くことで対応します。
私が知っている範囲では、DateTime 型かつ.toLocale()された値を取るためのコンバーターを作成するなど、ちょっとカスタムしたい場合に利用することもあります。

https://stackoverflow.com/a/62462178

koji-1009koji-1009

freezedは、Dartでデータクラスを作成したい時に利用する、ほぼ必須ツールです。
紹介動画もあるので、初見の人は動画から確認するのが良いでしょう。

https://www.youtube.com/watch?v=RaThk0fiphA


freezedを利用すると、データクラスに必要な機能(toStringcopyWithメソッド)が追加され、不変オブジェクト(@immutable)として利用できるようになります。
画面間やレイヤー間でやり取りするオブジェクトを不変オブジェクトにすることは、今時のFlutterアプリケーション開発では、ほぼ必須です。

https://medium.com/flutter-jp/immutable-d23bae5c29f8


そんなfreezedは、json_serializableをサポートしています。

https://pub.dev/packages/freezed#fromjsontojson

この対応により、freezedで作成したクラスを「JSONのデータをパースして作成する、Dartのオブジェクトのクラス」にすることができます。

koji-1009koji-1009

公式サンプルに従って、次のJSONをパースする際のクラスを解説します。

{
  "name": "John Smith",
  "email": "john@example.com"
}

ファイルは src/entity/user.dart として定義していることにします。
まず、このファイルをどのようなDartのオブジェクトにしたいかを考えて、freezedのクラスを生成します。

import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';


class User with _$User {
  const factory User({
    required String name,
    required String email,
  }) = _User;
}

次に、このクラスをjson_serializableに対応させます。

import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';


class User with _$User {
  const factory User({
    required String name,
    required String email,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) =>_$UserFromJson(json);
}

この状態でbuild_runnerを実行すると、User.fromJson(jsonDecode(data))でJSONからUserオブジェクトを生成できるようになります。

koji-1009koji-1009

地味に忘れがちなのが、JSONのプロパティ名からDartのプロパティ名への変換などです。
freezedはjson_serializableのbuild.yamlファイルの設定と組み合わせることができます。

例えば、次のようなJSONがあるとします。

{
  "user_id": "abcdefg_123456",
  "name": "John Smith",
  "email": "john@example.com"
}

Dartにおいて、プロパティ名はcamelCaseで記述するというルールがあります。
このため、freezedで生成するクラスは次のような記述になるでしょう。

import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';


class User with _$User {
  const factory User({
    required Stirng userId,
    required String name,
    required String email,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) =>_$UserFromJson(json);
}

この時、@JsonKey(name: 'user_id')を記述するか、build.yamlで「snake_caseをcamelCaseに変換する」ルールを指定するかを選ぶことができます。
個人的には、build.yamlで指定することをおすすめしています。


build.yamlはjson_serializableのドキュメントにあるものを、そのまま利用すればOKです。

https://pub.dev/packages/json_serializable#build-configuration

ここにあるbuld.yamlファイルを、pubspec.yamlと同じ階層に設置します。

targets:
  $default:
    builders:
      json_serializable:
        options:
          # Options configure how source code is generated for every
          # `@JsonSerializable`-annotated class in the package.
          #
          # The default value for each is listed.
          any_map: false
          checked: false
          constructor: ""
          create_factory: true
          create_field_map: false
          create_to_json: true
          disallow_unrecognized_keys: false
          explicit_to_json: false
          field_rename: snake # noneからsnakeに変更
          generic_argument_factories: false
          ignore_unannotated: false
          include_if_null: true

field_renamenoneからsnakeに変更することで、snake_caseが自動的に変換されるようになります。当然、toJson時にもsnake_caseのJSONが生成されるようになります。