
Using Freezed and JsonSerializable for Clean, Immutable Data Models in Flutter.
Alright, let’s cut to the chase: dealing with data models in Flutter can get messy fast. If you’ve ever thought, “There’s got to be a better way than manually writing constructors, copyWith
methods, and JSON serialization code,” you’re in luck! Today, we’re diving into Freezed and JsonSerializable – two powerful packages that work together to simplify your data models, keep them clean, and make them immutable. Ready? Let’s go!
Why Freezed and JsonSerializable?
Imagine this: you’re building an app with loads of data models, and you need each model to be:
- Immutable — Can’t have bugs from accidentally mutating objects, right?
- Serializable — If you’re dealing with APIs, you need to convert objects to JSON and back.
- Readable and Maintainable — Who has time to write all that boilerplate code?
Enter Freezed and JsonSerializable. Freezed takes care of immutability, copyWith
, and much more, while JsonSerializable automates JSON parsing. Together, they’re the dream team for clean, functional data classes in Flutter.
Step 1: Setting Up Freezed and JsonSerializable
Let’s get our project ready. Open your pubspec.yaml
file and add the following dependencies:
dependencies:
freezed_annotation: ^2.0.0
json_annotation: ^4.8.0
dev_dependencies:
build_runner: ^2.1.7
freezed: ^2.0.0
json_serializable: ^6.1.5
Run flutter pub get
to install these packages.
Quick Note:
freezed_annotation
andjson_annotation
go independencies
because they’re needed for runtime, whilefreezed
andjson_serializable
go indev_dependencies
as they’re used only during code generation.
Step 2: Creating a Freezed Data Class
Example 1: Simple User Model
Let’s start with a simple User
model with fields for ID, name, and email. Create a user.dart
file in your lib
directory and set it up like this:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
int? id,
String? name,
String? email,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
What’s Happening Here?
@freezed
tells the Freezed package to take control of this class and generate boilerplate code.- We define a factory constructor, marking fields as nullable to avoid runtime issues.
fromJson
is used for deserialization – it’ll help when we get JSON data from an API.
Now, let’s generate the necessary code by running:
flutter pub run build_runner build --delete-conflicting-outputs
This generates two files: user.freezed.dart
and user.g.dart
, which contain all the juicy code Freezed and JsonSerializable just generated for us.
Step 3: Going Deeper with Freezed — Union Types and CopyWith
Example 2: Enhanced User with Profile Information
Imagine your app has a more complex data structure. For example, User
could have Profile
information, which includes bio
and profileImageUrl
. Let’s add that.
@freezed
class Profile with _$Profile {
const factory Profile({
String? bio,
String? profileImageUrl,
}) = _Profile;
factory Profile.fromJson(Map<String, dynamic> json) => _$ProfileFromJson(json);
}
Update the User
class:
@freezed
class User with _$User {
const factory User({
int? id,
String? name,
String? email,
Profile? profile, // Nested Profile data
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
Now run flutter pub run build_runner build
again. Freezed handles nesting beautifully and automatically generates copyWith
methods for updating individual fields without needing to recreate the whole object.
Step 4: Handling API Responses with Freezed and JsonSerializable
When working with APIs, you often encounter different response statuses: loading, success, error. Let’s create a model for these scenarios using Freezed’s union types.
Example 3: API Response States
Create an api_response.dart
file:
@freezed
class ApiResponse<T> with _$ApiResponse<T> {
const factory ApiResponse.loading() = Loading<T>;
const factory ApiResponse.success(T data) = Success<T>;
const factory ApiResponse.error(String message) = Error<T>;
}
Here’s how each type works:
loading()
– Represents a loading state.success()
– Contains the data when the API call is successful.error()
– Holds an error message if something goes wrong.
This union type makes it easy to handle different states of API responses elegantly.
Step 5: Advanced Usage — Custom JSON Serialization
Sometimes the API data doesn’t match our model structure. Let’s tackle that with custom JSON key mappings.
Example 4: Custom Keys in JSON
Assume the API returns user_id
instead of id
. You can use JsonSerializable’s @JsonKey
to map it correctly:
@freezed
class User with _$User {
const factory User({
@JsonKey(name: 'user_id') int? id, // Custom JSON key
String? name,
String? email,
Profile? profile,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
Run flutter pub run build_runner build
again to generate updated code. Now, the id
field maps to user_id
automatically during serialization and deserialization.
Step 6: Best Practices for Working with Freezed and JsonSerializable
1. Use Nullable Fields Wisely
Freezed doesn’t enforce null safety directly on fields. Be intentional about where fields should be nullable or required to avoid potential issues.
2. Keep Data Classes Pure
Avoid adding methods with side effects or dependencies (like HTTP requests) inside your Freezed classes. This ensures that your data classes remain pure and easy to test.
3. Optimize Serialization with JsonSerializable
Use JsonSerializable
options like explicitToJson
if you have nested classes that need special serialization handling.
4. Use Union Types for Clearer Logic
Union types allow you to better organize complex states, especially for API responses. They help you write more readable, maintainable code with fewer null checks.
Wrapping Up
Congratulations! You’ve just leveled up your Flutter data modeling skills with Freezed and JsonSerializable. By using Freezed for immutability and boilerplate reduction, and JsonSerializable for JSON parsing, your Flutter code is cleaner, more readable, and easier to maintain.
Whether you’re dealing with simple data models or complex nested structures, these tools save you time, reduce errors, and keep your codebase lean. Next time you’re setting up a new model, give Freezed and JsonSerializable a spin — you’ll wonder how you ever coded without them. Happy coding!
Stackademic 🎓
Thank you for reading until the end. Before you go:
- Please consider clapping and following the writer! 👏
- Follow us X | LinkedIn | YouTube | Discord | Newsletter | Podcast
- Create a free AI-powered blog on Differ.
- More content at Stackademic.com