Dart 47 🎯 JSON Serialization and Deserialization using dart:convert
JSON is the wire format of the modern web. The REST APIs return it, the configuration files store it, the WebSocket messages carry it, and the local storage persists it. The dart:convert library is the built-in codec for the JSON, and it provides the two functions: the jsonEncode and the jsonDecode. The jsonEncode takes the Dart object and returns the JSON string, and the jsonDecode takes the JSON string and returns the Dart object. The functions are the foundation, and the pattern is the mapping between the dynamic JSON structure and the typed Dart classes.
The mapping is not automatic. The jsonDecode returns the dynamic — the Map<String, dynamic> for the objects, the List<dynamic> for the arrays, the String, the num, the bool, and the null for the primitives. The Dart class does not know how to construct itself from the map, and the developer writes the fromJson factory and the toJson method. The manual mapping is the explicit and the predictable, and it is the pattern that the dart:convert provides. The json_serializable package is the code-generation alternative, and it generates the boilerplate, but the manual approach is the foundation.
The types are the subtlety. The JSON has the number which is the int or the double, and the Dart separates them. The JSON has the null, and the Dart’s null safety distinguishes the nullable from the non-nullable. The JSON has the nested objects and the arrays, and the Dart maps them to the nested classes and the List<T>. The DateTime is not the JSON type, and the convention is the ISO 8601 string or the Unix timestamp. The enum is not the JSON type, and the convention is the string name or the integer value.
This chapter covers three areas. First, why the dart:convert exists — the JSON-codec problem and the manual-mapping approach. Second, the encoding and the decoding — the jsonEncode, the jsonDecode, the JsonEncoder, the JsonDecoder, and the toEncodable. Third, the patterns and the errors — the fromJson factory, the toJson method, the nested objects, the lists, the enums, the DateTime, the null safety, and the FormatException. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the JSON-to-Dart mapping.
Key point: The jsonEncode converts the Dart objects into the JSON string, and the jsonDecode parses the JSON string into the Dart objects. The jsonDecode returns the dynamic — the Map<String, dynamic>, the List<dynamic>, the String, the num, the bool, and the null. The developer writes the fromJson factory and the toJson method for the typed classes. The JsonEncoder and the JsonDecoder are the configurable codecs. The FormatException is the malformed JSON error.
Why dart:convert exists
The JSON-codec problem. The JSON is the text format, and the Dart is the object format. The bridge between them is the codec. The dart:convert library provides the jsonEncode and the jsonDecode functions, and they are the built-in codec. The jsonEncode walks the Dart object and writes the JSON, and the jsonDecode reads the JSON and constructs the Dart object. The codec is the foundation of the data interchange.
The dynamic problem. The jsonDecode returns the dynamic, because the JSON is the untyped. The Map<String, dynamic> is the object, the List<dynamic> is the array, and the primitives are the String, the num, the bool, and the null. The dynamic means the compiler does not check the access, and the runtime error is the possibility. The typed class with the fromJson factory restores the type safety.
The manual-mapping problem. The Dart does not have the reflection in the Flutter and the AOT-compiled builds, so the automatic mapping is not possible without the code generation. The manual fromJson and toJson are the explicit and the predictable. The json_serializable package generates them from the annotations, and the generated code is the boilerplate. The manual approach is the foundation, and the code generation is the convenience.
The type-mapping problem. The JSON types do not map directly to the Dart types. The number is the int or the double, and the Dart distinguishes them. The DateTime is not the JSON type, and the convention is the ISO 8601 string. The enum is not the JSON type, and the convention is the string or the integer. The fromJson and the toJson handle the conversions.
The null-safety problem. The JSON has the null, and the Dart’s null safety distinguishes the nullable from the non-nullable. The json['name'] as String throws when the value is the null or the wrong type. The json['name'] as String? is the nullable, and the as String is the non-nullable with the runtime check. The fromJson must handle the missing and the null fields.
The trade-off. The manual mapping is the explicit and the verbose. The code generation is the concise and the hidden. The dart:convert is the built-in and the zero-dependency. The json_serializable is the additional dependency and the build step. The trade-off is between the transparency of the manual mapping and the convenience of the code generation.
a. The jsonEncode and the jsonDecode
The jsonEncode converts the Dart object into the JSON string.
import 'dart:convert';
void main() {
final data = {
'name': 'Alice',
'age': 30,
'active': true,
'tags': ['admin', 'user'],
};
final json = jsonEncode(data);
print(json);
// {"name":"Alice","age":30,"active":true,"tags":["admin","user"]}
}
The jsonEncode accepts the Map, the List, the String, the num, the bool, and the null. The other objects require the toEncodable or the custom toJson method .
The jsonDecode parses the JSON string into the Dart object.
void main() {
final json = '{"name":"Alice","age":30,"active":true,"tags":["admin","user"]}';
final dynamic data = jsonDecode(json);
print(data['name']); // Alice
print(data['age']); // 30
print(data['tags']); // [admin, user]
}
The jsonDecode returns the dynamic, and the developer accesses the fields with the [] operator. The nested objects and the arrays are the Map<String, dynamic> and the List<dynamic> .
The Map<String, dynamic> cast. The jsonDecode returns the Map<String, dynamic> for the objects, and the developer can cast the result.
final Map<String, dynamic> data = jsonDecode(json) as Map<String, dynamic>;
The explicit cast makes the type the Map<String, dynamic>, and the field access is the typed.
The JsonEncoder and the JsonDecoder. The JsonEncoder and the JsonDecoder are the classes behind the functions, and they provide the configuration.
const encoder = JsonEncoder.withIndent(' ');
final pretty = encoder.convert(data);
print(pretty);
// {
// "name": "Alice",
// "age": 30
// }
The withIndent produces the pretty-printed JSON, and the default is the compact. The JsonDecoder has the reviver for the custom parsing .
The toEncodable. The jsonEncode has the toEncodable parameter for the custom objects:
final json = jsonEncode(customObject, toEncodable: (obj) {
if (obj is User) {
return obj.toJson();
}
return obj.toJson();
});
The toEncodable is the fallback for the objects the encoder does not know how to serialize.
b. The fromJson and the toJson
The typed class provides the fromJson factory and the toJson method. The fromJson constructs the object from the map, and the toJson returns the map.
class User {
final int id;
final String name;
final String email;
final bool active;
User({
required this.id,
required this.name,
required this.email,
required this.active,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
active: json['active'] as bool,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'active': active,
};
}
}
The factory constructor is the convention for the fromJson, and the toJson returns the Map<String, dynamic>. The as casts are the runtime checks, and the wrong type throws the TypeError .
The nullable fields. The optional fields are the nullable, and the fromJson handles the null:
class User {
final int id;
final String name;
final String? phone;
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
phone: json['phone'] as String?,
);
}
}
The as String? is the nullable cast, and the missing key returns the null.
The default values. The fromJson can provide the defaults for the missing fields:
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
active: json['active'] as bool? ?? true,
);
}
The ?? operator provides the default when the value is the null.
The nested objects. The nested object is the Map<String, dynamic>, and the fromJson calls the nested fromJson:
class Post {
final int id;
final String title;
final User author;
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'] as int,
title: json['title'] as String,
author: User.fromJson(json['author'] as Map<String, dynamic>),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'author': author.toJson(),
};
}
}
The json['author'] as Map<String, dynamic> casts the nested object, and the User.fromJson constructs the nested class.
The lists. The list of the objects is the List<dynamic>, and the fromJson maps each element:
class UserList {
final List<User> users;
factory UserList.fromJson(List<dynamic> json) {
return UserList(
users: json
.map((item) => User.fromJson(item as Map<String, dynamic>))
.toList(),
);
}
List<Map<String, dynamic>> toJson() {
return users.map((user) => user.toJson()).toList();
}
}
The .map transforms each dynamic element into the User, and the .toList() collects the result.
The enums. The enum is the string or the integer, and the fromJson maps it:
enum Role { admin, user, guest }
class User {
final Role role;
factory User.fromJson(Map<String, dynamic> json) {
return User(
role: Role.values.byName(json['role'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'role': role.name,
};
}
}
The Role.values.byName finds the enum value by the name, and the role.name returns the name. The integer convention uses the Role.values[json['role'] as int] .
The DateTime. The DateTime is the ISO 8601 string, and the fromJson parses it:
class Event {
final DateTime start;
final DateTime? end;
factory Event.fromJson(Map<String, dynamic> json) {
return Event(
start: DateTime.parse(json['start'] as String),
end: json['end'] != null
? DateTime.parse(json['end'] as String)
: null,
);
}
Map<String, dynamic> toJson() {
return {
'start': start.toIso8601String(),
'end': end?.toIso8601String(),
};
}
}
The DateTime.parse parses the ISO 8601 string, and the toIso8601String produces it. The Unix timestamp convention uses the DateTime.fromMillisecondsSinceEpoch and the millisecondsSinceEpoch.
c. The errors and the patterns
The FormatException. The jsonDecode throws the FormatException when the string is not the valid JSON:
try {
final data = jsonDecode('{invalid json}');
} on FormatException catch (e) {
print('Invalid JSON: ${e.message}');
}
The FormatException carries the message, the source, and the offset, and the catch handles the malformed JSON.
The TypeError. The as cast throws the TypeError when the value is the wrong type:
try {
final user = User.fromJson(json);
} on TypeError catch (e) {
print('Type error: $e');
}
The TypeError is the runtime error, and the fromJson must handle the missing and the wrong-typed fields.
The defensive fromJson. The robust fromJson handles the null and the type with the helpers:
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: _parseInt(json['id']),
name: json['name'] as String? ?? '',
active: json['active'] as bool? ?? false,
);
}
static int _parseInt(dynamic value) {
if (value is int) return value;
if (value is String) return int.parse(value);
if (value is double) return value.toInt();
throw FormatException('Expected an int, got ${value.runtimeType}');
}
The _parseInt handles the int, the string, and the double, and the FormatException is the clear error. The defensive parsing is the robust, and it handles the API that returns the inconsistent types.
The toJson with the null. The toJson omits the null fields or includes them:
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
if (phone != null) 'phone': phone,
};
}
The if (phone != null) includes the phone only when it is the not-null. The pattern is the clean JSON that omits the optional fields.
The list of the primitives. The list of the strings or the ints is the List<dynamic>, and the cast is the List<String>:
final tags = (json['tags'] as List<dynamic>).cast<String>();
The .cast<String>() is the lazy cast, and the List<String>.from(json['tags']) is the eager.
The json_serializable alternative. The json_serializable package generates the fromJson and the toJson from the annotations:
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
final int id;
final String name;
User({required this.id, required this.name});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
The build runner generates the user.g.dart with the _$UserFromJson and the _$UserToJson. The annotations handle the field names, the defaults, and the custom converters.
Complete Example Session
// ============================================
// PART 1: THE BASIC ENCODE
// ============================================
import 'dart:convert';
void main() {
final data = {
'name': 'Alice',
'age': 30,
'active': true,
'tags': ['admin', 'user'],
};
final json = jsonEncode(data);
print(json);
// {"name":"Alice","age":30,"active":true,"tags":["admin","user"]}
}
// ============================================
// PART 2: THE BASIC DECODE
// ============================================
void main() {
final json = '{"name":"Alice","age":30,"active":true}';
final dynamic data = jsonDecode(json);
print(data['name']); // Alice
print(data['age']); // 30
}
// ============================================
// PART 3: THE TYPED CLASS
// ============================================
class User {
final int id;
final String name;
final String email;
final bool active;
User({
required this.id,
required this.name,
required this.email,
required this.active,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
active: json['active'] as bool,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'active': active,
};
}
}
// ============================================
// PART 4: THE NULLABLE AND THE DEFAULT
// ============================================
class User {
final int id;
final String name;
final String? phone;
final bool active;
User({
required this.id,
required this.name,
this.phone,
this.active = true,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
phone: json['phone'] as String?,
active: json['active'] as bool? ?? true,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
if (phone != null) 'phone': phone,
'active': active,
};
}
}
// ============================================
// PART 5: THE NESTED OBJECT
// ============================================
class Post {
final int id;
final String title;
final User author;
Post({required this.id, required this.title, required this.author});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'] as int,
title: json['title'] as String,
author: User.fromJson(json['author'] as Map<String, dynamic>),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'author': author.toJson(),
};
}
}
// ============================================
// PART 6: THE LIST OF THE OBJECTS
// ============================================
List<User> usersFromJson(List<dynamic> json) {
return json
.map((item) => User.fromJson(item as Map<String, dynamic>))
.toList();
}
List<Map<String, dynamic>> usersToJson(List<User> users) {
return users.map((user) => user.toJson()).toList();
}
// ============================================
// PART 7: THE ENUM
// ============================================
enum Role { admin, user, guest }
class Account {
final Role role;
Account({required this.role});
factory Account.fromJson(Map<String, dynamic> json) {
return Account(
role: Role.values.byName(json['role'] as String),
);
}
Map<String, dynamic> toJson() {
return {'role': role.name};
}
}
// ============================================
// PART 8: THE DATETIME
// ============================================
class Event {
final String name;
final DateTime start;
final DateTime? end;
Event({required this.name, required this.start, this.end});
factory Event.fromJson(Map<String, dynamic> json) {
return Event(
name: json['name'] as String,
start: DateTime.parse(json['start'] as String),
end: json['end'] != null
? DateTime.parse(json['end'] as String)
: null,
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'start': start.toIso8601String(),
'end': end?.toIso8601String(),
};
}
}
// ============================================
// PART 9: THE ERROR HANDLING
// ============================================
void parseJson(String source) {
try {
final data = jsonDecode(source);
final user = User.fromJson(data as Map<String, dynamic>);
print('Parsed: ${user.name}');
} on FormatException catch (e) {
print('Invalid JSON: ${e.message}');
} on TypeError catch (e) {
print('Type error: $e');
} catch (e) {
print('Unknown error: $e');
}
}
// ============================================
// PART 10: THE COMPLETE EXAMPLE
// ============================================
class Address {
final String street;
final String city;
final String zip;
Address({required this.street, required this.city, required this.zip});
factory Address.fromJson(Map<String, dynamic> json) {
return Address(
street: json['street'] as String,
city: json['city'] as String,
zip: json['zip'] as String,
);
}
Map<String, dynamic> toJson() => {
'street': street,
'city': city,
'zip': zip,
};
}
class Customer {
final int id;
final String name;
final String email;
final Address? address;
final List<String> tags;
final DateTime createdAt;
Customer({
required this.id,
required this.name,
required this.email,
this.address,
required this.tags,
required this.createdAt,
});
factory Customer.fromJson(Map<String, dynamic> json) {
return Customer(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
address: json['address'] != null
? Address.fromJson(json['address'] as Map<String, dynamic>)
: null,
tags: (json['tags'] as List<dynamic>).cast<String>(),
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
if (address != null) 'address': address!.toJson(),
'tags': tags,
'createdAt': createdAt.toIso8601String(),
};
}
}
void main() {
final json = '''
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zip": "12345"
},
"tags": ["vip", "wholesale"],
"createdAt": "2024-01-15T10:30:00.000Z"
}
''';
final customer = Customer.fromJson(
jsonDecode(json) as Map<String, dynamic>,
);
print(customer.name); // Alice
print(customer.address?.city); // Springfield
print(customer.tags); // [vip, wholesale]
print(customer.createdAt); // 2024-01-15 10:30:00.000Z
final encoded = jsonEncode(customer.toJson());
print(encoded);
}
The ten parts show the basic encode, the basic decode, the typed class, the nullable and the default, the nested object, the list of the objects, the enum, the DateTime, the error handling, and the complete example.
Quick Reference
The Core Functions
| Function | The purpose |
|---|---|
jsonEncode(object) | The Dart object to the JSON string |
jsonDecode(string) | The JSON string to the Dart object |
JsonEncoder().convert(obj) | The configurable encoder |
JsonEncoder.withIndent(' ') | The pretty-printed encoder |
JsonDecoder().convert(str) | The configurable decoder |
The JSON Types to the Dart Types
| The JSON | The Dart |
|---|---|
| The object | The Map<String, dynamic> |
| The array | The List<dynamic> |
| The string | The String |
| The number (int) | The int |
| The number (double) | The double |
| The boolean | The bool |
| The null | The null |
The fromJson Patterns
| The pattern | The example |
|---|---|
| The required field | json['id'] as int |
| The nullable field | json['phone'] as String? |
| The default | json['active'] as bool? ?? true |
| The nested object | User.fromJson(json['author'] as Map<String, dynamic>) |
| The list | (json['tags'] as List<dynamic>).cast<String>() |
| The list of the objects | json.map((e) => User.fromJson(e)).toList() |
| The enum | Role.values.byName(json['role'] as String) |
| The DateTime | DateTime.parse(json['date'] as String) |
The toJson Patterns
| The pattern | The example |
|---|---|
| The simple field | 'id': id |
| The optional field | if (phone != null) 'phone': phone |
| The nested object | 'author': author.toJson() |
| The list | 'tags': tags |
| The enum | 'role': role.name |
| The DateTime | 'date': date.toIso8601String() |
The Errors
| The error | The cause |
|---|---|
FormatException | The malformed JSON |
TypeError | The wrong type in the cast |
NoSuchMethodError | The missing field access |
The null check | The missing required field |
Best Practices
✅ Do This:
// Use the fromJson factory
factory User.fromJson(Map<String, dynamic> json) => User(...); // ✅
// Use the toJson method
Map<String, dynamic> toJson() => {'id': id, 'name': name}; // ✅
// Handle the nullable with the ?
phone: json['phone'] as String?, // ✅
// Provide the defaults with the ??
active: json['active'] as bool? ?? true, // ✅
// Cast the list
(json['tags'] as List<dynamic>).cast<String>(), // ✅
// Parse the DateTime
DateTime.parse(json['date'] as String), // ✅
// Map the enum
Role.values.byName(json['role'] as String), // ✅
// Catch the FormatException
try { jsonDecode(s); } on FormatException catch (e) { ... } // ✅
❌ Don’t Do This:
// Don't use the dynamic without the cast
final user = jsonDecode(s); user.name; // the runtime error // ❌
// Don't forget the null check
final phone = json['phone'] as String; // the throws on null // ❌
// Don't use the string concatenation for the JSON
final json = '{"name":"' + name + '"}'; // the injection // ❌
// Don't forget the Content-Type for the HTTP
// The POST with the JSON body needs the header // ❌
// Don't use the DateTime directly
DateTime.parse(json['date']); // the missing cast // ❌
// Don't ignore the FormatException
jsonDecode(invalid); // the unhandled // ❌
// Don't forget the toList()
json.map((e) => User.fromJson(e)); // the lazy Iterable // ❌
Common Pitfalls
| The pitfall | The why | The fix |
|---|---|---|
The TypeError | The wrong cast | Check the type |
The null error | The missing field | Use the ? and the ?? |
The FormatException | The malformed JSON | Catch and handle |
The list is the Iterable | The missing .toList() | Add the .toList() |
| The DateTime is the string | The missing parse | Use DateTime.parse |
| The enum is the string | The missing lookup | Use Role.values.byName |
| The nested object is the map | The missing fromJson | Call the nested fromJson |
| The number is the int or the double | The JSON has one type | Check the runtime type |
Real-World Examples
1. The Basic Encode
final json = jsonEncode({'name': 'Alice', 'age': 30});
2. The Basic Decode
final data = jsonDecode('{"name":"Alice"}');
3. The Typed Class
factory User.fromJson(Map<String, dynamic> json) => User(id: json['id'] as int);
4. The Nullable
phone: json['phone'] as String?,
5. The Default
active: json['active'] as bool? ?? true,
6. The Nested
author: User.fromJson(json['author'] as Map<String, dynamic>),
7. The List
tags: (json['tags'] as List<dynamic>).cast<String>(),
8. The Enum
role: Role.values.byName(json['role'] as String),
9. The DateTime
start: DateTime.parse(json['start'] as String),
10. The Complete
final customer = Customer.fromJson(jsonDecode(json));
final encoded = jsonEncode(customer.toJson());
Visual
The JSON-to-Dart Mapping
┌──────────────────────────────────────────────────────────────┐
│ THE JSON-TO-DART MAPPING │
│ │
│ THE JSON: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "id": 1, │ │
│ │ "name": "Alice", │ │
│ │ "active": true, │ │
│ │ "tags": ["vip", "wholesale"], │ │
│ │ "address": { "city": "Springfield" }, │ │
│ │ "createdAt": "2024-01-15T10:30:00.000Z" │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ │ jsonDecode │
│ ▼ │
│ THE DART DYNAMIC: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Map<String, dynamic> { │ │
│ │ 'id': int 1, │ │
│ │ 'name': String 'Alice', │ │
│ │ 'active': bool true, │ │
│ │ 'tags': List<dynamic> ['vip', 'wholesale'], │ │
│ │ 'address': Map<String, dynamic> { ... }, │ │
│ │ 'createdAt': String '2024-01-15T10:30:00.000Z', │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ │ Customer.fromJson │
│ ▼ │
│ THE DART TYPED: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Customer { │ │
│ │ id: 1, │ │
│ │ name: 'Alice', │ │
│ │ active: true, │ │
│ │ tags: ['vip', 'wholesale'], │ │
│ │ address: Address { city: 'Springfield' }, │ │
│ │ createdAt: DateTime(2024, 1, 15), │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Encode and the Decode
┌──────────────────────────────────────────────────────────────┐
│ THE ENCODE AND THE DECODE │
│ │
│ THE DART OBJECT: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ User(id: 1, name: 'Alice', active: true) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ │ toJson() │
│ ▼ │
│ THE MAP: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ {'id': 1, 'name': 'Alice', 'active': true} │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ │ jsonEncode │
│ ▼ │
│ THE JSON STRING: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ '{"id":1,"name":"Alice","active":true}' │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ │ jsonDecode │
│ ▼ │
│ THE MAP: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ {'id': 1, 'name': 'Alice', 'active': true} │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ │ fromJson() │
│ ▼ │
│ THE DART OBJECT: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ User(id: 1, name: 'Alice', active: true) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Error Handling
┌──────────────────────────────────────────────────────────────┐
│ THE ERROR HANDLING │
│ │
│ try { │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ final data = jsonDecode(source); │ │
│ │ final user = User.fromJson(data); │ │
│ └──────────────────────────────────────────────────────┘ │
│ } on FormatException catch (e) { │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The malformed JSON. │ │
│ │ e.message, e.source, e.offset │ │
│ └──────────────────────────────────────────────────────┘ │
│ } on TypeError catch (e) { │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The wrong type in the cast. │ │
│ │ 'Expected a value of type int, but got String' │ │
│ └──────────────────────────────────────────────────────┘ │
│ } catch (e) { │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The unknown error. │ │
│ └──────────────────────────────────────────────────────┘ │
│ } │
│ │
└──────────────────────────────────────────────────────────────┘
The json_serializable Alternative
┌──────────────────────────────────────────────────────────────┐
│ THE json_serializable ALTERNATIVE │
│ │
│ @JsonSerializable() │
│ class User { │
│ final int id; │
│ final String name; │
│ │
│ User({required this.id, required this.name}); │
│ │
│ factory User.fromJson(Map<String, dynamic> json) => │
│ _$UserFromJson(json); │
│ │
│ Map<String, dynamic> toJson() => _$UserToJson(this); │
│ } │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The build_runner generates the user.g.dart: │ │
│ │ │ │
│ │ User _$UserFromJson(Map<String, dynamic> json) { │ │
│ │ return User( │ │
│ │ id: json['id'] as int, │ │
│ │ name: json['name'] as String, │ │
│ │ ); │ │
│ │ } │ │
│ │ │ │
│ │ Map<String, dynamic> _$UserToJson(User instance) { │ │
│ │ return { │ │
│ │ 'id': instance.id, │ │
│ │ 'name': instance.name, │ │
│ │ }; │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The annotations are the source; the generated code is the │
│ boilerplate. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
jsonEncode | The Dart object to the JSON string |
jsonDecode | The JSON string to the Dart object |
| The decoded types | The Map, the List, the String, the num, the bool, the null |
fromJson | The factory that constructs the typed object |
toJson | The method that returns the map |
| The nullable | The as String? |
| The default | The as bool? ?? true |
| The nested | The User.fromJson(json['author']) |
| The list | The .map().toList() |
| The enum | The Role.values.byName |
| The DateTime | The DateTime.parse |
| The error | The FormatException |
Key takeaways:
- The
dart:convertlibrary provides thejsonEncodeand thejsonDecode. ThejsonEncodeconverts the Dart object into the JSON string, and thejsonDecodeparses the JSON string into the Dart object. The library is the built-in codec, and the two functions are the foundation . - The
jsonDecodereturns thedynamic. TheMap<String, dynamic>is the object, theList<dynamic>is the array, and the primitives are theString, thenum, thebool, and thenull. The typed class with thefromJsonfactory restores the type safety . - The
fromJsonfactory and thetoJsonmethod are the mapping. ThefromJsonconstructs the typed object from the map, and thetoJsonreturns the map. Theascasts are the runtime checks, and the wrong type throws theTypeError. - The nullable and the default handle the optional fields. The
as String?is the nullable cast, and theas bool? ?? trueprovides the default. Theif (phone != null)in thetoJsonomits the null fields . - The nested objects and the lists are the composition. The
User.fromJson(json['author'] as Map<String, dynamic>)handles the nested object, and the.map((e) => User.fromJson(e)).toList()handles the list of the objects . - The enum and the
DateTimeneed the conversion. TheRole.values.byName(json['role'] as String)maps the enum, and theDateTime.parse(json['date'] as String)parses the DateTime. ThetoIso8601Stringproduces the DateTime string . - The
FormatExceptionis the malformed JSON, and theTypeErroris the wrong type. Thetry/catchhandles both, and the defensivefromJsonhandles the inconsistent types with the helper functions . - The
json_serializableis the code-generation alternative. The package generates thefromJsonand thetoJsonfrom the annotations, and the build runner produces the.g.dartfile. The manual approach is the foundation, and the code generation is the convenience .
Remember: The dart:convert library is the built-in JSON codec. The jsonEncode converts the Dart object into the JSON string, and the jsonDecode parses the JSON string into the Dart object. The jsonDecode returns the dynamic, and the typed class with the fromJson factory and the toJson method restores the type safety. The nullable, the default, the nested objects, the lists, the enums, and the DateTime are the patterns. The FormatException and the TypeError are the errors. The result is the reliable JSON serialization, which is the foundation of the data interchange.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!