開発

Freezed(Flutter)とは?データクラスとUnion型の生成と依存の固定【2026年8月時点】

Freezedは、Dartのイミュータブルなデータクラスとunion型を注釈1つから生成するコード生成パッケージです。2026年8月時点の安定版は3.2.5(2026年2月3日公開)で、Dart SDKは3.8.0以上を求めます。この記事では生成される実装の中身、3.0.0で必須になった宣言構文、json_serializableとの連携、build_runnerの運用、そして安定版がbuild_runnerの版を止めてしまう依存の重なりまでを一次情報から整理します。

まとめ:定型コードの量で入れるか決め、依存の固定範囲まで見て判断する

結論から言えば、Freezedは「同じ形の手書きコードが増えてきたプロジェクト」で効果を発揮するツールです。データクラス1個につき、コンストラクタ・copyWith==hashCodetoStringを手で書くと30行前後になり、フィールドを1つ足すたびに全箇所を直す作業が発生します。モデルが10個を超えるあたりから、この手作業の総量が生成ツールの導入コストを上回ります。

逆に、モデルが数個しかなく、JSONの構造も浅いプロジェクトでは入れないほうが軽く収まります。build_runnerを他に使っていない場合、Freezedのためだけにビルド工程が1段増えるからです。DartのrecordsとsealedクラスがDart 3で言語機能になったため、小さな組や単純な分岐は生成なしでも表現できます。

そして2026年8月時点では、依存の固定範囲まで見ておく必要があります。安定版3.2.5はanalyzerを10系までしか許容せず、analyzer 13.3.0以上を求めるbuild_runner 2.16.0とは同居できません。導入すると、build_runnerは2.15.1(2026年7月8日公開)以前へ止まります。この点を知らずに入れると、後から「build_runnerだけ上げたい」という要求に応えられなくなります。

Freezedがコード生成で肩代わりする範囲と3.0.0以降の書き方

Freezedは実行時に働くライブラリではなく、ビルド前にDartのソースを読んで同じディレクトリへ実装ファイルを書き出す仕組みです。この前提が、後述するpart指令やGit除外の判断をそのまま決めます。

@freezedを付けたクラスから生成される4種類の実装コード

宣言側に書くのはフィールドの並びだけで、そこから生成されるのは次の4種類です。

  • フィールドを保持する具象クラス(_Personのような、宣言で右辺に置いた名前のクラス)
  • 一部のフィールドだけ差し替えた複製を返すcopyWith
  • 全フィールドを比較する==と、それに対応するhashCode
  • フィールド名と値を並べたtoString

宣言したクラス自体は、生成された_$Personというmixinを取り込む器になります。union型を書いた場合は、ここに分岐ごとのサブクラスが加わります。JSONの変換だけは別のパッケージが担当するため、Freezed単体ではfromJsontoJsonも出てきません。

part指令とwith _$が3.0.0以降で必須になった理由

3.0.0(2025年2月25日公開)の破壊的変更として、公式のCHANGELOGは「Freezedのクラスはabstractsealedであるか、手動で_$MyClassをimplementsする必要がある」と記載しました。2.x系では通常のクラス宣言でも通っていたため、上げるときはクラス宣言の書き換えが要ります。

書き方は、単一の形ならabstract class Person with _$Person、分岐を持つならsealed class Result with _$Resultの2択です。宣言側は署名だけを持ち、実体は生成ファイルへ置く形に整理されました。

freezed.dartと.g.dartの2ファイルに分かれる仕組み

生成物は用途で分かれます。part 'person.freezed.dart';がFreezed本体の出力先で、part 'person.g.dart';がjson_serializableの出力先です。2つのパッケージが別々にファイルを書き、宣言ファイルからpartで束ねる構造になっています。

詰まりやすいのは、JSONを扱わないクラスに.g.dartのpart指令を書いてしまう場合です。生成されないファイルを参照するのでビルドが落ちます。fromJsonを書かないクラスでは、part指令は1行だけに留めてください。

json_serializableと組み合わせてJSON変換まで生成する手順

APIレスポンスをモデルへ落とす用途では、json_serializableとの2本立てが前提になります。導入コマンドと最小構成を先に固めると、後の追加は機械的に済みます。

導入コマンドとdependenciesの振り分けで迷いやすい点

注釈クラスを参照するfreezed_annotationjson_annotationは通常のdependenciesへ、生成を実行するfreezedbuild_runnerjson_serializableはdev_dependenciesへ入れます。公式READMEもdev:接頭辞で両者を分ける形を示しました。

flutter pub add freezed_annotation json_annotation
flutter pub add dev:freezed dev:build_runner dev:json_serializable

