Freezed Union の fromJson に関するメモ
freezedのUnion Keyで指定されたカラムに対して、 json_serializableの FieldRename を併用しているときに、 fromJson で少しハマったのでメモ。
前提
以下のようなデータがAPIからJSONとして返ってくるとする。
[
{
"animal_type": "dog",
...
},
{
"animal_type": "cat",
...
}
]
TypeScriptで表現すると、以下の Response 型が返ってくるイメージ。
type Response = Animal[];
type Animal = Dog | Cat;
type Dog = {
animal_type: 'dog';
...
};
type Cat = {
animal_type: 'cat';
...
};
このとき、これらのデータをFreezedのUnionを用いた以下の Animal クラスに変換したい。
JSON側のキーは、snake_caseなので、 @JsonSerializable(fieldRename: FieldRename.snake) を用いる。
FreezedのUnion keyについては以下を参照。
import 'package:freezed_annotation/freezed_annotation.dart';
part 'animal.freezed.dart';
part 'animal.g.dart';
enum AnimalType {
dog,
cat,
;
}
(unionKey: 'animalType')
sealed class Animal with _$Animal {
const Animal._();
('dog')
(fieldRename: FieldRename.snake)
factory Animal.dog({
(AnimalType.dog) AnimalType animalType,
}) = Dog;
('cat')
(fieldRename: FieldRename.snake)
factory Animal.cat({
(AnimalType.cat) AnimalType animalType,
}) = Cat;
factory Animal.fromJson(Map<String, dynamic> json) => _$AnimalFromJson(json);
}
問題
上の状態で、 build_runnerを走らせてビルドすると、ビルドは成功する。
一方で、実際に Animal クラスを用いて、以下のようにJSONから変換しようとすると、ランタイムでエラーが発生する。
import 'dart:convert';
import 'package:sample/animal.dart';
void main() {
final json = jsonDecode('[{"animal_type":"dog"},{"animal_type":"cat"}]')
as List<dynamic>;
final response =
json.cast<Map<String, dynamic>>().map(Animal.fromJson).toList();
print(response);
}
Unhandled exception:
CheckedFromJsonException
Could not create `Animal`.
There is a problem with "animalType".
Invalid union type "null"!
つまりJSON側に animalType がないというエラーが出ている。
原因
実際に生成されたファイルを見ると原因はわかる。
Animal.fromJson は実際には、 animal.freezed.dart の以下の関数が呼ばれているので見てみると、 json['animalType'] で分岐していることがわかる。
...
Animal _$AnimalFromJson(Map<String, dynamic> json) {
switch (json['animalType']) {
case 'dog':
return Dog.fromJson(json);
case 'cat':
return Cat.fromJson(json);
default:
throw CheckedFromJsonException(json, 'animalType', 'Animal',
'Invalid union type "${json['animalType']}"!');
}
}
...
FieldRename.snake をアノテーションに指定したため、以下のように生成されてくれればよかったが、@JsonSerializable に指定しただけのため、この設定が freezed 側の生成ファイル (.freezed.dart) に影響を与えることはできない。
...
Animal _$AnimalFromJson(Map<String, dynamic> json) {
- switch (json['animalType']) {
+ switch (json['animal_type']) {
...
解決策
自動生成の fromJson が期待したものではないだけなので、 解決策としては fromJson を自前実装すれば良い。
最終的には以下のコードで動くようになる。
import 'package:freezed_annotation/freezed_annotation.dart';
part 'animal.freezed.dart';
part 'animal.g.dart';
enum AnimalType {
dog,
cat,
;
}
- (unionKey: 'animalType')
+ (unionKey: 'animalType', fromJson: true)
sealed class Animal with _$Animal {
const Animal._();
('dog')
(fieldRename: FieldRename.snake)
factory Animal.dog({
(AnimalType.dog) AnimalType animalType,
}) = Dog;
('cat')
(fieldRename: FieldRename.snake)
factory Animal.cat({
(AnimalType.cat) AnimalType animalType,
}) = Cat;
- factory Animal.fromJson(Map<String, dynamic> json) => _$AnimalFromJson(json);
+ factory Animal.fromJson(Map<String, dynamic> json) {
+ switch (json['animal_type']) {
+ case 'dog':
+ return Dog.fromJson(json);
+ case 'cat':
+ return Cat.fromJson(json);
+
+ default:
+ throw CheckedFromJsonException(
+ json,
+ 'animal_type',
+ 'Animal',
+ 'Invalid union type "${json['animal_type']}"!',
+ );
+ }
+ }
}
1点ポイントとして、 @Freezed アノテーションに fromJson: true を追加している。
factory Animal.fromJson(Map<String, dynamic> json) を自前実装したことにより、 _$AnimalFromJson(json) を削除することになる。
json_serializable側の fromJson メソッド (Dog.fromJson, Cat.fromJson) が自動的には生成されないようになってしまう。 fromJson: true を指定することで強制的に fromJson メソッドを生成するようにしている。
fromJson の自前実装部分は、元々 freezed 側で生成されていたものをほとんどそのままコピーしただけ。
懸念点
自前実装だと少し保守性が下がる。
例えば以下のような変更があった際には、 fromJson メソッドを手動で修正する必要がある。
-
unionKeyが変更された場合 -
AnimalTypeに要素が追加された場合 - (あまりなさそう)
FieldRenameの設定変更された場合
最後に
他に良い方法をご存じの方はぜひ教えてください!
Discussion