๐ฆ Lesson 9: JSON & Data Models
The Data Pipeline: JSON โ Dart Objects
๐ง JSON is just text. Your app needs typed Dart objects.
The journey from raw API response to clean, type-safe Dart data involves: fetching text โ decoding JSON โ mapping to model classes. Every Dart app that talks to an API needs to master this pipeline.
Raw JSON String
Map<String, dynamic>
Typed Model Object
Safe, typed data
JSON & dart:convert โ The Foundation
๐ง JSON (JavaScript Object Notation) is the universal data format of the web
Dart's dart:convert library provides jsonDecode() to parse JSON strings into Dart objects, and jsonEncode() to convert Dart objects back to JSON strings.
JSON โ Dart Type Mapping
import 'dart:convert';
void main() {
// JSON String โ Dart Object
final jsonString = '{"name": "Dart", "version": 3.12, "isAwesome": true}';
final decoded = jsonDecode(jsonString);
// decoded is Map<String, dynamic>
print(decoded['name']); // Dart
print(decoded['version']); // 3.12
print(decoded['isAwesome']); // true
// Dart Object โ JSON String
final encoded = jsonEncode(decoded);
print(encoded); // {"name":"Dart","version":3.12,"isAwesome":true}
}
| JSON Type | Dart Type | Example |
|---|---|---|
| String | String | "hello" |
| Number | int / double | 42, 3.14 |
| Boolean | bool | true, false |
| null | null | null |
| Object {} | Map<String, dynamic> | {"key":"value"} |
| Array [] | List<dynamic> | [1, 2, 3] |
jsonDecode() returns Map<String, dynamic>. The dynamic type means no compiler checks โ you can accidentally treat a number as a string and crash at runtime. That's why we create typed model classes.
๐ฌ Try It: Parse and Explore JSON
Building Data Model Classes โ fromJson() & toJson()
๐ง A data model is a Dart class that mirrors a JSON structure with typed properties
The standard pattern is to create a factory constructor named fromJson() that takes a Map<String, dynamic> and returns a typed instance, and a toJson() method that does the reverse.
Complete Model Class โ Line by Line
class User {
// Properties โ typed, not dynamic!
final int id;
final String name;
final String? email; // Nullable โ might not exist in JSON
final int? age; // Nullable โ optional field
// Regular constructor
User({
required this.id,
required this.name,
this.email,
this.age,
});
// Factory: JSON โ User
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String?,
age: json['age'] as int?,
);
}
// Method: User โ JSON
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
if (email != null) 'email': email,
if (age != null) 'age': age,
};
}
๐ Breakdown
class User โ a typed data modeldynamic!String? โ nullable, may not be in JSONfactory ... fromJson() โ the decoderas casts dynamic to typed valuetoJson() โ encodes back to Mapif in map โ skips null values๐๏ธ Build Your Own Model
๐ Generated Code
class Product { ... }
The Official Tutorial Example โ Wikipedia API
๐ This example comes directly from the official Dart tutorial
The official tutorial uses the Wikipedia API to demonstrate real-world JSON handling. Here's the complete example, explained step by step.
๐ง The Wikipedia API returns JSON like this (simplified):
{
"batchcomplete": "",
"query": {
"searchinfo": { "totalhits": 123 },
"search": [
{
"ns": 0,
"title": "Dart (programming language)",
"pageid": 12345,
"snippet": "Dart is a programming language...",
"timestamp": "2024-01-01T00:00:00Z"
}
]
}
}
Step 1: Create the innermost model โ SearchResult
This represents a single search result from the "search" array.
class SearchResult {
final int pageid;
final String title;
final String snippet;
SearchResult({
required this.pageid,
required this.title,
required this.snippet,
});
factory SearchResult.fromJson(Map<String, dynamic> json) {
return SearchResult(
pageid: json['pageid'] as int,
title: json['title'] as String,
snippet: json['snippet'] as String,
);
}
}
Step 2: Create the wrapper model โ WikipediaResponse
This represents the entire JSON response, containing the list of search results.
class WikipediaResponse {
final List<SearchResult> results;
WikipediaResponse({required this.results});
factory WikipediaResponse.fromJson(Map<String, dynamic> json) {
// Navigate nested structure: json โ query โ search
final searchList = json['query']['search'] as List<dynamic>;
// Map each JSON object to a SearchResult
final results = searchList
.map((item) => SearchResult.fromJson(item as Map<String, dynamic>))
.toList();
return WikipediaResponse(results: results);
}
}
Step 3: Use it in your app
import 'dart:convert';
void main() {
// Imagine this came from an HTTP request
final jsonString = '{ "query": { "search": [...] } }';
// 1. Decode the JSON string
final rawJson = jsonDecode(jsonString) as Map<String, dynamic>;
// 2. Convert to typed model
final response = WikipediaResponse.fromJson(rawJson);
// 3. Use typed data safely!
for (final result in response.results) {
print('๐ \${result.title}');
print(' \${result.snippet}');
}
}
WikipediaResponse.fromJson() navigates json['query']['search'] to reach the array, then uses .map() to convert each item using SearchResult.fromJson(). Each JSON nesting level gets its own Dart class.
Pattern Matching for JSON Validation (Dart 3+)
๐ง Pattern matching validates JSON structure AND extracts values in one step
Instead of manually checking each field exists and has the right type, a switch expression with map patterns does both simultaneously. If the structure doesn't match, you get a clear error instead of a cryptic null exception.
โ Traditional (No Validation)
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
);
// If 'id' is missing โ runtime null error!
// If 'name' is a number โ runtime cast error!
}
โ Pattern Matching (Validates + Extracts)
factory User.fromJson(Map<String, Object?> json) {
return switch (json) {
{
'id': final int id,
'name': final String name,
'email': final String? email,
} => User(id: id, name: name, email: email),
_ => throw FormatException('Invalid JSON: \$json'),
};
}
How the Pattern Match Works
switch (json) {
// Pattern: Must have 'id' as int AND 'name' as String
{
'id': final int id, // โ Check key exists & is int
'name': final String name, // โก Check key exists & is String
'email': final String? email, // โข Optional โ may be absent or null
} => User(id: id, name: name, email: email), // โฃ Create with extracted values
// Wildcard: catches anything that doesn't match
_ => throw FormatException('Invalid user JSON: \$json'),
}
๐ What Each Part Does
id โ fails if missing or not intname โ fails if missing or not StringString? โ nullable, so absent is OK_ is the wildcard โ catches everything else๐งช Test Pattern Matching with Different JSON Inputs
Handling Nested JSON Structures
๐ง Real APIs send deeply nested JSON. Each nesting level becomes its own model class.
The rule is simple: if you see { } in JSON, create a class for it. If you see [ ], use a List<SomeClass>. The classes reference each other just like the JSON structure references itself.
Nested JSON โ Nested Models
๐ฅ JSON Structure
{
"user": {
"id": 1,
"profile": {
"name": "Alice",
"address": {
"street": "123 Main St",
"city": "Techville"
}
},
"posts": [
{ "id": 1, "title": "Hello" },
{ "id": 2, "title": "World" }
]
}
}
๐ค Corresponding Dart Models
class User {
final int id;
final Profile profile;
final List<Post> posts;
}
class Profile {
final String name;
final Address address;
}
class Address {
final String street;
final String city;
}
class Post {
final int id;
final String title;
}
User.fromJson() calls Profile.fromJson(), which calls Address.fromJson(), and uses .map() to convert the posts array with Post.fromJson(). Each class handles its own level of nesting.
Serialization โ Object Back to JSON
๐ง toJson() completes the round-trip: JSON โ Object โ JSON
Every model class should have a toJson() method that returns a Map<String, dynamic>. Nested models should call toJson() on their children. Use collection-if to skip null values for cleaner output.
The Complete Round-Trip
// JSON String โ Dart Object
final raw = jsonDecode(jsonString);
final user = User.fromJson(raw);
// Dart Object โ JSON String
final map = user.toJson(); // Object โ Map
final json = jsonEncode(map); // Map โ String
// Nested toJson example
Map<String, dynamic> toJson() => {
'id': id,
'profile': profile.toJson(), // Call toJson on nested model
'posts': posts.map((p) => p.toJson()).toList(), // Map each item
if (email != null) 'email': email, // Skip null values
};
๐ Round-Trip Demo
๐ฏ JSON & Data Models Quiz
Score: 0/5 | Hints: 3
Loading question...
Practice Projects
Apply what you've learned by building these hands-on projects.