๐Ÿ“ฆ Lesson 9: JSON & Data Models

โฑ 35-45 min ๐Ÿ“Š Intermediate ๐Ÿ“– In-depth tutorial ๐Ÿ”— Official Source
0

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.

๐ŸŒ API Response
Raw JSON String
โ†’
๐Ÿ”„ jsonDecode()
Map<String, dynamic>
โ†’
๐Ÿ—๏ธ fromJson()
Typed Model Object
โ†’
โœจ Your App
Safe, typed data
1

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 TypeDart TypeExample
StringString"hello"
Numberint / double42, 3.14
Booleanbooltrue, false
nullnullnull
Object {}Map<String, dynamic>{"key":"value"}
Array []List<dynamic>[1, 2, 3]
โš ๏ธ The dynamic Problem: 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

Parsed output will appear here...
2

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

1 class User โ€” a typed data model
3-6 Typed properties โ€” no more dynamic!
5 String? โ€” nullable, may not be in JSON
9-14 Regular constructor for manual creation
17-24 factory ... fromJson() โ€” the decoder
20-21 as casts dynamic to typed value
28-33 toJson() โ€” encodes back to Map
31-32 if in map โ€” skips null values

๐Ÿ—๏ธ Build Your Own Model

๐Ÿ“„ Generated Code

class Product { ... }
3

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}');
  }
}
๐Ÿ“Œ Key Pattern: Nested Models: Notice how 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.
4

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

โ‘  Extracts id โ€” fails if missing or not int
โ‘ก Extracts name โ€” fails if missing or not String
โ‘ข String? โ€” nullable, so absent is OK
โ‘ฃ All checks passed โ€” create the object
_ _ is the wildcard โ€” catches everything else
Extra fields in JSON are ignored โ€” not an error!

๐Ÿงช Test Pattern Matching with Different JSON Inputs

Select a test case to see the result...
5

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;
}
๐Ÿ“Œ The fromJson chain for nested models: 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.
6

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

Enter JSON to test round-trip...

๐ŸŽฏ JSON & Data Models Quiz

Score: 0/5 | Hints: 3

Loading question...

๐Ÿš€

Practice Projects

Apply what you've learned by building these hands-on projects.

๐Ÿ“ฆ Project 1 ๐Ÿ“ฆ Project 2