Freezed Models
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 "
Free to install — no account needed
Copy the command below and paste into your agent.
Instant access • No coding needed • No account needed
What you get in 5 minutes
- Full skill code ready to install
- Works with 4 AI agents
- Lifetime updates included
Description
--- name: freezed-models description: 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. --- # Freezed Models A skill for writing, refactoring, and debugging data models with the [Freezed](https://pub.dev/packages/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: 1. **Immutable by default** — use `@freezed`; reach for `@unfreezed` only when truly needed. 2. **The right shape for the job** — `abstract` for single-constructor models, `sealed` for unions. 3. **Pattern-matched, not `.when`'d** — on Dart 3 / Freezed 3, use `switch` expressions. The legacy `when`/`map` methods no longer exist in Freezed 3.x. 4. **Generated** — never hand-edit `.freezed.dart` or `.g.dart` files; rerun `build_runner` instead. 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 `factory` constructors **must** now be marked `abstract` (single constructor) or `sealed` (multiple constructors / union). - `.when` / `.map` / `.maybeWhen` / `.maybeMap` and their variants **were removed**. Use Dart's built-in `switch` pattern matching instead. - "Mixed mode" was added — Freezed now also works with regular Dart constructors (no `factory` required), enabling `extends` and non-constant defaults more naturally. - `MyClass._()` private constructors can now accept parameters and call `super()`. 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: ```bash 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: ```bash 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: ```yaml analyzer: errors: invalid_annotation_target: ignore ``` For lint help, also recommend `freezed_lint` (catches missing mixin / missing `._()` constructor): ```bash dart pub add dev:custom_lint dev:freezed_lint ``` ```yaml # 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". ```dart 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. ```dart @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. ```dart @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 (`ResultData` not `_Data`) makes pattern matching readable. **In Freezed 3.x, prefer public names** like `ResultData` over `_Data` because they're what users see in `switch` cases. - Generic unions need `genericArgumentFactories: true` in the build config or a per-class `@Freezed(genericArgumentFactories: true)` to serialize. See `references/json-serialization.md`. - For multi-state JSON deserialization, Freezed switches on a `runtimeType` field 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. ```dart @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. ```dart @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`: ```dart // 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: ```dart 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: ```dart @Freezed(makeCollectionsUnmodifiable: false) abstract class Foo with _$Foo { ... } ``` Or globally, in `build.yaml`: ```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: ```dart @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: ```dart 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: 1. **Add `abstract` or `sealed`** to every `@freezed`/`@Freezed(...)` class. Single-constructor → `abstract`; multi-constructor → `sealed`. 2. **Replace `.when`/`.map`/etc. with `switch`** expressions. The methods no longer exist. 3. **Rename short private redirects** (e.g. `_Loading`) to public names (e.g. `LoadingState`) — required because `switch` cases reference the type by name and `_Loading` from another file doesn't compile. 4. **Replace `@JsonKey(ignore: true)`** with `@JsonKey(includeFromJson: false, includeToJson: false)`. 5. **Bump `freezed_annotation` and `freezed`** in `pubspec.yaml`, then `dart 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.dart` or `.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` + `@unfreezed` confusion** — most data models should be immutable. `@unfreezed` is rare. Ask "do you actually need to mutate this in place?" before recommending it. - **Disabling `makeCollectionsUnmodifiable` reflexively** — usually a sign the user is mutating where they should `copyWith`. - **Reaching for `.when`/`.map` on Freezed 3.x** — those methods are gone; use `switch`. - **Naming union variants `_Loading`, `_Error`** in Freezed 3.x — compile fine but pattern matching by name is less ergonomic. Prefer `LoadingState`, `ErrorState`, etc. - **Forgetting `part 'foo.freezed.dart';`** or the `_$Foo` mixin — Freezed silently won't generate. The `freezed_missing_mixin` lint catches this; recommend installing `freezed_lint`. - **One Freezed class per file is not required** — but it's the easiest mental model. Multiple per file works fine, just one `part` directive 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_serializable` integration deep dive: unions, generics, custom converters, nested toJson, `unionKey`/`unionValueCase`, `@JsonKey` patterns. - `references/pattern-matching.md` — Dart 3 pattern matching for Freezed unions: `switch` expression vs statement, destructuring, exhaustiveness, replacing `when`/`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 abstract` data class with JSON. - `union_state.dart` — `@freezed sealed` union for BLoC/Cubit state with pattern matching. - `generic_response.dart` — generic `Result<T>` / `ApiResponse<T>` with `genericArgumentFactories`. - `classic_class.dart` — mixed-mode example with `extends` + non-const defaults. These are ready to copy into a project; replace the example fields with the user's domain.
Security Status
Unvetted
Not yet security scanned
Related AI Tools
More Make Money tools you might like
Social Autoposter
Free"Automate social media posting across Reddit, X/Twitter, LinkedIn, and Moltbook. Find threads, post comments, create original posts, track engagement stats. Use when: 'post to social', 'social autoposter', 'find threads to comment on', 'create a post
PICT Test Designer
FreeDesign comprehensive test cases using PICT (Pairwise Independent Combinatorial Testing) for any piece of requirements or code. Analyzes inputs, generates PICT models with parameters, values, and constraints for valid scenarios using pairwise testing.
Product Manager Skills
FreePM skill for Claude Code, Codex, Cursor, and Windsurf. Diagnoses SaaS metrics, critiques PRDs, plans roadmaps, runs discovery, coaches PM career transitions, pressure-tests AI product decisions, and designs PLG growth strategies. Seven knowledge doma
paper-fetch
FreeUse when the user wants to download a paper PDF from a DOI, title, or URL via legal open-access sources. Tries Unpaywall, arXiv, bioRxiv/medRxiv, PubMed Central, and Semantic Scholar in order. Never uses Sci-Hub or paywall bypass.
Beautiful Prose (Claude Skill)
FreeA hard-edged writing style contract for timeless, forceful English prose without modern AI tics. Use when users ask for prose or rewrites that must be clean, exact, concrete, and free of AI cadence, filler, or therapeutic tone.
SkillCheck (Free)
FreeValidate Claude Code skills against Anthropic guidelines. Use when user says "check skill", "skillcheck", "validate SKILL.md", or asks to find issues in skill definitions. Covers structural and semantic validation. Do NOT use for anti-slop detection,