Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

Follow publication

Using Freezed and JsonSerializable for Clean, Immutable Data Models in Flutter.

Hemant Jam
Stackademic
Published in
4 min readOct 29, 2024

--

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:

  1. Immutable — Can’t have bugs from accidentally mutating objects, right?
  2. Serializable — If you’re dealing with APIs, you need to convert objects to JSON and back.
  3. 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 and json_annotation go in dependencies because they’re needed for runtime, while freezed and json_serializable go in dev_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:

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Published in Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

Written by Hemant Jam

Passionate mobile developer specializing in Flutter. Sharing insights on Flutter.

No responses yet

Write a response