[読書会]freezed 2.5.7 翻訳及び補足(How to use / FromJson/ToJson)

freezedは、データクラス、ユニオン(合併型)、およびクローニング用のコードジェネレーターです。
これはDartプログラミング言語におけるモデル定義を簡単にするために設計されています。

Note(注釈)
現在、Freezedのマクロを使用した初期プレビュー版が利用可能です。
詳細については、GitHubのリンク https://github.com/rrousselGit/freezed/tree/macros を参照してください。

本ブログの(上記https://pub.dev/packages/freezedの)翻訳および解説の目次頁(構成上のトップページ)は次のURLになります。

また前回記事は、こちらになります。

本頁は、上記(目次)のうちの、「How to use / FromJson/ToJson」の章の翻訳および解説頁になります。

2.4 FromJson/ToJson

(1)fromJSON – classes with multiple constructors( 複数のコンストラクタを持つクラス )

Freezed は通常の fromJson/toJson をそれ自体で生成しませんが、json_serializable が何であるかを認識します。 クラスに json_serializable との互換性を持たせるのは非常に簡単です。 次のスニペットを考えてみましょう。

(json_serializable との互換性を持たせる前のコード)
import 'package:freezed_annotation/freezed_annotation.dart';

part 'model.freezed.dart';

@freezed
sealed class Model with _$Model {
  factory Model.first(String a) = First;
  factory Model.second(int b, bool c) = Second;
}

json_serializable との互換性を持たせるために必要な変更は、次の 2 行で構成されます。

  • ‘model.g.dart’;
  • Factory Model.fromJson(Map<String, Dynamic> json) => _$ModelFromJson(json);

以下は変更後のコードです。

(json_serializable との互換性を持たせたコード)
import 'package:freezed_annotation/freezed_annotation.dart';

part 'model.freezed.dart';
part 'model.g.dart';

@freezed
sealed class Model with _$Model {
  factory Model.first(String a) = First;
  factory Model.second(int b, bool c) = Second;

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

json_serializable を pubspec.yaml ファイルに忘れずに追加してください。

(pubspec.yaml)json_serializable パッケージ依存関係登録:
dev_dependencies:
  json_serializable:

それでおしまい!
これらの変更により、Freezed は自動的に json_serializable に必要なすべての fromJson/toJson を生成するように要求します。

NOTE)

Freezed は、ファクトリ => を使用している場合にのみ fromJson を生成します。

fromJSON – classes with multiple constructors 

複数のコンストラクタを持つクラスの場合、Freezed JSON 応答で runtimeType という文字列要素を確認し、その値に基づいて使用するコンストラクタを選択します。たとえば、次のコンストラクタがあるとします。

(複数のコンストラクタを持つクラスの場合)識別子(MyResponse() / .special / .error )でコンストラクタを選択する。:
@freezed
sealed class MyResponse with _$MyResponse {
  const factory MyResponse(String a) = MyResponseData;
  const factory MyResponse.special(String a, int b) = MyResponseSpecial;
  const factory MyResponse.error(String message) = MyResponseError;

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

次に、Freezed は各 JSON オブジェクトruntimeType を使用して、次のようにコンストラクタを選択します。

(JSON オブジェクト) runtimeType に設定されている識別子で、コンストラクタを選択します。:
[
  {
    "runtimeType": "default",
    "a": "This JSON object will use constructor MyResponse()"
  },
  {
    "runtimeType": "special",
    "a": "This JSON object will use constructor MyResponse.special()",
    "b": 42
  },
  {
    "runtimeType": "error",
    "message": "This JSON object will use constructor MyResponse.error()"
  }
]

@Freezed および @FreezedUnionValue デコレータを使用して、キー別のものカスタマイズできます。

(識別子目印(unionKey)、キー値のケース(unionValueCase)のカスタマイズ)runtimeType → type、FreezedUnionCase.pascal に変更できる。:
@Freezed(unionKey: 'type', unionValueCase: FreezedUnionCase.pascal)
sealed class MyResponse with _$MyResponse {
  const factory MyResponse(String a) = MyResponseData;

  @FreezedUnionValue('SpecialCase')
  const factory MyResponse.special(String a, int b) = MyResponseSpecial;

  const factory MyResponse.error(String message) = MyResponseError;

  // ...
}
//FreezedUnionCase.pascal を設定することで、Freezed パッケージがどのように JSON データの unionKey で指定されたフィールドの値を解釈し、どのコンストラクタにマッピングするかの規則を定義します。具体的には、unionValueCase: FreezedUnionCase.pascal を使用することで、JSON のキー値のケース(大文字小文字の使用方法)を PascalCase(各単語の最初の文字が大文字で始まり、その他の文字が小文字)に一致させることができるようになります。

これにより、前の json が次のように更新されます。

(JSONオブジェクト)(識別子目印(unionKey)、キー値のケース(unionValueCase)のカスタマイズ)runtimeType → type、FreezedUnionCase.pascal に変更した後のJSONオブジェクト:
[
  {
    "type": "Default",
    "a": "This JSON object will use constructor MyResponse()"
  },
  {
    "type": "SpecialCase",
    "a": "This JSON object will use constructor MyResponse.special()",
    "b": 42
  },
  {
    "type": "Error",
    "message": "This JSON object will use constructor MyResponse.error()"
  }
]

すべてのクラスキーカスタマイズする場合は、 build.yaml ファイル 内で指定できます。次に例を示します。

( build.yaml )すべてのクラスのキーと値をカスタマイズできる。:
targets:
  $default:
    builders:
      freezed:
        options:
          union_key: type
          union_value_case: pascal

JSON 応答を制御できない場合があります。

  1. APIが返す応答の形式が不規則または変動的:
     例えば、外部のAPIや異なるエンドポイント間で統一された応答形式がない場合、応答の形式やデータ構造がコントロール外で変わる可能性があります。このような場合、標準的なJSONデシリアライゼーション手法だけでは、すべての可能性をカバーできないことがあります。
  2. APIからの応答に一貫性が欠ける場合:
     APIが異なる状況に応じて異なるデータ構造を返す場合(例:成功応答がオブジェクトを返すが、エラー応答が異なる構造を持つ場合など)、これを標準のデシリアライゼーションプロセスで適切に処理するのが難しくなります。
  3. 特定のフィールドに基づいて適切なモデルを選択する必要がある場合:
     応答JSONに含まれる特定のフィールドの値に基づいて、どのデータモデルを使用して解析するかを動的に決定する必要がある場合です。これには、APIからのデータに対して複雑なロジックを適用する必要があります。

上記の様な場合は、カスタム コンバータを実装できます。
カスタム コンバータは、使用するコンストラクタを決定するための独自のロジックを実装する必要があります。

カスタム コンバータの利用

このような状況では、カスタム コンバータを実装して、応答を制御し、適切なコンストラクタやデータモデルへのマッピングをカスタマイズすることが推奨されます。カスタム コンバータは、以下の機能を提供することができます。

  1. 動的なコンストラクタの選択:
     応答の内容を解析し、適切なコンストラクタを選択するロジックを実装します。これにより、異なる形式のデータを同じ型のオブジェクトに変換することができます。
  2. 条件付き処理の実装:
     JSONオブジェクト内の特定のフィールドをチェックし、その存在または値に基づいて異なる処理を行うことができます。これは、APIからの応答が一貫性を欠いている場合に特に有効です。
  3. エラーハンドリングの強化:
     JSONデータからオブジェクトへの変換中に問題が発生した場合に、より具体的なエラーメッセージを提供することができます。これにより、デバッグが容易になり、アプリケーションのロバスト性が向上します。

このように、カスタム コンバータは、標準的なシリアライズ手法では対応が難しい複雑なシナリオや特定の要件に対応するための強力なツールです。Flutter開発において、API応答の適切な処理を確実に行うために、このようなカスタマイズが推奨されることが多いです。

(Jsonカスタムコンバータ)複雑なJSON構造や特定のシナリオでのデータ処理が求められる場合に非常に役立ちます。Flutter開発では、API応答を適切にハンドリングし、アプリケーション内で正しく使用できるようにするためにこのようなカスタムコンバータを設計することが推奨されます。
//JsonConverter インターフェースを実装しており、MyResponse タイプのオブジェクトを JSON マップとしてシリアライズおよびデシリアライズする方法を定義しています。
class MyResponseConverter implements JsonConverter<MyResponse, Map<String, dynamic>> {
  const MyResponseConverter();

  //このコンバータは、runtimeType フィールドをチェックして適切な MyResponse のコンストラクタを選択します。これにより、複数のサブタイプを持つクラスであっても、正確に適切な型にデータを変換できます。
  @override
  MyResponse fromJson(Map<String, dynamic> json) {
    //既に runtimeType が設定されている場合、標準の JSON デシリアライザを使用
    if (json['runtimeType'] != null) {
      return MyResponse.fromJson(json);
    }
    //特定のフィールドを調べて適切なコンストラクタを選択
    if (isTypeData) {
      return MyResponseData.fromJson(json);
    } else if (isTypeSpecial) {
      return MyResponseSpecial.fromJson(json);
    } else if (isTypeError) {
      return MyResponseError.fromJson(json);
    } else {
      throw Exception('Could not determine the constructor for mapping from JSON');
    }
 }

  @override
  Map<String, dynamic> toJson(MyResponse data) => data.toJson();
}

//カスタムコンバータは、複雑なJSON構造や特定のシナリオでのデータ処理が求められる場合に非常に役立ちます。Flutter開発では、API応答を適切にハンドリングし、アプリケーション内で正しく使用できるようにするためにこのようなカスタムコンバータを設計することが推奨されます。

次に、カスタム コンバータ適用するには、デコレータ()をコンストラクタ パラメータに渡します。

(カスタム コンバータの適用)デコレータ(@MyResponseConverter()使用)をコンストラクタ パラメータに渡します。:
@freezed
class MyModel with _$MyModel {
  const factory MyModel(@MyResponseConverter() MyResponse myResponse) = MyModelData;

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

こうすることで、json Serializable MyResponseConverter.fromJson() と MyResponseConverter.toJson() を使用して MyResponse変換します。 リストに含まれるコンストラクタ パラメータに対してカスタム コンバータを使用することもできます。

リストに含まれるコンストラクタ パラメータに対してカスタム コンバータを使用することもできます。
@freezed
class MyModel with _$MyModel {
  const factory MyModel(@MyResponseConverter() List<MyResponse> myResponse) = MyModelData;

  factory MyModel.fromJson(Map<String, dynamic> json) => _$MyModelFromJson(json);
}
NOTE) フリーズしたオブジェクトのネストしたリストをシリアライズする手順

@Freezedしたオブジェクトのネストしたリストシリアライズするには、
・ @JsonSerializable(explicitToJson: true)を指定するか、
・ build.yamlファイル内でexplicit_to_jsonを変更する
必要があります(ドキュメントを参照( 以下 ))。

ビルド構成:
関連するアノテーション・クラスに引数を設定する以外に、build.yamlに値を設定することでコード生成を設定することもできる。

Dart
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_json_keys: false
          create_per_field_to_json: false
          create_to_json: true
          disallow_unrecognized_keys: false
          explicit_to_json: false
          field_rename: none
          generic_argument_factories: false
          ignore_unannotated: false
          include_if_null: true

(2) Deserializing generic classes

ジェネリッククラスの逆シリアル

ジェネリック型のフリーズされたオブジェクト逆シリアル化するには、genericArgumentFactoriesにします。
必要なのは、
・ fromJson メソッドシグネチャ(以下の例では、T Function(object?) fromJsonT)を変更
・ フリーズされた構成に genericArgumentFactories: true を追加
することだけです。

(1)<T>型クラス(@Freezed使用(genericArgumentFactories: true あり))オブジェクトを逆シリアル化したい。:
@Freezed(genericArgumentFactories: true)
sealed class ApiResponse<T> with _$ApiResponse<T> {
  const factory ApiResponse.data(T data) = ApiResponseData;
  const factory ApiResponse.error(String message) = ApiResponseError;

  factory ApiResponse.fromJson(
   Map<String, dynamic> json, 
   T Function(Object?) fromJsonT //←追加後、以降の(2)~(5)作業が必要:
  ) 
  => _$ApiResponseFromJson(json, fromJsonT);
}

補足) T Function(Object?) fromJsonT とは

fromJsonT 関数は、JSONからデータをデコードしてジェネリック型 T のオブジェクトに変換するための関数です。この関数は ApiResponse.fromJson のようなジェネリックを含む fromJson メソッドにおいて、具体的なデータ型のデコード方法を提供します。これにより、型 T が何であれ、適切なデシリアライズ処理を行うことが可能になります。

具体的な fromJsonT 関数の例
仮に ApiResponse の T が User クラスのインスタンスである場合、User クラスは次のようになるかもしれません:

(2)(具体的な fromJsonT 関数例) 仮に ApiResponse の T が User クラスのインスタンスである場合、User クラスは次のようになるかもしれません。(User.fromJson() 作成):
class User {
  final String name;
  final int age;

  User({required this.name, required this.age});

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      name: json['name'] as String,
      age: json['age'] as int,
    );
  }
}

この場合、fromJsonT は User.fromJson メソッドへの参照として渡すことができます。したがって、ApiResponse をデシリアライズする際には、次のようになります。

(3)ApiResponse<User>のインスタンスを生成し、User.fromJson()メソッドに、jsonDataを処理させる。:
ApiResponse<User> response = ApiResponse<User>.fromJson(
  //(2)で作成したUser.fromJsonにjsoDataを渡して処理
  //(デシリアライズ(パース)させる。
 jsonData, User.fromJson 
);

ここで、jsonData は ApiResponse を表すJSONオブジェクトです。

上記(1)~(3)を実装後、実際に
(以下の(4)で)jsonData のフォーマットを定義して、
(以下の(2)(3)で)デシリアライズ(パース)されたデータ(ApiResponse response)の内容により、
(以下の(5)で)ロジック(識別、振分処理等)を組んでいます。

(処理全体(まとめ(main.dart))):上記(1)~(3)を実装後、実際に (以下の(4)で)jsonData のフォーマットを定義して、(以下の(2)(3)で)デシリアライズ(パース)されたデータ(ApiResponse response)の内容により、(以下の(5)で)ロジック(識別、振分処理等)を組んでいます。:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'api_response.freezed.dart';
part 'api_response.g.dart';

//(1)Freezedを使用してジェネリック型のApiResponseクラスを定義します。
//genericArgumentFactories: trueは、ジェネリック型Tのデシリアライズ処理をカスタマイズ可能にします。
@freezed
class ApiResponse<T> with _$ApiResponse<T> {
  //データを格納するコンストラクタです。T型のデータを引数に取ります。
  const factory ApiResponse.data(T data) = ApiResponseData;
  //エラー情報を格納するコンストラクタです。エラーメッセージを文字列で受け取ります。
  const factory ApiResponse.error(String message) = ApiResponseError;
  //fromJson()はJSONデータとT型データを生成する為の関数fromJsonTを引数に取る。
  //このメソッドは、JSONデータをApiResponse<T>型に変換します。
  factory ApiResponse.fromJson(
   Map<String, dynamic> json, 
   T Function(Object?) fromJsonT
  ) 
  => _$ApiResponseFromJson(json, fromJsonT);
}

//(2)Userクラスはデータのモデルを定義します。
//このクラスは名前と年齢をプロパティとして持ちます。
class User {
  final String name;
  final int age;

  //コンストラクタでプロパティを初期化します。
  User({required this.name, required this.age});

  //fromJsonファクトリメソッドは、JSONマップを受け取り、新しいUserインスタンスを生成します。
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      name: json['name'] as String,
      age: json['age'] as int,
    );
  }
}

//(4)以下は、ApiResponse<User>を使用する実際の例です。
//jsonDataは、外部から取得したJSONデータを表します。
//User.fromJsonは、User型のデータをデシリアライズするための関数です。
void main() {
  Map<String, dynamic> jsonData = {
    "type": "data",
    "data": {"name": "John Doe", "age": 30}
  };

  //(3)ApiResponse<User>のインスタンスを生成し、fromJson()を使用してjsonDataをデシリアライズ(パース)します。
  ApiResponse<User> response
   = ApiResponse<User>.fromJson(jsonData, User.fromJson);

  //(5)データが正常にロードされたかどうかを確認し、内容を表示します。
  if (response is ApiResponseData<User>) {
    print('User Name: ${response.data.name}');
    print('User Age: ${response.data.age}');
  } else if (response is ApiResponseError) {
    print('Error: ${response.message}');
  }
}

