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.