[Update, 20th March 2020: I wrote this article shortly after the release of freezed. A lot of the points of this article aren’t valid any more]

Remi Rousselet recently released freezed, a library for generating immutable data classes and union types.

Naturally, I was curious. We are using built_value for creating immutable model objects and for JSON serialization and deserialization for communicating with APIs, and are quite happy with it.

However, we are annoyed by a few things with built_value:

It requires a lot of boilerplate

It was written for Dart 1, which is still noticeable. For example, it does manual type checking instead of relying on the compiler, generics and type inference. built_value introduces some new methods and constructors to fix this, but it’s still there in some cases.

The code generation is pretty slow. For a project ~24k lines of code (excluding generated code, whitespace, and comments), code generation takes about 1 minute. That is with creating a build.yaml file so built_value generator only runs on the model objects. The results should be cached, but in my experience, even a small change often triggers a full rebuild.

So I decided to check out freezed if it’s a better fit for us. Here are my findings:

Simple Immutable Model Objects

This is the bread and butter of both froze and built_value. freezed really shines here.

Simple immutable object using freezed

built_value:

Simple immutable object using built_value

The freezed version is much shorter and has no repetition. For built_value, if you want to have a factory that takes all variables of the object, you need to type all variables twice.

You can also use code templates for generating new models:

Generating a new data class from a template

Live template for Android Studio + freezed

JSON serialization/deserialization

freezed does not handle that at all, but it delegates all to work to json_serializable. For simple objects, all we have to do is to add another factory called fromJson:

JSON (de)serialization using freezed

That’s it, a .toJson() and .fromJson() method will be created.

For built_value, it’s a bit more work:

JSON (de)serialization using built_value

You have to create a Serializer, and need to create the toJson() and from() methods that are using that serializer yourself. Furthermore, you have to create a global Serializers object, where you need to register all root-types that you want to serialize (omitted here)

Enums

I don’t like “Stringly-Typed” applications or magic constants, I like to use enums for fields that can only contain a handful of possible values.

How do built_value and freezed+json_serializable handle these?

freezed:

enum (de)serialization using freezed (with json_serializable)

built_value:

enum (de)serialization using built_value

freezed with json_serializable supports dart enums, which require a lot less boilerplate.

However, built_value offers a nice feature that json_serializable doesn’t:

You can specify a fallback value in case the server adds a new field that the app does not support yet. In json_serializable, how have to handle this an the object that owns the enum, using something like:

enum deserialization with fallback using freezed

But if you have the same enum in multiple objects, you need to specify the fallback in each of them.

Unions

freezed:

Unions are a first-class feature of freezed and are a breeze to use:

unions with freezed

That’s it! You can access sharedValue without checking the concrete type, and freezed generates nice methods for you to handle all possible types:

built_value does not really offer any support for union types.

The closest thing you can do is:

But you still have to use the is operator to get the concrete type of the union.

Updates

Of course, even if you use immutable objects, we want to update them.

freezed offers a simple copyWith method for each object:

updating a field with freezed

built_value uses a mutable builder-object that is used to create a new model:

updating a field with built_value

Slightly more boilerplate, but built_value has a nicer syntax if you want to change a value that’s nested within several objects:

update a nested object using built_value

In freezed, the syntax would be:

built_value creates mutable builders for all fields by default if you update objects, which enables this more concise syntax. This comes with a performance penalty for big, nested objects. This feature can be disabled, though (at the cost of a more verbose syntax for updates).

Handling Collections

When using immutable data classes, you typically do not want to use the mutable collections from the standard library, otherwise, you lose many of the benefits and immutability of your objects is not guaranteed anymore. If you do use the collections from the standard library, you have the following choices:

Mutate them and not rely on immutability

Don’t mutate them, rather making sure you copy them every time instead, maybe wrapping them in unmodifiable views. But that requires discipline and is error-prone.

freezed does not limit your choice here in principle. It supports both the collections from the standard library (and compares them using DeepCollectionEquality) and custom immutable collections like built_collection or kt.dart.

