agentsclimarketplace

Dart freezed models skill

Skill DionisiuBrovka/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

Install
npx -y skills add DionisiuBrovka/dart-freezed-models-skill

Assembled 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:

  1. Immutable by default — use @freezed; reach for @unfreezed only when truly needed.
  2. The right shape for the jobabstract 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:

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 needShapeAnnotation + 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 controlMixed mode / Classic@freezed class (no abstract/sealed) — write your own constructor + final fields
You want to extract one variant of a union for custom logicEject the variantManually 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 (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.

@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

DecoratorWhere it goesWhat it does
@freezedClassDefault annotation; immutable, generates everything
@unfreezedClassMutable variant; skips ==/hashCode
@Freezed(...)ClassCustomized form: copyWith: false, equal: false, genericArgumentFactories: true, makeCollectionsUnmodifiable: false, unionKey:, unionValueCase:, etc.
@Default(value)Constructor parameterConst default value (also adds @JsonKey(defaultValue:) if using JSON)
@Assert('expr', 'msg')Factory constructorAdds an assert(expr, 'msg') to the generated constructor
@Implements<X>() / @With<X>()Variant constructorMake one union variant implement / mix in a class
@Implements.fromString('X<T>') / @With.fromString(...)Variant constructorSame, but for generics where the literal-type form fails
@FreezedUnionValue('name')Variant constructorOverride the JSON discriminator value for that variant
@JsonKey(name: 'x', includeFromJson: false, includeToJson: false)Constructor parameterjson_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:

  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

ErrorLikely causeFix
The non-abstract class _$_Foo is missing implementations for these membersAdded a method/getter without a Foo._() private constructorAdd 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 classAdd abstract (single ctor) or sealed (union)
Undefined name '_Foo' after buildBuild didn't run or .freezed.dart was deletedRun dart run build_runner build -d
The argument type 'String' can't be assigned to ... runtimeTypeJSON for a union missing the runtimeType discriminatorEither include runtimeType in payload, configure @Freezed(unionKey: 'type'), or write a custom JsonConverter
Multiple builders apply / outputs conflictStale generated filesdart run build_runner build --delete-conflicting-outputs
@Default(DateTime.now()) won't compileDefault must be constMove to ._() constructor + nullable param + fallback
assistant?.call(name: 'X') doesn't compileForgot the ?.call for nullable nested copyWithUse .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.mdjson_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.

What ships with it: 10 files

53.0 KB alongside SKILL.md

Keep looking

Skills are one crate of 326,506. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.