SDKの導入がまだなら、先にFlutterの環境構築|Windows・macOS別のSDK導入とflutter doctor診断を済ませてください。3.2.5が要求するDart SDKは3.8.0以上で、2026年8月12日公開のFlutter 3.47.0はDart 3.13.0を同梱するため条件は満たします。

fromJsonとtoJsonを生やす最小構成のコードと確認点

宣言側で書くのは、factoryコンストラクタとJSON入口の2つだけです。toJsonは明示的に書かなくても、fromJsonを宣言した時点で対になる実装が出力されます。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'person.freezed.dart';
part 'person.g.dart';

@freezed
abstract class Person with _$Person {
  const factory Person({
    required String firstName,
    @Default(0) int age,
    @JsonKey(name: 'last_name') String? lastName,
  }) = _Person;

  factory Person.fromJson(Map<String, Object?> json) =>
      _$PersonFromJson(json);
}

確認したいのは右辺の_Personです。この名前が生成される具象クラスになるため、綴りがずれると解決できない参照として残ります。

@Defaultと@JsonKeyでAPI側のキー名と既定値を吸収する

factoryコンストラクタでは通常の既定値構文が使えないため、既定値は@Default(0)のように注釈で与えます。渡す値は定数式に限られ、実行時に決まる値は呼び出し側で組み立てます。

サーバ側のキー名がDartの命名と食い違う場合は@JsonKey(name: 'last_name')で吸収します。モデル側の名前をDartの流儀のまま保てるうえ、サーバ側の命名が変わったときの修正も注釈1行で済みます。

sealed classとUnion型で画面の状態を型へ落とす書き方

データクラスと並ぶ導入動機がunion型です。読み込み中・成功・失敗といった画面の状態を、フラグの組み合わせではなく別々の型として持てます。

複数のfactoryコンストラクタで画面の分岐を型へ落とす書き方

1つのsealed classに複数のfactoryコンストラクタを並べると、それぞれが独立したサブクラスとして生成されます。呼び出し側はswitch式で受け、sealedなので網羅性は静的解析が見てくれます。

@freezed
sealed class Result with _$Result {
  const factory Result.data(List<Item> items) = ResultData;
  const factory Result.loading() = ResultLoading;
  const factory Result.error([String? message]) = ResultError;
}

Widget build(BuildContext context) {
  return switch (state) {
    ResultData(:final items) => ItemListView(items),
    ResultLoading() => const CircularProgressIndicator(),
    ResultError(:final message) => ErrorView(message),
  };
}

フラグを2つ持つ設計だと「読み込み中かつ失敗」という起こりえない組み合わせが型の上では表現できてしまいます。union型に寄せると、その状態は最初から作れません。

3.0.0でmapとwhenが消え3.1.0で戻された経緯と現在

3.0.0の破壊的変更には「mapwhenおよびその派生を削除した。Dartがパターンマッチを得てから非推奨としてきたため」という項目が入りました。ところが3.1.0(2025年7月2日公開)でこれらは復活しています。削除から復活まで約4か月です。

いま新しく書くなら、公式が非推奨の理由に挙げたとおりパターンマッチ側で揃えるほうが将来の振れ幅は小さく済みます。既存コードにwhenが大量に残っている場合は、3.1.0以降であれば動くため、移行と版上げを同時にやる必要はありません。

unionKeyとunionValueCaseでサーバ側のtypeに合わせる

union型をJSONへ載せるとき、どの分岐かを示す判別キーが要ります。既定ではruntimeTypeという名前のキーが使われるため、サーバ側の設計に合わせるなら注釈で指定します。

@Freezed(unionKey: 'type', unionValueCase: FreezedUnionCase.pascal)
sealed class ApiEvent with _$ApiEvent {
  const factory ApiEvent(String a) = ApiEventData;

  @FreezedUnionValue('SpecialCase')
  const factory ApiEvent.special(String a, int b) = ApiEventSpecial;
}

プロジェクト全体で同じ規約にするなら、build.yamlのfreezedビルダーにunion_keyunion_value_caseを書けば既定値を変えられます。個別の値だけ揃わない分岐は@FreezedUnionValueで上書きします。

build_runner運用で詰まりやすい生成物とコレクションの扱い

運用上の論点は、注釈の書き方よりも生成物の扱いに集中します。ここを決めずに配ると参加者ごとに手順が割れます。

buildとwatchの使い分けと出力衝突を解消するフラグの指定

一度だけ生成するならdart run build_runner build、編集しながら追随させるならdart run build_runner watchです。公式READMEが例示しているのはdart run build_runner watch -dで、この-d--delete-conflicting-outputsの短縮形にあたります。

既存の生成ファイルと新しい出力がぶつかると、フラグなしでは処理が止まります。生成物をGitに含めない運用なら常に付けて差し支えありません。同じbuild_runnerを使うパッケージ、たとえばアセット側の生成を担うFlutterGenとは|Flutterアセット管理の導入・使い方を解説のツールとは、1回のビルドでまとめて走ります。

