Why I Added freezed to My Flutter App — Unifying Model Patterns
Why I did it
My solo app Book Log has 4 entity models: AuthUser, Book, BookSearchResult, Sentence. Each had a different style of fromJson — some had copyWith and some didn't. No consistent pattern.
Inconsistent model classes can lead to unexpected bugs. I wanted every model to follow the same structure. With freezed, adding a new model requires no guesswork.
What I liked
-
Immutable by default —
const factorymakes it impossible to accidentally mutate object values. -
Auto-generated
copyWith— no need to write it manually when you only want to change part of an object. -
Explicit key mapping with
@JsonKey— server uses snake_case, Dart uses camelCase. The mapping is visible right in the code.
@freezed
abstract class Sentence with _$Sentence {
const factory Sentence({
required String id,
@JsonKey(name: 'book_id') required String bookId,
@JsonKey(name: 'page_number') int? pageNumber,
@JsonKey(name: 'created_at') required DateTime createdAt,
}) = _Sentence;
}
freezed also auto-generates == and hashCode for object comparison, but I haven't needed that yet so I'm not actively using it.
Didn't apply auto-generated fromJson to every model
AuthUser — auto-generated
factory AuthUser.fromJson(Map<String, dynamic> json) =>
_$AuthUserFromJson(json);
This data comes from my own server. I can trust the response structure. Keys and types are clean. Auto-generated fromJson works fine here.
Book — kept custom fromJson
Book data comes from the Aladin public API. I read the official docs, but I couldn't fully trust an external API's response. So I kept the custom fromJson.
factory Book.fromJson(Map<String, dynamic> json) {
final id = json['id']?.toString() ?? '';
if (id.isEmpty) throw Exception('Book id is empty');
return Book(
id: id,
rating: json['rating'] == null
? null
: (json['rating'] is int
? json['rating'] as int
: int.tryParse(json['rating'].toString())),
status: ReadingStatusX.fromApiValue(json['status']?.toString() ?? ''),
// ...
);
}
Three reasons I kept it custom:
-
idvalidation —idis referenced by every feature in the app. Book detail, sentence saving, representative sentence — all depend on it. A missingidcauses critical failure across the entire app. That's why the exception is there. -
Type coercion — external API sometimes sends
ratingasint, sometimes asString. Neededint.tryParsefallback. -
ReadingStatus — this is a status enum created during app development. The server sends
'wish','reading','done'as strings, and the conversion logic had to be preserved.
Wrap-up
The goal of adding freezed was pattern unification. Immutability, auto-generated copyWith, and explicit key mapping with @JsonKey were the practical wins.
Top comments (0)