Why I'm Still Using Freezed In 2026

Stop writing boilerplate in Flutter! Freezed is a powerful code generator that delivers immutable models, automatic copyWith, value equality, and seamless JSON serialization. Learn how Freezed's Union Types create type-safe state, leading to cleaner, more robust, and maintainable applications.

Why I'm Still Using Freezed In 2026
Photo by James Harrison / Unsplash

If you've developed apps with Flutter for some time, you might have come across the Freezed package. If you're fed up of writing and maintaining boilerplate for your data models, Freezed is the powerful code generation tool that should be on your radar. It’s a real game-changer for writing clean, immutable and type-safe Dart code.

Let's look at how Freezed, created by Rémi Rousselet (of Riverpod fame), can reduce friction in your development workflow, and lead to more maintainable Flutter apps, and why I think it's more useful than ever in 2026.


The Problem

When we build apps, we spend a lot of time defining models. Hopefully this isn’t a new concept! Let’s take this example of a simple product in an e-commerce app:

class Product {
 Product({
   this.name,
   this.description,
   this.price,
   this.imageUrls,
   this.isActive,
 });

 final String? name;
 final String? description;
 final double? price;
 final List<String>? imageUrls;
 final bool? isActive;
}

Before we even add any logic, a class like this can easily take up 10-15 lines of code, and that's just the tip of the iceberg.

Realistically, the data for this class will be coming from a backend via an API, and so we need to add more code to help interact with this:

  • fromJson - Convert the raw JSON content into a model Flutter can interpret
  • Optionally, toJson - If we need to send the model back to the backend we need to complete the circle and convert the model back to JSON

These two factory methods alone have almost doubled the amount of code we're now responsible for maintaining, just for some simple utility.

class Product {
 Product({
   this.name,
   this.description,
   this.price,
   this.imageUrls,
   this.isActive,
 });

 final String? name;
 final String? description;
 final double? price;
 final List<String>? imageUrls;
 final bool? isActive;

 factory Product.fromJson(Map<String, dynamic> json) => Product(
       name: json['name'],
       description: json['description'],
       price: json['price'],
       imageUrls: json['image_urls']?.cast<String>(),
       isActive: json['is_active'],
     );

 Map<String, dynamic> toJson() => {
       'name': name,
       'description': description,
       'price': price,
       'image_url': imageUrls,
       'is_active': isActive,
     };
}

If want to add more functionality, say copying and editing, we have to add even more code:

class Product {
 Product({
   this.name,
   this.description,
   this.price,
   this.imageUrls,
   this.isActive,
 });

 final String? name;
 final String? description;
 final double? price;
 final List<String>? imageUrls;
 final bool? isActive;

 factory Product.fromJson(Map<String, dynamic> json) => Product(
       name: json['name'],
       description: json['description'],
       price: json['price'],
       imageUrls: json['image_urls']?.cast<String>(),
       isActive: json['is_active'],
     );

 Map<String, dynamic> toJson() => {
       'name': name,
       'description': description,
       'price': price,
       'image_url': imageUrls,
       'is_active': isActive,
     };

 Product copyWith({
   String? name,
   String? description,
   double? price,
   List<String>? imageUrls,
   bool? isActive,
 }) =>
     Product(
       name: name ?? this.name,
       description: description ?? this.description,
       price: price ?? this.price,
       imageUrls: imageUrls ?? this.imageUrls,
       isActive: isActive ?? this.isActive,
     );
}

This causes problems because we're human

  • Developers are lazy (sorry!): Writing out all of this boilerplate each time is a boring task, I'm sure we'd all rather be spending our time building fun new features than maintaining boilerplate.
  • We're susceptible to human error: If you miss a field in your factories, or if a spelling mistake sneaks in, the compiler won't warn you. This causes all sorts of hard-to-spot headaches at runtime.
    • Did you spot the typo in toJson? 😉

Freezed to the Rescue

Freezed is a package aimed at leveraging code generation to create all of this fiddly code for us, allowing us to get back to building things.With a few imports and annotating our class with @freezed, build_runner generates all the rest of the code we need.

Installing

To get started, you'll need three key dependencies in your pubspec.yaml:

  1. freezed_annotation
  2. freezed
  3. build_runner

Once you've finished writing your new classes with Freezed, use build_runner to generate the boilerplate

flutter pub run build_runner build

With these changes, our model now looks like this:

import 'package:freezed_annotation/freezed_annotation.dart';

part 'product.freezed.dart';
part 'product.g.dart';

@freezed
class Product with _$Product {
 factory Product({
   String? name,
   String? description,
   double? price,
   List<String>? imageUrls,
   bool? isActive,
 }) = _Product;