生成ファイルをGitへ含めるかどうかをCIと配布形態から決める

判断軸は好みではなく配布形態です。自分たちだけがビルドするなら、*.freezed.dart*.g.dartを除外してCIで生成する形が扱いやすくなります。レビューの差分に生成結果が混ざらず、コンフリクトの発生源も減るためです。

一方、社内共有のパッケージとして配る場合は含める判断もあります。利用側がbuild_runnerを持たなくても解決できるからです。除外するなら、CIのビルド手順に生成コマンドを必ず1行入れてください。抜けると手元では通るのにCIだけ落ちます。

3.0.0で既定になった変更不可コレクションの扱いと解除の方法

3.0.0以降、生成されたインスタンスが持つリスト・マップ・セットは変更不可のビューへ自動変換されます。2.x系から上げたコードで生成後のリストへ要素を追加していると、ここが実行時のエラーへ変わります。

切り替えたい場合は、クラス単位で@Freezed(makeCollectionsUnmodifiable: false)を付けます。2026年8月18日時点の公式READMEには、この挙動を全体で無効にするbuild.yamlのキーは示されていません。全体で外す前提の設計にはせず、外す必要があるクラスだけ注釈で指定する運用が現実的です。

2026年8月時点の依存解決の実測とbuild_runnerが止まる版

ここからは導入前に把握しておきたい依存の状態です。数値はすべて2026年8月18日にpub.devのAPIから取得しました。

freezed 3.2.5のanalyzer上限が10系で止まっている事実

安定版の3.2.5は2026年2月3日の公開で、そこから約6か月更新が止まっています。pubspecが宣言するanalyzerの制約は9.0.0以上11.0.0未満で、上限が10系に張り付いた状態です。

一方でanalyzer本体は14.1.0(2026年7月13日公開)まで進みました。つまりFreezedを入れたプロジェクトでは、解決結果が10系(10.2.0が2026年2月21日公開)へ引き戻されます。10系のDart SDK要件は3.9.0以上なので、動くこと自体に支障はありません。

build_runner 2.16.0と同居できない依存の重なり方

問題が出るのはbuild_runner側です。2.15.2(2026年7月13日公開)以降、build_runnerはanalyzerを13.3.0以上15.0.0未満で要求するようになりました。Freezed 3.2.5の上限11.0.0未満とは重なる範囲がありません。

パッケージ 版と公開日 analyzerの制約
freezed 3.2.5(2026-02-03) 9.0.0以上11.0.0未満
build_runner 2.15.1(2026-07-08) 8.0.0以上14.0.0未満
build_runner 2.16.0(2026-07-31) 13.3.0以上15.0.0未満
json_serializable 6.14.1(2026-07-30) 10.0.0以上15.0.0未満
freezed 4.0.0-dev.3(2026-06-13) 13.0.0以上14.0.0未満

重なりが残るのはbuild_runner 2.15.1までです。json_serializableの最新6.14.1は下限が10.0.0なので、10系でFreezedと同居できます。結果として、いま入れると解決結果はbuild_runner 2.15.1・analyzer 10系に落ち着きます。

4.0.0-devはDart 3.12以上を要求し現行stableでは入る

この状態を解くのが4.0.0系です。4.0.0-dev.3(2026年6月13日公開)はanalyzerを13.0.0以上で要求し、Dart SDKは3.12.0以上を求めます。Flutter 3.47.0のDart 3.13.0はこの条件を満たすため、インストール自体は通ります。

ただし名前のとおり開発版です。納品物に載せるかは、build_runnerを最新へ上げたい具体的な理由があるかどうかで分けてください。理由が薄いなら、安定版に留めて2.15.1で運用するほうがリスクは小さく収まります。

受託開発でpubspecへ書く固定範囲と引き継ぎ資料への記載

生成系のパッケージは、版を範囲で書かず具体値で固定してください。範囲指定のままだと数か月後のpub getで解決結果が変わります。pubspec.lockもリポジトリへ入れ、参加者間で同じ解決結果を再現できる状態にします。

引き継ぎ資料には「build_runnerがこの版で止まっている理由」を1行残しておくと、次の担当者が版上げを試して詰まる時間を消せます。Flutter SDK本体の版を固定する手順はFVMとは?Flutter SDKをプロジェクト単位で固定する運用とCI設定にまとめました。SDKの版とパッケージの版は別の話なので、両方を書き残す形になります。

この記載範囲を含めて、クロスプラットフォームアプリの設計から引き渡しまでを外部と組んで進める場合は、Flutter / React Nativeによるクロスプラットフォーム開発で扱っています。生成物の管理方針とCI構成を先に決めると、後半の手戻りが出にくくなります。

