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