【Flutter】freezed, json_annotation の基本的な使い方とTIPS

エキサイトの武藤です。

Flutter 開発を通して、freezed でのJSONの扱いに慣れてきました。

今回は freezed, json_annotation パッケージの基本的な使い方やTIPSをまとめていきます。

@JsonKey アノテーション

json_annotation パッケージの @JsonKey アノテーションを使用して、JSONのキーとfreezedのオブジェクトのプロパティーマッピングする挙動を設定できます。

name プロパティー

name プロパティーでは、指定した文字列でJSONキーからプロパティーマッピングできます。

下記のコードでは、author_idauthorId , author_nameauthorName へのマッピングを記述しています。

@freezed
class Book with _$Book {

  factory Book({
    required String id,
    required String title,
    required String story,
    @JsonKey(name: 'author_id') required String authorId,
    @JsonKey(name: 'author_name') required String authorName,
  }) = _Book;

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

変換するフィールドが膨大な場合は、 @JsonSerializable を使って一括で指定する方法もあります。

FieldRename.snake を指定して、スネークケースからマッピングできるようになります。

@freezed
class Book with _$Book {
  @JsonSerializable(fieldRename: FieldRename.snake)
  factory Book({
    required String id,
    required String title,
    required String story,
    required String authorId,
    required String authorName,
  }) = _Book;

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

また、JSONのキー名を別名でプロパティーに設定したい場合にも使えます。 下記は、 contents から story命名を変更しています。

@freezed
class Book with _$Book {

  factory Book({
    required String id,
    required String title,
    @JsonKey(name: 'contents') required String story,
  }) = _Book;

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

defalutValue プロパティー

defalutValue プロパティーでは、初期値を設定できます。

APIレスポンスがnullで返ってきて、アプリケーションではnon nulllで扱いたいときに初期値を設定できます。

@freezed
class Book with _$Book {
  factory Book({
    required String id,
    required String title,
    @JsonKey(defaultValue: '') required String summary,
  }) = _Book;

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

fromJson プロパティー

fromJson プロパティーでは、マッピングする際に実行したい関数を設定できます。関数はトップレベルで定義する必要があります。

具体的なケースとしては、String型のレスポンスをint型に変換したい場合が考えられます。

まず、下記のような変換メソッドを作ります。

int? stringToInt(String stringValue) {
  return int.tryParse(stringValue);
}

freezedクラスでは、@JsonKey アノテーションfromJson プロパティーstringToInt() メソッドを指定します。

@freezed
class Book with _$Book {
  factory Book({
    required String id,
    required String title,
    required String story,
    @JsonKey(fromJson: stringToInt) required int? price,
  }) = _Book;

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

APIレスポンスの型が不定でアプリ側で吸収する必要がある場合は、下記のようにdynamic型で受け取るメソッドを用意します。

int? toInt(dynamic jsonValue) {
  if ((jsonValue) is String) {
    return int.tryParse(jsonValue);
  }
  if ((jsonValue) is int) {
    return (jsonValue);
  }
  return null;
}

カスタムゲッター、メソッドを追加する

freezedクラスにカスタムゲッターを設定します。

空のプライベートコンストラクタの追記がポイントになります。

@freezed
class Book with _$Book {
  const Book._();

  factory Book({
    required String id,
    required String title,
    required String story,
    @JsonKey(name: 'author_id') required String authorId,
    @JsonKey(name: 'author_name') required String authorName,
  }) = _Book;

  factory Book.fromJson(Map<String, dynamic> json) =>
      _$BookFromJson(json);
  
  String get summary => '${story.substring(0, 100)}...';
}

下記のように summary にアクセスできるようになります。

final summary = book.summary;

build_runner watch でコード変更を監視

build_runner watch を使って、変更を監視します。

flutter pub run build_runner watch --delete-conflicting-outputs

コードを修正して保存するタイミングでビルドされます。 体験的には build_runner build で実行するよりも、build_runner watch がスムーズです。

まとめ

基本的な内容ですが、freezed, json_annotation パッケージを使ったJSONから freezed オブジェクトへのマッピングについて説明しました。

アノテーションを利用して、変数名や型等の変換が細かく設定できます。

こちらの記事が参考になれば幸いです。

余談

先日行われたFlutterKaigi2022 にて、Dartのアップデートに関するセッションがありました。今後はDartのマクロ機能を使うことで、Flutterのコンパイルのタイミングでコードの自動生成が走るようになり、開発体験の向上が期待できそうです。アップデートが楽しみですね。

参考記事

各種パッケージについて

pub.dev

pub.dev

pub.dev

Dartのアップデートの内容について

speakerdeck.com

youtu.be