Dart 3の言語機能で足りる範囲とFreezedを見送ってよい場面

Freezedが埋めていた穴の一部は、Dart 3で言語機能として入りました。どこまでを言語側で賄えるかを線引きすると、導入判断が具体になります。

recordsとパターンマッチで置き換えられる範囲の見極め方

Dart 3のrecordsは、言語側で構造的な等価判定と文字列化を持ちます。関数から2つの値を返す用途なら、専用のクラスを生成する理由はありません。パターンマッチも言語機能なので、分岐の書き方だけが目的ならFreezedは不要です。

sealedそのものも言語機能です。したがってunion型の網羅性チェックだけが目的なら、生成なしで書けます。Freezedが上乗せするのは、そこにcopyWithとJSON変換、そして分岐ごとの命名を機械的に付ける部分です。逆に言えば、この3つを手で書く量が少ないプロジェクトでは上乗せ分が小さくなります。

equatableやdart_mappableとの使い分けを実測値で見る

同じ領域には複数のパッケージがあります。2026年8月18日時点のpub.devの数値を並べると、選択肢の位置関係が見えます。

パッケージ 30日ダウンロード likes 担う範囲
built_value 11,936,078 781 不変値と直列化
equatable 5,383,741 3,628 等価判定のみ
json_serializable 3,133,979 3,950 JSON変換のみ
freezed 2,391,769 4,517 複製と分岐と連携
dart_mappable 420,993 465 変換と複製を一括

ダウンロード数はライブラリの内部依存として引かれる分を含むため、そのまま人気の順位にはなりません。likesではFreezedが4,517で最上位です。品質指標のpub pointsは、Freezedが160点満点中140点なのに対し、json_serializableとdart_mappableは満点を取っています。

使い分けの目安は次の通りです。等価判定だけならequatableでコード生成そのものを避けられます。JSON変換だけならjson_serializable単体で足ります。copyWithとunion型とJSON変換の3つが揃って要るならFreezed、生成を1パッケージで完結させたいならdart_mappableという並びです。

Freezedを採用してよい条件と見送るべき条件の切り分け方

採用してよいのは、次の条件が2つ以上重なる場合だと考えています。データモデルが10個を超える。画面の状態をunion型で表す設計にしている。複数人で触る。CIで生成コマンドを回せる。この4つのうち2つ以上が当てはまるなら、手書きの総量と修正漏れのリスクが生成の手間を上回ります。

見送ってよいのは、モデルが数個でJSONも浅い場合、build_runnerを他の用途で使っていない場合、そしてanalyzerを最新へ追随させたい方針がある場合です。3つ目は2026年8月時点の依存制約から来る条件で、4.0.0の安定版が出れば解消します。判断を保留したいなら、equatableとjson_serializableの組み合わせで始め、モデル数が増えた時点で寄せる順序が無理なく進みます。

よくある質問

Freezed 3.xへ上げると既存コードのどこが壊れますか?

CHANGELOGが挙げる破壊的変更は3点です。クラス宣言にabstractsealedが要るようになったこと、mapwhenの削除、コレクションが変更不可のビューへ自動変換されるようになったことです。mapwhenは3.1.0で復活したため、実際に手を入れるのは残る2点になります。

.freezed.dartと.g.dartはGitに入れるべきですか?

アプリとしてビルドするだけなら除外し、CIで生成する形が扱いやすくなります。差分にコード生成の結果が混ざらず、コンフリクトも減るためです。社内パッケージとして配る場合は含める判断もあります。除外するなら、CIの手順に生成コマンドを入れ忘れないでください。

build_runnerが最新版に上がらないのはなぜですか?

Freezed 3.2.5がanalyzerを11.0.0未満に制限しているためです。build_runnerは2.15.2以降、analyzerを13.3.0以上で要求します。重なる範囲がないので、pubの解決先はbuild_runner 2.15.1以前です。最新のbuild_runnerが要るなら、Freezed側を4.0.0-devへ上げるか、Freezedの採用そのものを見直す形になります。

json_serializableを別に入れなくてもJSON変換できますか?

できません。Freezedが生成するのはデータクラスとunion型の実装までで、fromJsontoJsonの中身はjson_serializableが書き出します。宣言ファイルへpart 'x.g.dart';を足し、dev_dependenciesへ追加してください。JSONを扱わないクラスでは、逆にこのpart指令は書きません。

mapやwhenはもう書かないほうがよいですか?

新しく書く箇所はDartのパターンマッチへ寄せるほうが無難です。公式が3.0.0で削除を試みた理由も「Dartがパターンマッチを得たため」でした。既存コードは3.1.0で復活しているため、急いで書き換える必要はありません。

関連記事

資料請求

RELATED POSTS 関連記事