freezedを利用したJSONパースの実装方法
FlutterでJSONのパース処理を行う必要は、まず大抵のアプリケーション発生します。
そんなわけで、公式ドキュメントに実装例が紹介されています。
ただ、Flutterの開発経験が長い方だと「せめてjson_serializableは使ったほうが……」という気持ちになると思いますし、「freezedでやった方がもっと便利でしょ」とも思うのではないでしょうか。
諸事情により聞き取りなどを行なったところ、案外この辺りの対応の手引きが少ないようでした。なので、参考になればと、自分なりの理解と実装方法を記載しておきます。
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のGsonやMoshi)などがあります。
Dartにおいては、Dartチームが提供しているJSONをDartのオブジェクトに変換する仕組みはありません。
代わりに紹介されているのがjson_serializableです。
Dartはさまざまな面でJavaに似ていると言われますが、reflectionについては明確に異なります。
Dartでreflectionを利用できないわけではない(らしい)のですが、ほとんどのケースで利用しません。このため、Java/Kotlinにおけるreflectionを利用するライブラリのようなライブラリは、あまり採用されません。
代わりに、build_runnerを利用したコードの自動生成が採用されます。(Dartにstatic meta programmingの機能が入ると、変化する可能性があります)
json_serializableを利用すると、「JSONの内部構造をDartのクラスとして書き下すだけ」で、JSONの文字列(を jsonDecode して得られるMap<String, dynamic>
)から、Dartのオブジェクトを生成できるようになります。
この時、自動的に変換が可能な型は、ドキュメントに記載されています。
さりげなくDateTime
やEnum
もサポートされているので、ほとんど標準設定で対応できます。
FirestoreのTimestamp型など、標準で用意されていない場合には、コンバーターを書くことで対応します。
私が知っている範囲では、DateTime
型かつ.toLocale()
された値を取るためのコンバーターを作成するなど、ちょっとカスタムしたい場合に利用することもあります。
freezedは、Dartでデータクラスを作成したい時に利用する、ほぼ必須ツールです。
紹介動画もあるので、初見の人は動画から確認するのが良いでしょう。
freezedを利用すると、データクラスに必要な機能(toString
やcopyWith
メソッド)が追加され、不変オブジェクト(@immutable
)として利用できるようになります。
画面間やレイヤー間でやり取りするオブジェクトを不変オブジェクトにすることは、今時のFlutterアプリケーション開発では、ほぼ必須です。
そんなfreezedは、json_serializableをサポートしています。
この対応により、freezedで作成したクラスを「JSONのデータをパースして作成する、Dartのオブジェクトのクラス」にすることができます。
公式サンプルに従って、次の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オブジェクトを生成できるようになります。
地味に忘れがちなのが、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です。
ここにある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_rename
をnone
からsnake
に変更することで、snake_caseが自動的に変換されるようになります。当然、toJson
時にもsnake_caseのJSONが生成されるようになります。