Dart freezed models skill
Use this skill whenever the user works with Freezed in Dart or Flutter — writing new immutable data models, refactoring plain Dart classes to Freezed, migrating Freezed 2.x code to 3.x, or debugging Freezed-generated code. Trigger on any mention of "freezed", "@freezed", "@unfreezed", "@Freezed", `_$ClassName` mixin, `.freezed.dart` part files, `freezed_annotation`, `freezed_lint`, `genericArgumentFactories`, `@Default`, `@Assert`, `@Implements`, `@FreezedUnionValue`, deep `copyWith`, or pattern matching on freezed unions. Also trigger on Russian phrasing like "freezed модель", "сделай модель на freezed", "состояние для блока на freezed", "мигрируй на freezed 3", "почему freezed ругается". Trigger when the user pastes freezed code for review, asks about JSON for a freezed class, describes a state machine / sum type / Result type in Dart, or hits errors like "missing mixin" or "must be abstract or sealed". Do NOT trigger for non-Dart projects or plain `json_serializable` not involving Freezed.From its SKILL.md
npx -y skills add DionisiuBrovka/dart-freezed-models-skillAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
19.4 KB, ~4.6k tokens by cl100k_base, as published. Nobody here has run it
Freezed Models
A skill for writing, refactoring, and debugging data models with the Freezed package in Dart/Flutter. Targets Freezed 3.x (current major), with full coverage of migration from 2.x.
Core philosophy
Freezed exists to remove boilerplate from Dart data classes — ==, hashCode, toString, copyWith, JSON, and tagged unions. A good Freezed model has four properties:
- Immutable by default — use
@freezed; reach for@unfreezedonly when truly needed. - The right shape for the job —
abstractfor single-constructor models,sealedfor unions. - Pattern-matched, not
.when'd — on Dart 3 / Freezed 3, useswitchexpressions. The legacywhen/mapmethods no longer exist in Freezed 3.x. - Generated — never hand-edit
.freezed.dartor.g.dartfiles; rerunbuild_runnerinstead.
When guiding the user, steer them toward these properties. Push back on the anti-patterns listed at the bottom of this file.
What version are we targeting?
Freezed 3.x is the current major (released Feb 2025). It introduced breaking changes:
- Classes using
factoryconstructors must now be markedabstract(single constructor) orsealed(multiple constructors / union). .when/.map/.maybeWhen/.maybeMapand their variants were removed. Use Dart's built-inswitchpattern matching instead.- "Mixed mode" was added — Freezed now also works with regular Dart constructors (no
factoryrequired), enablingextendsand non-constant defaults more naturally. MyClass._()private constructors can now accept parameters and callsuper().
Always assume Freezed 3.x unless the user's pubspec.yaml shows freezed: ^2.x.x. If the user is on 2.x, they have two options: keep the old syntax or migrate. See references/migration-v2-to-v3.md.
Quick setup
For a Flutter project:
flutter pub add freezed_annotation
flutter pub add dev:build_runner dev:freezed
# only if the model needs JSON:
flutter pub add json_annotation dev:json_serializable
For a Dart project, swap flutter for dart. Then:
dart run build_runner watch -d
(watch rebuilds on save; -d deletes conflicting outputs without prompting. For a one-shot build use build instead of watch.)
If using JSON, also add this to analysis_options.yaml to silence a known warning:
analyzer:
errors:
invalid_annotation_target: ignore
For lint help, also recommend freezed_lint (catches missing mixin / missing ._() constructor):
dart pub add dev:custom_lint dev:freezed_lint
# analysis_options.yaml
analyzer:
plugins:
- custom_lint
Decision tree: which pattern to use
Before writing any Freezed model, decide which of these five shapes fits. Don't default to the union pattern when a single class is enough; don't default to a single class when the model is conceptually a sum type.
| User's need | Shape | Annotation + keyword |
|---|---|---|
| One immutable data class (User, Address, Product) | Single class | @freezed abstract class |
| One mutable data class (form state, editable draft) | Single class, mutable | @unfreezed abstract class |
| A sum type / multiple variants (Result, ApiResponse, BlocState, Event) | Union | @freezed sealed class |
Need extends, non-const defaults, or full constructor control | Mixed mode / Classic | @freezed class (no abstract/sealed) — write your own constructor + final fields |
| You want to extract one variant of a union for custom logic | Eject the variant | Manually write the subclass; optionally annotate it @freezed too |
Examples and full code for each are in the next section.
The five canonical shapes
1. Single immutable model (most common)
Use this for User, Address, Product, Settings, etc. — anything that's "just data".
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart'; // omit if no JSON
@freezed
abstract class User with _$User {
const factory User({
required String id,
required String name,
@Default(0) int age,
String? email,
}) = _User;
factory User.fromJson(Map<String, Object?> json) => _$UserFromJson(json);
}
What you get: ==, hashCode, toString, copyWith, fromJson, toJson. All fields are final. List/Map/Set fields become unmodifiable at runtime — see "Mutable collections" below if that's a problem.
The right-hand side _User is the redirect target — Freezed generates that private class. Keep the convention _ClassName unless you want to eject that variant (see shape #5).
A starter template lives at assets/templates/single_model.dart.
2. Single mutable model (@unfreezed)
Use this only when the user explicitly needs mutable fields (e.g. an editable form draft held in widget state). Most of the time, @freezed + copyWith is better — push back if the user reaches for @unfreezed reflexively.
@unfreezed
abstract class FormDraft with _$FormDraft {
factory FormDraft({
required String firstName,
required String lastName,
required final int submitCount, // `final` keeps a single field immutable
}) = _FormDraft;
}
Tradeoffs to call out: no ==/hashCode override (mutable identity), no const constructor, copyWith still works.
3. Union / sum type (sealed)
Use this for state machines, results, API responses — anything where the model is in exactly one of N states.
@freezed
sealed class Result<T> with _$Result<T> {
const factory Result.data(T value) = ResultData;
const factory Result.loading() = ResultLoading;
const factory Result.error(Object error, [StackTrace? stack]) = ResultError;
factory Result.fromJson(
Map<String, Object?> json,
T Function(Object?) fromJsonT,
) =>
_$ResultFromJson(json, fromJsonT);
}
Notes:
- Naming the right-hand sides clearly (
ResultDatanot_Data) makes pattern matching readable. In Freezed 3.x, prefer public names likeResultDataover_Databecause they're what users see inswitchcases. - Generic unions need
genericArgumentFactories: truein the build config or a per-class@Freezed(genericArgumentFactories: true)to serialize. Seereferences/json-serialization.md. - For multi-state JSON deserialization, Freezed switches on a
runtimeTypefield by default — see same reference for customizing the discriminator.
A starter template lives at assets/templates/union_state.dart. To consume this with switch, see references/pattern-matching.md.
4. Mixed mode / Classic class
Use this when the user needs extends, complex constructor logic, or non-const defaults that don't fit @Default(...). Freezed only generates ==/hashCode/toString/copyWith here — you write the fields yourself.
@freezed
class TimedEvent extends BaseEvent with _$TimedEvent {
TimedEvent({required this.name, DateTime? createdAt})
: createdAt = createdAt ?? DateTime.now(),
super(); // can call super in mixed mode
@override
final String name;
@override
final DateTime createdAt;
// For JSON, opt in manually:
factory TimedEvent.fromJson(Map<String, Object?> json) =>
_$TimedEventFromJson(json);
Map<String, Object?> toJson() => _$TimedEventToJson(this);
}
Note: with classic syntax, you also need @JsonSerializable() above the class if you want JSON, and the fields above must be marked @override because the generated mixin declares them.
5. Ejecting a variant of a union
Use this when one variant of a sealed union needs unique methods/getters and you don't want to bloat the parent class.
@freezed
sealed class Result<T> with _$Result<T> {
const Result._();
const factory Result.data(T value) = ResultData;
const factory Result.error(Object error) = ResultError;
}
// Ejected — write the implementation yourself, can be a Freezed class too.
@freezed
abstract class ResultData<T> extends Result<T> with _$ResultData<T> {
const factory ResultData(T value) = _ResultData;
const ResultData._() : super._();
// Add methods unique to ResultData here.
T get unwrap => value;
}
Caveat: when the parent has a ._() constructor, every subclass needs to call it via super._() — and the parent ._() must exist for Dart to allow extends.
Pattern matching on unions (replacing when/map)
In Freezed 3.x, .when/.map are gone. Use Dart's switch:
// Switch expression — preferred when you return a value
final message = switch (result) {
ResultData(:final value) => 'Got $value',
ResultLoading() => 'Loading…',
ResultError(:final error) => 'Failed: $error',
};
// Switch statement — when you don't return
switch (result) {
case ResultData(:final value):
print(value);
case ResultLoading():
showSpinner();
case ResultError(:final error):
showError(error);
}
Because the class is sealed, the compiler checks exhaustiveness — adding a new variant later turns into a compile error in every switch, which is exactly what you want. For cheat sheet, common pitfalls (is vs pattern, when to use _, partial matches), and migration from when/map, see references/pattern-matching.md.
JSON serialization
Add part 'foo.g.dart' and a fromJson factory; Freezed wires json_serializable automatically:
factory User.fromJson(Map<String, Object?> json) => _$UserFromJson(json);
Freezed only generates a fromJson if the factory uses => (arrow syntax). For unions, the JSON discriminator defaults to runtimeType and the variant name; both are configurable via @Freezed(unionKey: ..., unionValueCase: ...) and @FreezedUnionValue('...'). For generics, custom converters, nested toJson, multi-constructor switching, and the JsonKey(includeFromJson: false, includeToJson: false) replacement for the deprecated JsonKey(ignore: true), see references/json-serialization.md.
Common decorators reference
| Decorator | Where it goes | What it does |
|---|---|---|
@freezed | Class | Default annotation; immutable, generates everything |
@unfreezed | Class | Mutable variant; skips ==/hashCode |
@Freezed(...) | Class | Customized form: copyWith: false, equal: false, genericArgumentFactories: true, makeCollectionsUnmodifiable: false, unionKey:, unionValueCase:, etc. |
@Default(value) | Constructor parameter | Const default value (also adds @JsonKey(defaultValue:) if using JSON) |
@Assert('expr', 'msg') | Factory constructor | Adds an assert(expr, 'msg') to the generated constructor |
@Implements<X>() / @With<X>() | Variant constructor | Make one union variant implement / mix in a class |
@Implements.fromString('X<T>') / @With.fromString(...) | Variant constructor | Same, but for generics where the literal-type form fails |
@FreezedUnionValue('name') | Variant constructor | Override the JSON discriminator value for that variant |
@JsonKey(name: 'x', includeFromJson: false, includeToJson: false) | Constructor parameter | json_serializable field control. Use includeFromJson/includeToJson instead of the deprecated ignore: true |
Mutable collections in @freezed
By default, List/Map/Set fields become unmodifiable at runtime in @freezed classes — model.list.add(x) throws. Two options:
@Freezed(makeCollectionsUnmodifiable: false)
abstract class Foo with _$Foo { ... }
Or globally, in build.yaml:
targets:
$default:
builders:
freezed:
options:
make_collections_unmodifiable: false
But the better answer is usually: don't mutate; copyWith with a new list. Push back before disabling the safeguard.
Adding methods/getters to a Freezed class
The generated mixin only knows the fields. To add custom methods/getters, declare a private empty constructor so the generated class can extend yours:
@freezed
abstract class User with _$User {
const User._(); // <-- required when adding methods/getters
const factory User({required String firstName, required String lastName}) = _User;
String get fullName => '$firstName $lastName';
}
Without const User._();, you'll see: The non-abstract class _$_User is missing implementations for these members. The freezed_lint rule freezed_missing_private_empty_constructor catches this.
Deep copyWith
When a Freezed model contains other Freezed models, you get nested copy syntax for free:
final updated = company.copyWith.director.assistant(name: 'John');
// equivalent to:
final updated = company.copyWith(
director: company.director.copyWith(
assistant: company.director.assistant.copyWith(name: 'John'),
),
);
If a nested field can be null, the chain returns null-callable: company.copyWith.director.assistant?.call(name: 'John').
Migration v2 → v3
The minimum mechanical changes to move a v2 codebase to v3:
- Add
abstractorsealedto every@freezed/@Freezed(...)class. Single-constructor →abstract; multi-constructor →sealed. - Replace
.when/.map/etc. withswitchexpressions. The methods no longer exist. - Rename short private redirects (e.g.
_Loading) to public names (e.g.LoadingState) — required becauseswitchcases reference the type by name and_Loadingfrom another file doesn't compile. - Replace
@JsonKey(ignore: true)with@JsonKey(includeFromJson: false, includeToJson: false). - Bump
freezed_annotationandfreezedinpubspec.yaml, thendart run build_runner build -d.
For step-by-step manual migration, scripted bulk migration (Dart script approach), and the more nuanced cases (generics, ejected variants, mixed-mode opportunities), see references/migration-v2-to-v3.md.
Anti-patterns to push back on
- Hand-editing
.freezed.dartor.g.dart— these are regenerated; edits get wiped. If the user wants different output, they need a different decorator/option, not a hand edit. @freezed+@unfreezedconfusion — most data models should be immutable.@unfreezedis rare. Ask "do you actually need to mutate this in place?" before recommending it.- Disabling
makeCollectionsUnmodifiablereflexively — usually a sign the user is mutating where they shouldcopyWith. - Reaching for
.when/.mapon Freezed 3.x — those methods are gone; useswitch. - Naming union variants
_Loading,_Errorin Freezed 3.x — compile fine but pattern matching by name is less ergonomic. PreferLoadingState,ErrorState, etc. - Forgetting
part 'foo.freezed.dart';or the_$Foomixin — Freezed silently won't generate. Thefreezed_missing_mixinlint catches this; recommend installingfreezed_lint. - One Freezed class per file is not required — but it's the easiest mental model. Multiple per file works fine, just one
partdirective each. - Using
@Default(DateTime.now())—DateTime.now()is non-const and won't compile as a default. Use a._()constructor with a fallback (time = time ?? DateTime.now()), or move to a classic class.
Troubleshooting common errors
| Error | Likely cause | Fix |
|---|---|---|
The non-abstract class _$_Foo is missing implementations for these members | Added a method/getter without a Foo._() private constructor | Add const Foo._(); to the class |
Class must be marked as 'abstract', 'sealed', or implement _$Foo (Freezed 3.x) | Missing abstract/sealed keyword on a 2.x-style class | Add abstract (single ctor) or sealed (union) |
Undefined name '_Foo' after build | Build didn't run or .freezed.dart was deleted | Run dart run build_runner build -d |
The argument type 'String' can't be assigned to ... runtimeType | JSON for a union missing the runtimeType discriminator | Either include runtimeType in payload, configure @Freezed(unionKey: 'type'), or write a custom JsonConverter |
Multiple builders apply / outputs conflict | Stale generated files | dart run build_runner build --delete-conflicting-outputs |
@Default(DateTime.now()) won't compile | Default must be const | Move to ._() constructor + nullable param + fallback |
assistant?.call(name: 'X') doesn't compile | Forgot the ?.call for nullable nested copyWith | Use .copyWith.x?.call(...) form |
For more ("why isn't fromJson being generated?", "build_runner hangs forever", IDE-stale-cache issues, freezed_lint output, etc.), see references/troubleshooting.md.
Communication style
Users asking about Freezed range from beginners to senior Dart devs. Calibrate:
- If they say "сделай мне модель пользователя" / "make me a user model" with no prior code → start with the Single immutable model template, ask about needed fields and JSON.
- If they paste existing code → identify which of the five shapes it is, point out 2.x → 3.x issues if any, suggest the smallest change set.
- If they describe a state machine ("у меня есть состояния загрузки, успеха, ошибки") → reach for
sealed class+ pattern matching immediately. Don't make them ask twice. - If they hit a build_runner error → check it against the table above before guessing.
Match the user's language: answer in Russian if they wrote in Russian, English if in English. Code itself stays in Dart with English identifiers unless their existing code already uses another language for class names.
Reference files
references/migration-v2-to-v3.md— full v2 → v3 migration: manual checklist, gotchas, scripted bulk migration with a Dart script.references/json-serialization.md—json_serializableintegration deep dive: unions, generics, custom converters, nested toJson,unionKey/unionValueCase,@JsonKeypatterns.references/pattern-matching.md— Dart 3 pattern matching for Freezed unions:switchexpression vs statement, destructuring, exhaustiveness, replacingwhen/map, common idioms.references/troubleshooting.md— extended error-to-fix table, build_runner issues, IDE cache problems, lint configuration.
Starter templates
In assets/templates/:
single_model.dart— basic@freezed abstractdata class with JSON.union_state.dart—@freezed sealedunion for BLoC/Cubit state with pattern matching.generic_response.dart— genericResult<T>/ApiResponse<T>withgenericArgumentFactories.classic_class.dart— mixed-mode example withextends+ non-const defaults.
These are ready to copy into a project; replace the example fields with the user's domain.
What ships with it: 10 files
53.0 KB alongside SKILL.md
assets/
references/
- json-serialization.md9.7 KB
- migration-v2-to-v3.md14.1 KB
- pattern-matching.md7.4 KB
- troubleshooting.md9.7 KB