Skip to content

Intermediate · Networking, storage and Firebase · topic 32 of 50 · 30 min

JSON models

Explanation

Do not pass raw Map<String, dynamic> around your app. Convert JSON into model classes with a factory fromJson constructor and a toJson method, with null-safe parsing and sensible defaults. For many models, json_serializable with build_runner generates this code from annotations. Validate data at the edge, so bad server data shows an error instead of crashing deep inside a widget.

Why model classes?

jsonDecode gives you maps and lists of dynamic values. Writing post['title'] everywhere is fragile: a typo in a key or a wrong type only fails at run time, maybe on a user's phone. A model class turns JSON into a typed Dart object once, at the edge of your app. After that, the compiler checks every use of order.total, and editors can autocomplete fields.

A hand-written model

json
{"id": 9081, "item": "Paneer tikka", "qty": 2, "price_paise": 32000, "coupon": null}
A JSON object from an orders API.
dart
class Order {
  const Order({required this.id, required this.item, required this.qty, required this.pricePaise, this.coupon});

  final int id;
  final String item;
  final int qty;
  final int pricePaise;
  final String? coupon;

  factory Order.fromJson(Map<String, dynamic> json) => Order(
        id: json['id'] as int,
        item: json['item'] as String,
        qty: (json['qty'] as int?) ?? 1,             // default if missing
        pricePaise: json['price_paise'] as int,
        coupon: json['coupon'] as String?,           // nullable field
      );

  Map<String, dynamic> toJson() => {
        'id': id,
        'item': item,
        'qty': qty,
        'price_paise': pricePaise,
        'coupon': coupon,
      };

  int get totalPaise => qty * pricePaise;
}
fromJson maps snake_case keys to camelCase fields; toJson does the reverse for sending data.
dart
final list = (jsonDecode(response.body) as List<dynamic>)
    .map((e) => Order.fromJson(e as Map<String, dynamic>))
    .toList();
Decoding a JSON array into List<Order>.

Notice the naming: the API uses price_paise (snake_case) while Dart uses pricePaise (camelCase). The fromJson and toJson methods are the single place where the two styles meet, so the rest of your code never sees the server's naming.

Handling bad data

Servers change, and bugs happen. A field might be missing, null, or a string instead of a number. With as int, a wrong type throws a TypeError at the moment of parsing, which is good: it happens in one predictable place. Catch errors around parsing in your repository and turn them into a clear app error ("Could not read orders"). Use nullable types and defaults only for fields that are truly optional.

Write a small unit test for each fromJson with a real sample response. It catches mistakes cheaply.

Generating code with json_serializable

Writing fromJson and toJson for twenty models is boring and error-prone. json_serializable generates them from annotations. build_runner is the tool that runs code generators.

bash
flutter pub add json_annotation
flutter pub add --dev build_runner json_serializable
json_annotation is used in your code; the generator tools are dev dependencies.
dart
import 'package:json_annotation/json_annotation.dart';

part 'order.g.dart';                            // the generated file

@JsonSerializable(fieldRename: FieldRename.snake)
class Order {
  Order({required this.id, required this.item, this.qty = 1, required this.pricePaise, this.coupon});
  final int id;
  final String item;
  final int qty;
  final int pricePaise;
  final String? coupon;

  factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
  Map<String, dynamic> toJson() => _$OrderToJson(this);
}
fieldRename: snake maps pricePaise to price_paise automatically.
bash
dart run build_runner build --delete-conflicting-outputs
dart run build_runner watch          # regenerate on every save
Run the generator after creating or changing models.

Other options exist, such as freezed (immutable classes with copyWith and unions) built on the same generator system. Start with hand-written models to understand them, then use generation when the number of models grows.

Think of it like this: A model class is like the passport office converting a messy application form into a clean, standard passport. The messy form (JSON) is checked once at the counter; after that, every official reads the same well-formatted document and knows exactly where to find each detail.

Common mistakes

  • Using Map<String, dynamic> throughout the UI instead of models.
  • Making every field nullable to avoid crashes: then every widget must handle null.
  • Forgetting part 'order.g.dart' or not running build_runner: "Undefined name _$OrderFromJson".
  • Using double for money from APIs. Prefer integer paise or a decimal type agreed with the backend.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

JSON models · 1/6

JSON → Model

Convert JSON into model classes once, at the edge of the app. Do not pass raw maps around.

Prefer a video by a teacher? Find free videos on “flutter json serialization fromjson tojson json_serializable build_runner” on YouTube ↗

In real life: Train search results

A travel app receives train search results as JSON with fields like train_no, name, departs and available_seats, where the fare may be missing for waitlisted classes. A Train model with fromJson converts each result, with a nullable fare. The UI shows "Fare on request" when fare is null, and a parsing error shows "Could not load results" instead of crashing.

dart
class Train {
  const Train(this.number, this.name, this.fare);
  final int number;
  final String name;
  final int? fare;

  factory Train.fromJson(Map<String, dynamic> j) =>
      Train(j['train_no'] as int, j['name'] as String, j['fare'] as int?);
}

String fareText(Train t) => t.fare == null ? 'Fare on request' : 'Rs ${t.fare}';
Optional data is modelled as nullable on purpose.

Practice (0/3 done)

Task 1 · Question

JSON is {"name": "Asha"} and your fromJson does json['age'] as int. What happens?

Task 2 · Question

You see "Undefined name _$OrderFromJson". What is missing?

Task 3 · Practice

Write a Product model by hand for this JSON: {"id": 7, "name": "Kurta", "price_paise": 89900, "sizes": ["S", "M", "L"], "discount": null}. Include fromJson, toJson and a priceText getter. Parse a JSON array of three products and print each name and price. Then generate the same model with json_serializable.

Quick quiz

Question 1

1.Why convert JSON into model classes?

Question 2

2.Which tool runs code generators like json_serializable?

Question 3

3.Where should json_serializable and build_runner be listed?

Question 4

4.What does toJson do?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Why convert JSON into model classes?

Answer: So the compiler checks field names and types everywhere else

Type safety after one parsing step.

Which tool runs code generators like json_serializable?

Answer: build_runner

dart run build_runner build.

Where should json_serializable and build_runner be listed?

Answer: dev_dependencies

They are only needed while developing.

What does toJson do?

Answer: Turns a model into a Map ready for jsonEncode

Used when sending data.

Why do Flutter widgets often use const constructors?Related

Answer: Constant widgets are created once and can be skipped during rebuilds

const saves work at run time.

From Classes and constructors

How do you make a JSON field optional in the model?Related

Answer: Give it a nullable type and/or a default value

Defaults and nullability cover missing fields.

From JSON models and handling errors

JSON.parse("{'x': 1}") throws which error?Related

Answer: SyntaxError

Single quotes are not valid JSON, so parsing fails with SyntaxError.

From JSON: data in and out

What happens if a handler throws and nothing catches it?Related

Answer: The request can hang or the server can crash

Uncaught errors can stop the request or the process. Wrap handlers in try and catch.

From Handling errors well

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Networking, storage and Firebase” before its exam: Local storage, Firebase in Flutter.
  2. Module 8 exam: Networking, storage and Firebase · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Animations and performance. 50 topics left in the course before the final exam.

← Previous: HTTP requests and FutureBuilder