ジェネリックな場合複数の異なる型を取り得る場合

さらに一般的な使用例として、もし T が複数の異なる型を取り得る場合fromJsonT 関数はランタイムに応じて異なるデシリアライズロジックを適用するために使われます。
例えば、T が User または Product のどちらかである場合、次のようなコンディショナルロジックを含むことができます。

(ジェネリックな場合(複数の異なる型を取り得る場合))<T>により分岐処理できる。:
T Function(Object? json) getFromJsonT<T>() {
  if (T == User) {
    return User.fromJson as T Function(Object?);
  } else if (T == Product) {
    return Product.fromJson as T Function(Object?);
  } else {
    throw Exception('Unsupported type');
  }
}

この関数を使用して、適切な型のオブジェクトにデシリアライズ(パース)するための関数を取得し、それを ApiResponse.fromJson メソッドに渡すことができます。

注意点
fromJsonT 関数を提供する際には、適切な型安全を保つためにキャストを適切に行うことが重要です。また、すべての可能な型に対して fromJson メソッドが存在することを確認し、それらが適切に公開されていることも必要です。

あるいは、build.yaml ファイルを変更して以下を含めることにより、プロジェクト全体 genericArgumentFactories有効にすることもできます。

(build.yaml)プロジェクト全体で genericArgumentFactories を有効にする。:
targets:
  $default:
    builders:
      freezed:
        options:
          generic_argument_factories: true

@JsonKey アノテーションについてはどうですか?

単体の項目(プロパティ)毎に( JSONデータから個別に )取得したい場合

コンストラクタ パラメータ に渡されるすべてのデコレータは、生成されたプロパティにも「コピー&ペースト」されます。
したがって、次のように書くことができます。

(1)(単体の項目(プロパティ)毎に( JSONデータから個別に )取得したい場合)@JsonKey を使用する。:
@freezed
class Example with _$Example {

  factory Example(@JsonKey(name: 'my_property') String myProperty) = _Example;

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

すべてのクラスに対していくつかのカスタム json_serializable フラグ (explicit_to_json any_map など) を定義したい場合は、Build configurationで説明されているように build.yaml ファイルを介して実行できます。 デコレータセクションも参照してください

補足) 単体の項目(プロパティ)毎にJSONデータから個別に )取得したい場合

  1. 必要なクラスとライブラリを準備
    まず、Example クラスと必要な freezed アノテーションを用意します。これには、前に示した Example クラス定義を使用します。また、json_serializable を使うので、適切なライブラリをインポートすることが必要です。
(1)(同上(単体の項目(プロパティ)毎に( JSONデータから個別に )取得したい場合))@JsonKey を使用する。:
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:json_annotation/json_annotation.dart';

part 'example.freezed.dart';
part 'example.g.dart';

@freezed
class Example with _$Example {

  const factory Example(@JsonKey(name: 'my_property') String myProperty) = _Example;
  
  factory Example.fromJson(Map<String, dynamic> json) => _$ExampleFromJson(json);
}
  1. main() 関数の実装
    次に、main() 関数内で JSON データを定義し、Example.fromJson() メソッドを使ってデータをパースし、コンソールに出力するコードを書きます。
Dart
void main() {
  //JSON データのサンプル
  Map<String, dynamic> jsonData = {
    'my_property': 'sample value'
  };

  //JSON データを Example オブジェクトにデシリアライズ
  Example example = Example.fromJson(jsonData);

  //デシリアライズされたデータの myProperty を出力
  print('The value of myProperty is: ${example.myProperty}');
}

この main() 関数では以下のステップを行います:

jsonData に JSON 形式のデータを定義します。ここで、キー名 my_property に対応する値 sample value を設定しています。
Example.fromJson() を呼び出して、jsonData を Example オブジェクトにデシリアライズします。
最後に、デシリアライズされた Example オブジェクトの myProperty プロパティの値をコンソールに出力します。

FAQ

Q1)

上記で、取り扱う、JSONデータには、my_property というフィールドが、あることを、前提としていますか?

A1)

はい、その通りです。上記のコード例で使用されている @JsonKey(name: ‘my_property’) アノテーションは、JSONデータの中に my_property というキー名でフィールドが存在することを前提としています。このアノテーションを使うことで、my_property キーに対応する値を、Example クラスの myProperty プロパティにマッピングするように指示しています。

JSONデータとDartオブジェクトのマッピング
具体的には、以下のようなJSONデータが与えられた場合:

Dart
{
  "my_property": "example value"
}

このJSONデータは、Example クラスのインスタンスにデシリアライズされる際に、my_property キーの値 “example value” が Example クラスの myProperty プロパティにセットされます。このプロセスは自動生成された _$ExampleFromJson 関数によって行われます。

生成されたデシリアライズ関数の役割
生成された _$ExampleFromJson 関数は、指定されたJSONオブジェクトからデータを読み取り、新しい Example インスタンスを生成する際に、このデータを適切なプロパティに割り当てます。この関数は通常、以下のような形式で生成されます:

Dart
Example _$ExampleFromJson(Map<String, dynamic> json) {
  return Example(
    myProperty: json['my_property'] as String,
  );
}

このコードは、JSONオブジェクトから my_property キーに対応する値を取り出し、Example クラスのコンストラクタに渡すことで myProperty プロパティに割り当てます。

注意点
@JsonKey アノテーションを使用するときは、JSONデータに対象のキーが必ず存在することを確認するか、存在しない場合のデフォルト値やエラーハンドリングを適切に設定することが重要です。これにより、ランタイムエラーを防ぎ、より堅牢なアプリケーションを開発できます。

注意点
このコードを実行する前に、適切に build_runner を使ってコード生成を行い、必要な部分(example.freezed.dart および example.g.dart)が生成されていることを確認する必要があります。また、Dart のプロジェクトであれば pubspec.yaml に依存関係を追加しておく必要があります。

(続きの記事はこちら)

コメントを残す