However, json_serializable is more limited here. Out of the box, json_serializable only supports standards lists. You can specify Converters, but you have to write a new Converter for every type that you want to serialize (e.g. ImmutableList<MyModel> and ImmutableList<MyOtherModel> ). Also, you have to annotate every list-field with this converter:

Using freezed on models with custom collections that need to be serializable adds a big chunk of boilerplate to your code, unfortunately, negating most, if not all of the saved boilerplate from built_value.

It might be possible to avoid this boilerplate by creating a custom TypeHelper for json_serializable - However, there is no documentation on how to do this and it seems to involve writing a custom builder that runs the code generation- might be worth to do in the long run, but seems to be not that easy to setup.

Edit: I wrote an experimental builder for json_serializable that supports immutable collections. Check it out!

Small stuff

I̶t̶’̶s̶ ̶u̶s̶e̶f̶u̶l̶ ̶t̶o̶ ̶s̶e̶t̶ ̶d̶e̶f̶a̶u̶l̶t̶ ̶v̶a̶l̶u̶e̶s̶ ̶o̶n̶ ̶o̶b̶j̶e̶c̶t̶s̶.̶ ̶f̶r̶e̶e̶z̶e̶d̶ ̶d̶o̶e̶s̶ ̶n̶o̶t̶ ̶s̶u̶p̶p̶o̶r̶t̶ ̶t̶h̶i̶s̶ ̶a̶t̶ ̶t̶h̶e̶ ̶m̶o̶m̶e̶n̶t̶,̶ ̶b̶u̶i̶l̶t̶_̶v̶a̶l̶u̶e̶ ̶d̶o̶e̶s̶.̶

b̶u̶i̶l̶t̶_̶v̶a̶l̶u̶e̶ ̶s̶u̶p̶p̶o̶r̶t̶s̶ ̶c̶o̶m̶p̶u̶t̶e̶d̶ ̶p̶r̶o̶p̶e̶r̶t̶i̶e̶s̶ ̶t̶h̶a̶t̶ ̶c̶a̶n̶ ̶b̶e̶ ̶m̶e̶m̶o̶i̶z̶e̶d̶;̶ ̶f̶r̶e̶e̶z̶e̶d̶ ̶d̶o̶e̶s̶ ̶n̶o̶t̶ ̶s̶u̶p̶p̶o̶r̶t̶ ̶c̶o̶m̶p̶u̶t̶e̶d̶ ̶p̶r̶o̶p̶e̶r̶t̶i̶e̶s̶ ̶d̶i̶r̶e̶c̶t̶l̶y̶,̶ ̶b̶u̶t̶ ̶y̶o̶u̶ ̶c̶a̶n̶ ̶w̶r̶i̶t̶e̶ ̶a̶n̶ ̶e̶x̶t̶e̶n̶s̶i̶o̶n̶ ̶o̶n̶ ̶y̶o̶u̶r̶ ̶m̶o̶d̶e̶l̶ ̶o̶b̶j̶e̶c̶t̶.̶

Edit: Remi is almost faster adding new features to freezed than I am writing this article! freezed now supports computed, cached properties and default values, too!

built_value lets you customize which fields should be compared when generating the == method, freezed currently always compares all instance fields.

Code Generation Performance

After converting our project to freezed, code generation takes about 40 seconds on my development machine, which is a nice improvement from 60 seconds before. Additionally, it seems that caching works better with freezed, after some changes the build_runner typically only needs a few seconds.

Conclusion

freezed is an amazing library that employs clever tricks to offer data classes and unions with almost zero boilerplate.

However, there are still a few features missing, especially the missing support for immutable collections when using json_serializable is a big disadvantage for us (although technically not the fault of freezed).

freezed was only released a few days ago, I’m sure it will catch up fast. For new projects, I would definitely choose freezed over built_value; In my opinion, the long-term prospects of freezed are better.