Using Freezed For Domain Models With Validation And Serialization
Summary
Summary

This tutorial demonstrates using Freezed in Flutter mobile development to create immutable domain models, delegate validation to value objects or factories, and serialize safely with json_serializable adapters or DTOs. It covers patterns for predictable error handling (Either/Result), custom JsonKey adapters, and best practices for state updates and testing.

This tutorial demonstrates using Freezed in Flutter mobile development to create immutable domain models, delegate validation to value objects or factories, and serialize safely with json_serializable adapters or DTOs. It covers patterns for predictable error handling (Either/Result), custom JsonKey adapters, and best practices for state updates and testing.

Key insights:
Key insights:
  • Why Freezed For Domain Models: Freezed provides immutability, equatability, copyWith, and JSON generation that fit domain-driven models in Flutter.

  • Defining Immutable Domain Models: Keep Freezed classes thin; delegate validation to value objects and use fromJson/toJson for DTO interop.

  • Validation Strategies: Validate at boundaries with value objects or factory methods, and prefer Result/Either types over unchecked exceptions.

  • Serialization And Interoperation: Use JsonKey adapters or separate DTOs to map between primitives and validated domain types cleanly.

  • Best Practices For Mobile Development: Validate on UI and domain layers, favor explicit results for async flows, and rely on copyWith for immutable updates.

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.

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.

Build Flutter Apps Faster with Vibe Studio

Vibe Studio is your AI-powered Flutter development companion. Skip boilerplate, build in real-time, and deploy without hassle. Start creating apps at lightning speed with zero setup.

Other Insights

Join a growing community of builders today

Join a growing community of builders today

Join a growing community of builders today

Join a growing community of builders today

Join a growing community of builders today

28-07 Jackson Ave

Walturn

New York NY 11101 United States

© Steve • All Rights Reserved 2025

28-07 Jackson Ave

Walturn

New York NY 11101 United States

© Steve • All Rights Reserved 2025

28-07 Jackson Ave

Walturn

New York NY 11101 United States

© Steve • All Rights Reserved 2025

28-07 Jackson Ave

Walturn

New York NY 11101 United States

© Steve • All Rights Reserved 2025

28-07 Jackson Ave

Walturn

New York NY 11101 United States

© Steve • All Rights Reserved 2025

28-07 Jackson Ave

Walturn

New York NY 11101 United States

© Steve • All Rights Reserved 2025