Working with JSON in Dart
dart:convert provides jsonEncode() to turn Dart objects into a JSON string and jsonDecode() to parse a JSON string back into nested Map<String, dynamic> and List<dynamic> structures. Because the decoded data is dynamic, it must be explicitly cast or converted into typed Dart objects before use.
Cricket analogy: jsonDecode() turning a raw JSON string into a Map<String, dynamic> is like a scorer converting handwritten scoresheet notes into the structured digital scorecard format used by broadcasters.
Manual Serialization with fromJson/toJson
The idiomatic way to deserialize a class is a factory constructor named ClassName.fromJson(Map<String, dynamic> json), paired with a toJson() method for the reverse direction. Nested objects and lists must be recursively parsed, for example by calling a nested type's own fromJson constructor inside the parent's.
Cricket analogy: Writing a factory User.fromJson() constructor that also parses a nested Team object is like a scorecard app reading a match JSON and correctly nesting each player's stats under their respective team entry.
Code Generation with json_serializable
json_serializable, driven by the @JsonSerializable() annotation and run through build_runner, generates the fromJson and toJson boilerplate automatically into a companion .g.dart file, so developers only declare the model's fields and let the generator keep the parsing logic in sync with them.
Cricket analogy: Using @JsonSerializable to auto-generate parsing code is like a team relying on Hawk-Eye's automated ball-tracking instead of manually eyeballing whether a delivery was in line — the machine generates the precise result reliably.
Handling Nulls and Type Safety
Real-world JSON APIs frequently omit fields or return null unexpectedly, so robust Dart models use nullable types and the null-coalescing operator ?? to supply sensible defaults, and cast decoded values defensively, for example as int? rather than assuming a field is always present and correctly typed.
Cricket analogy: Providing a default value like runs ?? 0 when a JSON field is missing is like a scorecard defaulting an unavailable statistic to zero rather than crashing the whole scoreboard display.
import 'dart:convert';
class Address {
final String city;
final String country;
Address({required this.city, required this.country});
factory Address.fromJson(Map<String, dynamic> json) {
return Address(
city: json['city'] as String? ?? 'Unknown',
country: json['country'] as String? ?? 'Unknown',
);
}
Map<String, dynamic> toJson() => {'city': city, 'country': country};
}
class User {
final String name;
final int age;
final Address address;
User({required this.name, required this.age, required this.address});
factory User.fromJson(Map<String, dynamic> json) {
return User(
name: json['name'] as String,
age: json['age'] as int? ?? 0,
address: Address.fromJson(json['address'] as Map<String, dynamic>),
);
}
Map<String, dynamic> toJson() => {
'name': name,
'age': age,
'address': address.toJson(),
};
}
void main() {
const raw = '{"name":"Asha","age":29,"address":{"city":"Pune","country":"India"}}';
final user = User.fromJson(jsonDecode(raw) as Map<String, dynamic>);
print(jsonEncode(user.toJson()));
}
jsonDecode() returns dynamic data, typically Map<String, dynamic>, so mistyped keys like json['naem'] or wrong-type casts like json['age'] as String won't be caught by the compiler — they'll throw at runtime instead. Always write defensive fromJson constructors, and consider json_serializable so a single source of truth, your model class, drives both encoding and decoding consistently.
- dart:convert provides jsonEncode() and jsonDecode() to convert between JSON strings and Dart Map/List structures.
- jsonDecode() returns dynamic data, typically Map<String, dynamic>, so type casting mistakes only surface as runtime errors.
- Manual fromJson factory constructors and toJson methods give full control over field mapping and nested object or list parsing.
- json_serializable with build_runner generates .g.dart files, eliminating repetitive and error-prone manual boilerplate.
- The @JsonSerializable() annotation marks a class for code generation; running build_runner regenerates code whenever the model changes.
- Nullable fields and null-coalescing (??) should be used to handle optional or missing JSON keys gracefully.
- Consistent serialization logic between fromJson and toJson prevents subtle round-trip bugs when re-encoding parsed data.
Practice what you learned
1. Which dart:convert function converts a JSON string into a Dart Map/List structure?
2. What is the idiomatic Dart convention for deserializing a class from JSON?
3. Which package, combined with build_runner, generates JSON serialization boilerplate automatically?
4. Why is jsonDecode()'s dynamic return type risky?
5. What is a good practice when a JSON field might be missing or null?
Was this page helpful?
You May Also Like
Testing Dart Code
How to write reliable unit, mock-based, and widget or integration tests for Dart and Flutter code using package:test and Mockito.
Dart for Server-Side Development
Building and deploying backend services in Dart using dart:io and the shelf framework, from routing to native executable deployment.
Dart and Flutter Overview
An introduction to the Dart language, how it powers Flutter's UI framework, and where else Dart is used beyond mobile app development.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics