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
numand converted. JSON has no separate int and double:4.8arrives as a double, but5arrives as an int. Casting5directlyas doublecrashes.(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
· 5 min read
Flutter Splash Screen and App Icon: The Complete Setup Guide
Replace the default Flutter icon and white launch screen. Set up a native splash screen with flutter_native_splash, including Android 12, and add every iOS and Android app icon size the easy way.
· 5 min read
Responsive Flutter Layouts: One App for Phone, Tablet and Web
Make your Flutter UI responsive with MediaQuery, LayoutBuilder, breakpoints and adaptive navigation. Build one layout that looks right on phones, tablets, desktop and the web.
· 5 min read
Flutter Animations for Beginners: AnimatedContainer, AnimatedSwitcher and Hero
Learn Flutter animations the easy way. Use AnimatedContainer, AnimatedOpacity, AnimatedSwitcher, TweenAnimationBuilder and Hero to make your UI feel smooth, with no AnimationController needed.