Parse JSON in Flutter: Create Dart Model Classes (fromJson and toJson)

Stop using Map<String, dynamic> everywhere. Learn how to turn JSON into Dart model classes with fromJson and toJson, handle nested objects, lists and null values, and generate models instantly.

By Yaqoob Developer · · 5 min read

  • #Flutter
  • #Dart
  • #JSON
  • #API
  • #Beginners

When you fetch data from an API, you get JSON. The quick way to use it is to decode it and read keys directly:

final data = jsonDecode(response.body);
Text(data['user']['name']);

This works until it doesn't. One typo in a key ('nmae') and you get null at runtime with no warning. Your editor can't autocomplete anything. And when the API changes, you have to search every screen for the old key.

The fix is a model class: a Dart class that describes the JSON, with a fromJson constructor to create it and a toJson method to send it back.

In this guide you will learn:

  • How to write a model class with fromJson and toJson
  • How to handle nested objects and lists
  • How to deal with null and missing fields
  • How to parse an API response into a list of models
  • How to generate models instantly from any JSON

If you have not fetched data from an API yet, start with fetch API data in Flutter with http.

The JSON we will parse

{
  "id": 7,
  "name": "Sara Ahmed",
  "email": "sara@example.com",
  "is_verified": true,
  "rating": 4.8,
  "address": {
    "city": "Lahore",
    "country": "Pakistan"
  },
  "skills": ["Flutter", "Firebase", "UI Design"],
  "avatar_url": null
}

It has every common case: numbers, text, a boolean, a nested object, a list, and a field that can be null.

Step 1: A simple model

Start with the top-level fields:

class User {
  const User({
    required this.id,
    required this.name,
    required this.email,
    required this.isVerified,
    required this.rating,
  });

  final int id;
  final String name;
  final String email;
  final bool isVerified;
  final double rating;

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
      isVerified: json['is_verified'] as bool,
      rating: (json['rating'] as num).toDouble(),
    );
  }

  Map<String, dynamic> toJson() => {
        'id': id,
        'name': name,
        'email': email,
        'is_verified': isVerified,
        'rating': rating,
      };
}

Things to notice:

  • Fields are final. A model describes data; it should not change by accident.
  • JSON uses snake_case (is_verified), Dart uses camelCase (isVerified). The model is the one place where you translate.
  • rating is read as num and converted. JSON has no separate int and double: 4.8 arrives as a double, but 5 arrives as an int. Casting 5 directly as double crashes. (json['rating'] as num).toDouble() handles both.

Step 2: Nested objects

address is an object of its own, so it gets its own class:

class Address {
  const Address({required this.city, required this.country});

  final String city;
  final String country;

  factory Address.fromJson(Map<String, dynamic> json) => Address(
        city: json['city'] as String,
        country: json['country'] as String,
      );

  Map<String, dynamic> toJson() => {'city': city, 'country': country};
}

In User, add the field and parse it with Address.fromJson:

final Address address;

// in fromJson:
address: Address.fromJson(json['address'] as Map<String, dynamic>),

// in toJson:
'address': address.toJson(),

Step 3: Lists

skills is a list of strings. Decoded JSON lists are List<dynamic>, so convert them:

final List<String> skills;

// in fromJson:
skills: List<String>.from(json['skills'] as List),

For a list of objects, map each item through its fromJson:

final List<Order> orders;

// in fromJson:
orders: (json['orders'] as List)
    .map((e) => Order.fromJson(e as Map<String, dynamic>))
    .toList(),

Step 4: Null and missing fields

avatar_url is null for this user. Make the Dart type nullable with ?:

final String? avatarUrl;

// in fromJson:
avatarUrl: json['avatar_url'] as String?,

For fields the API sometimes leaves out completely, give a default with ??:

skills: List<String>.from(json['skills'] as List? ?? const []),
rating: (json['rating'] as num?)?.toDouble() ?? 0,

Decide per field: is a missing value a bug (crash early, so you notice) or normal (use a default)? Required data like id should not have a default.

Step 5: Parse an API response

Put it together with the http package:

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<List<User>> fetchUsers() async {
  final response = await http.get(Uri.parse('https://api.example.com/users'));
  if (response.statusCode != 200) {
    throw Exception('Failed to load users (${response.statusCode})');
  }

  final list = jsonDecode(response.body) as List;
  return list
      .map((e) => User.fromJson(e as Map<String, dynamic>))
      .toList();
}

Now every screen works with List<User>. Your editor autocompletes user.address.city, and a typo is a compile error instead of a runtime null.

Step 6: Generate models in seconds

Writing models by hand is good practice, but for a real API with 30 fields it is slow and easy to get wrong. I built a free tool for this: the JSON to Dart converter.

Paste any JSON and it generates:

  • A class for every nested object
  • fromJson and toJson with the right casts
  • Nullable types for null values
  • camelCase field names mapped to the original keys

It runs in your browser, so your API data is never uploaded.

What about json_serializable?

For big projects, many teams use the json_serializable package. You write the fields with an annotation and a build step generates fromJson and toJson for you. It is great when models change often.

For learning and for small apps, handwritten (or generated-once) models are simpler: no build_runner, no generated files to keep in sync. Start there and switch when it hurts.

Common mistakes

type 'int' is not a subtype of type 'double' You cast a number as double and the JSON had a whole number. Use (json['x'] as num).toDouble().

type 'List<dynamic>' is not a subtype of type 'List<String>' You assigned a decoded list directly. Use List<String>.from(...).

type 'Null' is not a subtype of type 'String' The field was null or missing. Make the type nullable or add a default.

Calling jsonDecode twice. If you already decoded the body, passing the result to jsonDecode again fails. Decode once, at the edge of your app.

Summary

  • Turn JSON into model classes with a fromJson factory and a toJson method.
  • Translate snake_case keys to camelCase fields inside the model.
  • Read numbers as num, lists with List.from, and nested objects with their own fromJson.
  • Use nullable types and defaults for missing data.
  • Save time with the free JSON to Dart tool.

Keep reading

More articles