【freezed】required?optional?unknownEnum?な人に丁寧に解説してみた

Dart

概要

どうも、@daiki1003です!

この記事は、Qiita Advent Calendar Flutter6日目の記事です。

中規模以上のアプリになると多分ほぼほぼ欠かせない存在になっているであろうfreezed
でも色んなannotationのパラメタがあったりrequired付けたらどうなるのか。
今回の記事はそんな疑問にお答えする形で進めていければと思っています。

もし、他にも分からないことがあればTwitterでぜひ聴いてもらえると嬉しいです!

それでは行ってみましょう!

筆者環境

freezed: 0.15.0+1
freezed_annotation: 0.15.0

※これを調べていて、1.0.0にアップデートしていないことに気がついたので
もしアップデートで何か下記内容に変更があれば更新します!

required?nullable?unknownEnum?

ある程度、使い慣れて最初にぶつかった壁というか疑問はこれでした。

・なんかとりあえずで requiredとか?とか付けてたけど実際どういう風に関係してるのか分からん。
unknownEnumdefaultValueとの関係性は?

requiredって何のために指定しているの?

少し雑な言い方になってしまうのですが、ズバリ
初期値を指定しなくても済む様にしているだけ
と思ってもらってOKだと思います。

@freezed
class Hoge {
  const factory Hoge({
    required String param,
  });

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

これはOKですが、requiredを外すと生成に失敗します。

「nullでもない、デフォルト値もない変数があってもいいだろう」とぺこぱのように許してくれたりはしません。

nullableにしたり、@Default('')などを指定することで
値が安定するのでfreezedファイルが生成可能になります。

つまり、言い換えるとnon-nullableなパラメタに対して、
jsonに含まれるからそれを使ってね、ということになります。

requiredを指定したパラメタは、json内に該当するキーが必ず必要というだよね?

答えは、 必ずしもYESではない です。
ちょっと驚いた人もいると思います。
かくいう僕も、最初はそういう意味だと思っていました。

例として、先ほどのparamnullableにしたクラスを考えてみましょう。

@freezed
class Hoge {
  const factory Hoge({
    required String? param,
  });

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

ここで、hoge.freezed.dartを覗いてみましょう。

_Hoge call({required String? param}) {
  return _Hoge(
    param: param,
  );
}

requiredの指定の有無はこのnamed parametersrequiredの有無を意味します。
つまり、 コンストラクタ経由でインスタンスを生成する際にそのパラメタの指定が必須 という意味になります。

次に、hoge.g.dartを覗いてみます。

_$_Hoge _$$_HogeFromJson(Map<String, dynamic> json) => _$_Hoge(
      param: json['param'] as String?,
);

となっており、requiredはここに一切の関与をしていません。
仮にparamキーが存在しなくとも、paramにはnullが入るだけです。

なので、requiredを指定したパラメタはjsonに存在しなくとも問題ないということになります。
(もちろん、non-nullableな場合は必要です)

さらに。

freezedから生成されるクラスは、APIレスポンスのパース時に使われる場合も多いのではないかと思います。
その際は、基本的に Hoge.fromJson(response) の様な形でパースするため、コンストラクタを直で使うことはないでしょう。

つまり、このような利用用途においてはrequiredの有無はどちらでも良いということになります。

意外とrequiredが担っている役割が大したことないなー🤔と言う感想になりませんか?

余談ですが、もしキーが必ず存在していることを保証したい場合は、@JsonKey(required: true)を指定します。

enumを定義したけど、知らない値の場合はどうすれば良いの?

さて、次にenumについて考えてみましょう。

APIレスポンスなどでenumを使った場合、サーバサイドの変更により未知の値をパースしなければいけない場合があります。
そんな時は、@JsonKey(unknownEnumValue:)を使いましょう。

enum Fuga {
 foo,
 bar,
}

があるとします。
これを含むHogeクラスを考えてみます。

@freezed
class Hoge {
  const factory Hoge({
    required Fuga fuga,
  });

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

生成すると、 hoge.g.dart

const _$FugaEnumMap = {
  Fuga.foo: 'foo',
  Fuga.bar: 'bar',
};

の様なMapが定義されます。
これを利用して何をやるかは、自明かと思いますので割愛します。

この時にjson'buz'が含まれているとどうなるでしょう?
パースエラーとなり処理が止まってしまいます。

════════ Exception caught by gesture ═══════════════════════════════════════════
Invalid argument(s): `buz` is not one of the supported values: foo, bar
════════════════════════════════════════════════════════════════════════════════

そんな時は、 @JsonKey(unknownEnumValue:)を使いましょう。

該当するenumがない時にnullにして欲しい場合は、JsonKey.nullForUndefinedEnumValueを指定します。
必要があれば、Fuga.unknownなどを追加します。

@freezed
class Hoge {
  const factory Hoge({
    @JsonKey(unknownEnumValue: Fuga.unknown) required Fuga fuga,
  });

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

これで未知の値に対しては、パースエラーは出ずFuga.unknownとして処理が進められます。

nullableにしたら知らない値があっても大丈夫?

答えはNOです。

@freezed
class Hoge {
  const factory Hoge({
    required Fuga? fuga,
  });

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

hoge.g.dartを見てみましょう。

_$_Hoge _$$_HogeFromJson(Map json) => _$_Hoge(
 fuga: $enumDecodeNullable(_$FugaEnumMap, json['fuga']),
);

と、$enumDecodeNullableを使ってパースされていることが確認できます。
そこで、$enumDecodeNullableの中身を見てみると

K? $enumDecodeNullable<K extends Enum, V>(
  Map<K, V> enumValues,
  Object? source, {
  Enum? unknownValue,
}) {
  if (source == null) {
    return null;
  }

  for (var entry in enumValues.entries) {
    if (entry.value == source) {
      return entry.key;
    }
  }

  if (unknownValue == JsonKey.nullForUndefinedEnumValue) {
    return null;
  }

  if (unknownValue == null) {
    throw ArgumentError(
      '`$source` is not one of the supported values: '
      '${enumValues.values.join(', ')}',
    );
  }

  if (unknownValue is! K) {
    throw ArgumentError.value(
      unknownValue,
      'unknownValue',
      'Must by of type `$K` or `JsonKey.nullForUndefinedEnumValue`.',
    );
  }

  return unknownValue;
}

となっており、キーが見つからなければunknownValue == nullのif文の中の処理によりエラーが投げられてしまうためパースエラーとなってしまいます。

じゃぁ、defaultValueがあったら知らない値でも大丈夫?

これも、残念ながら答えはNOです。

@freezed
class Hoge {
  const factory Hoge({
    @JsonKey(defaultValue: Fuga.foo) required Fuga fuga,
  });

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

hoge.g.dartを見てみましょう。

_$_Hoge _$$_HogeFromJson(Map json) => _$_Hoge(
 fuga: $enumDecodeNullable(_$FugaEnumMap, json['fuga']) ?? Fuga.foo,
);

となっており、先ほどの$enumDecodeNullableがnullを返した時の値として使われていることがわかると思います。

ですが、先ほども見たように知らない値の場合はArgumentErrorが投げられてしまうのでこれも望んだ挙動にはならないことになります。

いかがでしたでしょうか?
本当であれば、 ポリモーフィズムをfreezedで実現するというところまで書きたかったのですが
想定外に長くなってしまったのでまたの機会としたいと思います。

誰かのお役に立てば。

Twitterフォローお願いします

「次回以降も記事を読んでみたい!」
「この辺分からなかったから質問したい!」

そんな時は、是非@daiki1003のフォローお願いします♪

また、記事がとても役に立ったと思う人は
コーヒーを奢っていただけると非常に嬉しいです!

コメント

タイトルとURLをコピーしました