DEV Community

koreanDev
koreanDev

Posted on

Flutter freezed + json_serializable — When to Use Auto-Generated fromJson and When Not To

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 defaultconst factory makes 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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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() ?? ''),
    // ...
  );
}
Enter fullscreen mode Exit fullscreen mode

Three reasons I kept it custom:

  1. id validationid is referenced by every feature in the app. Book detail, sentence saving, representative sentence — all depend on it. A missing id causes critical failure across the entire app. That's why the exception is there.
  2. Type coercion — external API sometimes sends rating as int, sometimes as String. Needed int.tryParse fallback.
  3. 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)