--- name: "generating-freezed-models" description: "Generates immutable Dart data classes, sealed union types, and deep copyWith using Freezed (v3.2.x) with optional json_serializable integration. Use when creating DTOs, API response models, app state classes, or any model requiring immutability, pattern matching, or JSON serialization. Activates on: @freezed annotation, freezed_annotation import, immutable data class request, copyWith deep nesting, union/sealed types for loading/data/error states, generic response wrappers, or build_runner code generation for models." metadata: last_modified: "2026-04-27 17:41:00 (GMT+8)" --- # Freezed Guide (v3.2.x) ## Goal Generate immutable data classes, union types, and deep `copyWith` using Freezed v3.2.x. Complements Dart 3 `sealed` classes by adding `copyWith`, `==`/`hashCode`, and JSON serialization via `json_serializable`. ## Instructions ### 1. Dependencies and Environment Setup Because Freezed relies heavily on code generation, ensure your `pubspec.yaml` is fully configured: ```yaml dependencies: freezed_annotation: ^3.2.5 json_annotation: ^4.11.0 # If mutual conversion with JSON is required dev_dependencies: build_runner: ^2.14.1 freezed: ^3.2.5 json_serializable: ^6.13.1 # If mutual conversion with JSON is required ``` ### 2. Defining an Immutable Data Class ```dart import 'package:freezed_annotation/freezed_annotation.dart'; // You MUST declare part files; the name must exactly match the current file part 'user_model.freezed.dart'; part 'user_model.g.dart'; // If utilizing JSON serialization // Freezed 3.x: prefer `sealed` modifier for exhaustive pattern matching @freezed sealed class UserModel with _$UserModel { const factory UserModel({ required String id, required String name, @Default(18) int age, @JsonKey(name: 'is_active') @Default(true) bool isActive, }) = _UserModel; // Required for JSON serialization factory UserModel.fromJson(Map json) => _$UserModelFromJson(json); } ``` > After every model change, run `dart run build_runner build -d`. ### 3. Custom Getters and Methods To add computed properties or methods, add a private parameterless constructor. ```dart @freezed abstract class Product with _$Product { // private constructor required to add custom methods const Product._(); const factory Product({ required double price, required double discountRate, }) = _Product; // Custom Getter double get discountedPrice => price * (1 - discountRate); // Custom Method bool isFree() => discountedPrice == 0; } ``` ### 4. Generic Types Support ```dart @freezed @JsonSerializable(genericArgumentFactories: true) // required for generic JSON parsing sealed class PaginatedResponse with _$PaginatedResponse { const factory PaginatedResponse({ required int currentPage, required int totalPages, required List data, }) = _PaginatedResponse; // fromJson for generics requires a conversion function parameter factory PaginatedResponse.fromJson( Map json, T Function(Object? json) fromJsonT, ) => _$PaginatedResponseFromJson(json, fromJsonT); } ``` ### 5. Union Types and Dart 3 Pattern Matching Use Freezed union types for sealed state modeling with automatic subtypes. ```dart @freezed sealed class ApiResponse with _$ApiResponse { const factory ApiResponse.loading() = ApiResponseLoading; const factory ApiResponse.data(List items) = ApiResponseData; const factory ApiResponse.error(String message) = ApiResponseError; } ``` **Use Dart 3 native pattern matching with Freezed union types:** ```dart Widget buildStatus(ApiResponse response) { return switch (response) { ApiResponseLoading() => const CircularProgressIndicator(), ApiResponseData(:final items) => Text('Item Count: ${items.length}'), ApiResponseError(:final message) => Text('Error: $message'), }; } ``` > Prefer native `switch` expressions over `.when()` / `.map()` — better compiler exhaustiveness checking. ### 6. Deep Copy (`copyWith`) vs `@unfreezed` #### 6.1 Automatic Deep CopyWith When a Freezed class contains another Freezed class, deep `copyWith` chaining is generated automatically. ```dart // Instead of: user.copyWith(address: user.address.copyWith(city: 'Taipei')) final newUser = user.copyWith.address(city: 'Taipei'); ``` #### 6.2 When to use `@unfreezed` - **Default:** Always use `@freezed` (immutable). - `@unfreezed` allows mutable fields (`user.age = 20`) but skips `==`/`hashCode` generation. - **Anti-pattern** in Riverpod/BLoC — breaks unidirectional data flow. Only consider for large mutable forms where deep-copying causes measurable performance issues. ### 7. JSON Serialization with `@JsonKey` Use `@JsonKey` to customize field serialization: - **Ignore a field:** `@JsonKey(includeFromJson: false, includeToJson: false)` - **Custom converter** for non-standard formats (e.g., date strings): ```dart class MyDateConverter implements JsonConverter { const MyDateConverter(); @override DateTime fromJson(String json) => DateTime.parse(json.replaceAll('/', '-')); @override String toJson(DateTime object) => object.toIso8601String(); } @freezed sealed class Event with _$Event { const factory Event({ @MyDateConverter() required DateTime eventDate, }) = _Event; // ... fromJson ... } ``` ### 8. Summary 1. Use Freezed for all DTOs and app state classes. 2. Add a private constructor (`const ClassName._()`) to enable custom getters/methods. 3. Use `switch` expressions instead of `.when()` / `.map()`. 4. Default to `@freezed` (immutable); avoid `@unfreezed` in app state. ## Constraints * Generics with JSON: always add `@JsonSerializable(genericArgumentFactories: true)`. * Avoid `@unfreezed` in app state; use only for mutable form models where deep-copy performance is proven problematic.