Introduction
Freezed is a code-generation package that makes immutable data classes and unions ergonomic in Dart. For Flutter mobile development, designing domain models that enforce validation and support serialization is essential for robust apps. This tutorial shows practical patterns for using Freezed to represent domain models, validate inputs at boundaries, and serialize safely for persistence and APIs.
Why Freezed For Domain Models
Freezed yields immutable classes, equatable behavior, copyWith, pattern matching, and seamless JSON interoperability via json_serializable. These properties align with domain-driven design: immutability prevents accidental state changes, equatability simplifies testing, and generated serialization removes boilerplate when exchanging data with REST, local storage, or platform channels common in mobile development.
Defining Immutable Domain Models
When you model a domain entity, prefer typed fields and encapsulate validation in value objects or factory constructors. Keep Freezed classes thin — they represent valid domain state. Use fromJson/toJson for DTO interop, but perform validation when converting primitive payloads into domain types.
Example Freezed model (note the part files and build_runner usage are assumed):
@freezed
class User with _$User {
const factory User({
required UserId id,
required Email email,
required FullName name,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}This class is lightweight and relies on UserId, Email, and FullName value objects to guarantee correctness. That separation keeps domain invariants centralized and testable.
Validation Strategies
Validation belongs at the domain boundary. Implement value objects that encapsulate validation rules and expose a single canonical representation. Options for handling invalid input include throwing exceptions, returning Result/Either types, or using freezed unions to model success/failure. For predictable mobile development flows (forms, network input), returning a typed failure result improves UX because you can map errors to form fields.
A minimal value object pattern using a Result wrapper (pseudo Either) looks like this:
class Email {
final String value;
Email._(this.value);
static Either<ValueFailure, Email> create(String raw) =>
_isValidEmail(raw) ? Right(Email._(raw)) : Left(ValueFailure('Invalid email'));
}Use Either/Result in factories and repository boundaries. For example, a UserFactory can take raw JSON, create value objects, and aggregate failures into a domain error type. This keeps business logic predictable and side-effect free.
Serialization And Interoperation
Freezed + json_serializable handles standard JSON maps. For value objects, provide toJson/fromJson adapters so serialization doesn't leak invalid forms. Two patterns work well:
Convert value objects to primitives in the Freezed model's toJson (e.g., email.value). Use custom JsonKey with fromJson/toJson helpers if you want a direct mapping.
Keep DTOs separate: create plain map DTOs for network/local storage, convert to domain models with factories that validate.
Example of a custom adapter using JsonKey:
@Freezed()
class Contact with _$Contact {
const factory Contact({
@JsonKey(fromJson: PhoneNumber.fromJson, toJson: PhoneNumber.toJson)
required PhoneNumber phone,
}) = _Contact;
factory Contact.fromJson(Map<String, dynamic> json) => _$ContactFromJson(json);
}This keeps serialization deterministic while still invoking your value object logic on deserialization.
Best Practices For Mobile Development
Validate at the edge: UI -> Use lightweight client validation for UX, then validate again in domain/repository methods.
Favor explicit results over exceptions for predictable flow control in async code common to Flutter apps.
Keep Freezed classes focused on state; place business rules in services or domain factories so you can test behavior without widgets.
Use copyWith extensively when updating models in state managers (Provider, Riverpod, Bloc) to maintain immutability.
Keep JSON adapters small and deterministic; avoid business validation during JSON serialization to prevent surprising runtime errors.
Vibe Studio

Vibe Studio, powered by Steve’s advanced AI agents, is a revolutionary no-code, conversational platform that empowers users to quickly and efficiently create full-stack Flutter applications integrated seamlessly with Firebase backend services. Ideal for solo founders, startups, and agile engineering teams, Vibe Studio allows users to visually manage and deploy Flutter apps, greatly accelerating the development process. The intuitive conversational interface simplifies complex development tasks, making app creation accessible even for non-coders.
Conclusion
Freezed is an excellent tool for Flutter mobile development when you model domain objects that require validation and serialization. The recommended approach is to combine Freezed immutable models with dedicated value objects or factories that perform validation, and to use json_serializable adapters or DTO layers for safe serialization. This pattern yields easier testing, clearer boundaries, and more maintainable code in production mobile apps.