JSON to Flutter Model: Freezed vs json_serializable vs Plain Dart
Understand common Flutter model styles and when to use plain Dart, json_serializable, Freezed, or Equatable.
Plain Dart models: the simplest approach
Plain Dart models are handwritten or generated class definitions with no external package dependencies. They include a constructor, a fromJson factory, and a toJson method written in pure Dart. Plain models are the right choice for small to medium Flutter projects, quick prototypes, developer tools, or teams that want generated code that is easy to read and modify without understanding a code generation framework. The main limitation is that plain models require manual updates when fields change, and value equality comparison requires writing the == operator and hashCode by hand, which is tedious and error-prone at scale.
json_serializable models: generated parsing with annotations
json_serializable is a popular Flutter package that uses build_runner to generate the fromJson and toJson implementations automatically from annotations you add to your model class. You write the class with the @JsonSerializable annotation and the build_runner command generates a companion .g.dart file with all the parsing code. The main benefits are that model classes stay clean and readable, field name remapping with @JsonKey is straightforward, and regenerating after an API change only requires updating the class and running build_runner. json_serializable is the right choice for most production Flutter projects with structured API integration.
Freezed models: immutable models with full value semantics
Freezed is a code generation package that creates deeply immutable model classes with built-in copyWith, equality comparison, hashCode, and toString. Freezed models are the standard choice for Flutter apps using Riverpod, Bloc, or any state management pattern that requires immutable state objects. The copyWith method generated by Freezed allows you to create modified copies of a model instance — for example, updating a single field — without mutating the original, which is essential for predictable state updates. Freezed also supports union types and sealed classes for modeling multiple states in the same object, which is powerful for loading, loaded, and error states in asynchronous data flows.
Choosing the right model approach for your Flutter project
Use plain Dart for simplicity when you want dependency-free generated code that any Dart developer can read immediately without knowing a framework. Use json_serializable when you need automatic parsing code generation, want cleaner model class files, and are working on a project that already uses build_runner. Use Freezed when your project requires immutable state management, value equality, or sealed union types — particularly if you are using Riverpod or Bloc. In most production Flutter projects that will grow over time, starting with either json_serializable or Freezed from the beginning is better than refactoring away from plain Dart models later.
Flutter model choices should be driven by how the state will be used, not by which package looks popular in a sample project. Plain Dart is fine when the structure is small and stable, while generated models are better when you expect repeated API changes, nullable fields, or immutable state handling. The most reliable workflow is to start from real data, keep the model layer boring, and reserve advanced helpers for places where they actually reduce future work. Simple code that matches the API well is usually easier to maintain than a clever model that is hard to read later.
After generating the model, make one quick pass for naming, wrapper objects, and optional fields. That last review is where you usually catch the mismatches that would otherwise become deserialization bugs in the app.
Frequently asked questions