 factory Product.fromJson(Map<String, dynamic> json) =>
     _$ProductFromJson(json);
}

This gives us three immediate benefits:

1. JSON Serialisation

When paired with json_serializable, Freezed generates all the fromJson and toJson boilerplate for you. It’s a simple, one-line factory that handles all the tedious and repetitive code for you.

2. Immutability and copyWith

The package handily generates copyWith for us now. This allows us to safely make edits to our models while keeping mutability.

3. Value Equality

Freezed also handles equality for us, implementing the == operator and hashCode getter. This is really nice because we no longer have to maintain a fiddly list of props to ensure our models are equal - something really easy to forget about if you're not careful.


Deep Copy

With Freezed we can also make updates to complex models much easier. Imagine a model comprised of other models, like a house object.

class House {
 Product({
   this.name,
   this.address,
 });

 final String? name;
 final Address? address;
}
class Address {
 Product({
   this.number,
   this.street,
   this.city,
   this.county,
   this.country,
   this.postcode,
 });

 final String? number;
 final String? street;
 final String? city;
 final String? county;
 final String? country;
 final String? postcode;
}

To update an address field for a house object, we'd traditionally have to do something like:

final updatedHouse = oldHouse.copyWith(
  address: oldHouse.address.copyWith(
    street: 'New Street Name',
  ),
);

With Freezed's deep copy functionality, we can simplify this to just:

final updatedHouse = oldHouse.copyWith.address(
  street: 'New Street Name',
);

This is much easier to read and write, and is a game changer when working with complex models.


Sealed Classes

Freezed also offers support for Union Types, (sealed classes) in Dart. This allows you to define a finite list of possible states for a class, which is really useful for managing the different business logic states.

For example, a user state might be defined like this:

@freezed
abstract class UserState with _$UserState {
  // A sealed class with multiple possible states
  const factory UserState.initial() = _Initial;
  const factory UserState.loading() = _Loading;
  const factory UserState.loaded(User user) = _Loaded;
  const factory UserState.error(String message) = _Error;
}

Freezed generates methods like .when() and .maybeWhen() that force us to be exhaustive when handling these states. As this functions like a sealed class, the Dart analyser forces us to account for all possible state scenarios.

state.when(
  initial: () => Text('Tap to load'),
  loading: () => CircularProgressIndicator(),
  loaded: (user) => UserProfile(user: user),
  error: (message) => ErrorMessage(message: message),
);

If you forget to handle a state, the IDE will tell you. This gives us confidence our UI can support any of our pre-determined outcomes.

Dart 3 introduced sealed classes, so this is less of a benefit today, but still provides functionality for older codebases, and helps maintain the idea that Freezed is leading innovation in the Dart world!


Considerations

While Freezed saves you time in development and maintenance, the code generation process powered by build_runner does take time to run.

There is the question of whether or not to check the generated Freezed files into source control, and to that I don't think there's a good one-size-fits-all answer.

Checking generated files into source control means time (and therefore money) is saved, as build pipelines don't need to generate these files. This approach requires discipline though, as developers need to remember to keep generated files up to date, and avoid editing generated code by hand. Also, the number of files generated by Freezed can inflate the size of pull requests significantly - causing further potential headaches.

Personally I would recommend excluding generated files from source control, as it ensures the code output from Freezed is guaranteed to be up to date and free of any manual tampering, while also keeping pull requests lean. As mentioned though, the key thing to consider here is generating code can take time - and if you're using a service like Github actions to build your app then that time all adds up when you're billed per minute of CI run time.

Why Freezed is still relevant in 2026

First of all, lets address the elephant in the room. Yes, AI can generate a lot of this tedious code for us. I'm sure it's a lot quicker than waiting around for build_runner to finish generating and it can get you the same functionality. I prefer to stick with Freezed for a few reasons though.

First, as the code is generated programatically, we know that we generate the exact same code every time, regardless of AI model. This consistency helps avoid confusion and helps get devs up to speed with a codebase faster than a mishmash of boilerplate cobbled together by a combination of ChatGPT, Claude and Copilot.

Furthermore, with freezed there's zero risk of the generated code being sent to a data centre in some far-off destination to train the next generation of AI models. This is a key consideration if you're writing code for more security-focused applications.

The Future

Dart is always evolving, and developments like the introduction of native sealed classes have helped address some of the issues Freezed was originally designed to solve.

I still believe Freezed remains useful because it bundles together so much functionality into one package, and continues to provide value in 2026. While Dart macros ultimately didn't work out, advances in build_runner and augmentations continue to be made, making code generation options an easier sell.

Ultimately there's only one way to find our for yourself if Freezed is a good fit for you, and that's by trying it out. Give it a go and with any luck, you'll find it just as useful as I do.

Subscribe or else