JSON
Understanding JSON — the universal data format for APIs and how to work with it in Flutter.
JSON (JavaScript Object Notation) is a lightweight data-interchange format that is easy for humans to read and write, and easy for machines to parse and generate. It's the most common format used by REST APIs to send and receive data.
🎯 Why JSON?
- Universal – Used by almost every API and programming language
- Human-readable – Easy to understand and debug
- Lightweight – Minimal overhead compared to XML
- Language-independent – Works with Dart, JavaScript, Python, etc.
JSON represents data as key-value pairs and ordered lists . Here's a simple example:
{
"userId"
:
1
,
"id"
:
1
,
"title"
:
"A sample album"
,
"isActive"
:
true
,
"tags"
: [
"music"
,
"art"
,
"design"
],
"owner"
: {
"name"
:
"John Doe"
,
"email"
:
"john@example.com"
}
}
JSON supports a small set of data types. Understanding these is essential for parsing JSON in Flutter.
🔤 String
Text enclosed in double quotes
"Hello, World!"
🔢 Number
Integer or floating-point values
42
or
3.14
✅ Boolean
True or false values
true
or
false
📦 Object
Key-value pairs enclosed in
{ }
{
"key"
:
"value"
}
📋 Array
Ordered list enclosed in
[ ]
[
"one"
,
"two"
,
"three"
]
🚫 Null
Empty or missing value
null
Dart provides the
dart:convert
library with
jsonDecode
and
jsonEncode
functions for working with JSON.
import 'dart:convert';
void main() {
// 1. JSON string (from an API response)
final jsonString = '{"name": "John", "age": 30, "isStudent": false}';
// 2. Parse JSON to Dart object (Map/String/List)
final Map<String, dynamic> user = jsonDecode(jsonString);
// 3. Access values
print(user['name']);
// John
print(user['age']);
// 30
print(user['isStudent']);
// false
// 4. Convert Dart object back to JSON
final String jsonOutput = jsonEncode(user);
print(jsonOutput);
}
📦 Important Types
- jsonDecode() – Converts JSON string to Dart object
- jsonEncode() – Converts Dart object to JSON string
- Map<String, dynamic> – Type for JSON objects
- List<dynamic> – Type for JSON arrays
- dynamic – Values can be String, int, bool, null, or nested Map/List
Most real-world APIs return complex JSON with nested objects and arrays. Here's how to parse them.
import 'dart:convert';
void main() {
final jsonString = '''
{
"id": 1,
"title": "Flutter Developer",
"company": {
"name": "Tech Corp",
"location": "San Francisco"
},
"skills": ["Flutter", "Dart", "Supabase"],
"isRemote": true
}
''';
// Parse the JSON
final Map<String, dynamic> job = jsonDecode(jsonString);
// Access nested objects
final company = job['company'] as Map<String, dynamic>;
print(company['name']);
// Tech Corp
// Access arrays
final skills = job['skills'] as List<dynamic>;
skills.forEach((skill) => print(skill));
// Handle null values safely
final description = job['description'] as String?;
print(description ?? 'No description provided');
}
For better type safety, you should convert JSON data into strongly-typed Dart classes . This makes your code more maintainable and less error-prone.
class Album {
final int userId;
final int id;
final String title;
const Album({required this.userId, required this.id, required this.title});
// Convert JSON to Album object
factory Album.fromJson(Map<String, dynamic> json) {
return Album(
userId: json['userId'] as int,
id: json['id'] as int,
title: json['title'] as String,
);
}
// Convert Album object to JSON
Map<String, dynamic> toJson() {
return {'userId': userId, 'id': id, 'title': title};
}
}
import 'dart:convert';
import 'models/album.dart';
void main() {
final jsonString = '{"userId": 1, "id": 1, "title": "Sample Album"}';
// Parse JSON and create Album object
final Map<String, dynamic> jsonMap = jsonDecode(jsonString);
final Album album = Album.fromJson(jsonMap);
// Now we have a type-safe object
print(album.title);
// Sample Album
print(album.userId);
// 1
// Convert back to JSON
final String jsonOutput = jsonEncode(album.toJson());
print(jsonOutput);
}
✅ Best Practice
Always define
strongly-typed Dart classes
with
fromJson
and
toJson
methods. This provides type safety, IDE autocomplete, and makes
your code more maintainable.
Most APIs return lists of objects . Here's how to parse a list of albums.
// Add this to the Album class
static
List<Album> fromJsonList(List<dynamic> jsonList) {
return
jsonList.map((json) => Album.fromJson(json)).toList();
}
// Usage:
final
List<Album> albums = Album.fromJsonList(jsonList);
import 'dart:convert';
import 'models/album.dart';
void main() {
final jsonString = '''
[
{"userId": 1, "id": 1, "title": "Album 1"},
{"userId": 1, "id": 2, "title": "Album 2"},
{"userId": 2, "id": 3, "title": "Album 3"}
]
''';
// Parse list of albums
final List<dynamic> jsonList = jsonDecode(jsonString);
final List<Album> albums = Album.fromJsonList(jsonList);
// Use the typed list
albums.forEach((album) {
print('${album.id}: ${album.title}');
});
}
Here's a complete Flutter app that fetches JSON data from an API, parses it into Dart objects, and displays it in a list.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
// ----- Model -----
class Album {
final int userId;
final int id;
final String title;
const Album({required this.userId, required this.id, required this.title});
factory Album.fromJson(Map<String, dynamic> json) {
return Album(
userId: json['userId'] as int,
id: json['id'] as int,
title: json['title'] as String,
);
}
static List<Album> fromJsonList(List<dynamic> jsonList) {
return jsonList.map((json) => Album.fromJson(json)).toList();
}
}
// ----- API Service -----
class AlbumService {
final http.Client client;
AlbumService({http.Client? client}) : client = client ?? http.Client();
Future<List<Album>> fetchAlbums() async {
final response = await client.get(
Uri.parse('https://jsonplaceholder.typicode.com/albums'),
headers: {'Accept': 'application/json'},
);
if (response.statusCode == 200) {
final List<dynamic> jsonList = jsonDecode(response.body);
return Album.fromJsonList(jsonList);
} else {
throw Exception('Failed to load albums');
}
}
}
// ----- Flutter UI -----
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Albums App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: AlbumListScreen(),
);
}
}
class AlbumListScreen extends StatefulWidget {
@override
State<AlbumListScreen> createState() => _AlbumListScreenState();
}
class _AlbumListScreenState extends State<AlbumListScreen> {
late Future<List<Album>> futureAlbums;
final AlbumService _service = AlbumService();
@override
void initState() {
super.initState();
futureAlbums = _service.fetchAlbums();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('📀 Albums'), centerTitle: true),
body: FutureBuilder<List<Album>>(
future: futureAlbums,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size: 64, color: Colors.red),
SizedBox(height: 16),
Text('Error: ${snapshot.error}'),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() {
futureAlbums = _service.fetchAlbums();
});
},
child: Text('Retry'),
),
],
),
);
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('No albums found'));
}
final albums = snapshot.data!;
return ListView.builder(
itemCount: albums.length,
itemBuilder: (context, index) {
final album = albums[index];
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
leading: CircleAvatar(child: Text(album.id.toString())),
title: Text(album.title),
subtitle: Text('User ${album.userId}'),
trailing: Icon(Icons.arrow_forward_ios, size: 16),
),
);
},
);
},
),
);
}
}
Album
class with typed properties
and a
fromJson
factory constructor.
jsonDecode()
to convert the JSON string
into a Dart object (
Map
or
List
).
fromJson
factory constructor
to convert
Map
objects into typed
Album
objects.
fromJsonList
method to convert a list
of JSON objects into a typed list.
FutureBuilder
to
display the data in a list view.
jsonDecode()
returns
dynamic
. You must cast to
Map<String, dynamic>
or
List<dynamic>
before accessing values.
Use
jsonDecode(response.body) as Map<String, dynamic>
or
jsonDecode(response.body) as List<dynamic>
.
JSON can contain
null
values. Accessing them without handling will crash your app.
Use
json['field'] as String?
or
json['field'] ?? 'default'
to handle null values safely.
Always check
response.statusCode
before parsing the JSON. A 404 or 500 error
will contain error information, not the data you expect.
Use
if (response.statusCode == 200)
before parsing, and throw exceptions
for other status codes.
🎯 Key Takeaway
JSON is the
universal data format
for APIs. In Flutter, use
dart:convert
to parse JSON with
jsonDecode()
and
jsonEncode()
.
Always convert JSON to typed Dart classes
for type safety and maintainability.