Converting JSON to Dart model classes is one of the most common tasks in Flutter development. Whether you're fetching data from a REST API or reading local configuration, you need well-structured Dart classes to safely parse and use your JSON data.
Why You Need Model Classes
- Type safety β catch errors at compile time instead of runtime
- IDE autocompletion and refactoring support
- Clear data contracts between frontend and backend
- Easier testing and maintenance
Basic JSON to Dart Type Mapping
| JSON Type | Dart Type | Example |
|---|---|---|
| string | String | "hello" β "hello" |
| number (int) | int | 42 β 42 |
| number (float) | double | 3.14 β 3.14 |
| boolean | bool | true β true |
| null | Null / dynamic | null β null |
| array | List<T> | [1,2,3] β [1,2,3] |
| object | Map / Class | {} β User() |
Manual Model Class (fromJson / toJson)
The most straightforward approach is writing model classes by hand. Here's a complete example:
// JSON input
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"isActive": true,
"score": 95.5
}
// Dart model class
class User {
final int id;
final String name;
final String email;
final bool isActive;
final double score;
User({
required this.id,
required this.name,
required this.email,
required this.isActive,
required this.score,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
isActive: json['isActive'] as bool,
score: (json['score'] as num).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'isActive': isActive,
'score': score,
};
}
}Handling Nested Objects
When your JSON contains nested objects, create separate Dart classes for each level:
// JSON with nested object
{
"id": 1,
"name": "John",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zipCode": "62701"
}
}
// Dart classes
class Address {
final String street;
final String city;
final String zipCode;
Address({required this.street, required this.city, required this.zipCode});
factory Address.fromJson(Map<String, dynamic> json) {
return Address(
street: json['street'] as String,
city: json['city'] as String,
zipCode: json['zipCode'] as String,
);
}
Map<String, dynamic> toJson() => {
'street': street, 'city': city, 'zipCode': zipCode,
};
}
class User {
final int id;
final String name;
final Address address;
User({required this.id, required this.name, required this.address});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
address: Address.fromJson(json['address'] as Map<String, dynamic>),
);
}
Map<String, dynamic> toJson() => {
'id': id, 'name': name, 'address': address.toJson(),
};
}Handling Lists and Arrays
JSON arrays map to Dart List types. Handle both simple and complex arrays:
// JSON with arrays
{
"name": "John",
"hobbies": ["coding", "reading", "gaming"],
"orders": [
{"id": 1, "product": "Laptop", "price": 999.99},
{"id": 2, "product": "Mouse", "price": 29.99}
]
}
// Dart
class User {
final String name;
final List<String> hobbies;
final List<Order> orders;
User({required this.name, required this.hobbies, required this.orders});
factory User.fromJson(Map<String, dynamic> json) {
return User(
name: json['name'] as String,
hobbies: List<String>.from(json['hobbies'] as List),
orders: (json['orders'] as List)
.map((e) => Order.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
}Null Safety Best Practices
With Dart's sound null safety, you need to handle nullable fields properly:
- Use `required` keyword for fields that must always be present
- Use `?` suffix for nullable types (e.g., `String?`)
- Provide default values with `??` operator in fromJson
- Use `late` keyword only when you're certain the value will be initialized
class User {
final int id;
final String name;
final String? bio; // nullable
final String avatarUrl; // with default
User({
required this.id,
required this.name,
this.bio,
this.avatarUrl = 'https://example.com/default-avatar.png',
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
bio: json['bio'] as String?,
avatarUrl: json['avatarUrl'] as String? ?? 'https://example.com/default-avatar.png',
);
}
}Using json_serializable Package
For larger projects, the json_serializable package automates the boilerplate:
Step 1: Add Dependencies
# pubspec.yaml
dependencies:
json_annotation: ^4.8.1
dev_dependencies:
json_serializable: ^6.7.1
build_runner: ^2.4.6Step 2: Create Model with Annotations
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
final int id;
final String name;
final String email;
@JsonKey(name: 'is_active')
final bool isActive;
@JsonKey(defaultValue: 0.0)
final double score;
User({
required this.id,
required this.name,
required this.email,
required this.isActive,
required this.score,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}Step 3: Run Build Runner
dart run build_runner build --delete-conflicting-outputsUsing Freezed for Immutable Models
The freezed package generates immutable classes with copyWith, equality, and JSON serialization:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
required int id,
required String name,
required String email,
@Default(false) bool isActive,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}Common Mistakes and Fixes
| Mistake | Fix |
|---|---|
| Using `json['key']` without null check | Use `json['key'] as String? ?? ''` or handle null |
| Forgetting to convert nested objects | Call `NestedClass.fromJson(json['key'])` for objects |
| Using `int` for all numbers | Use `num` and convert: `(json['price'] as num).toDouble()` |
| Not handling missing keys | Use `json.containsKey('key')` or provide defaults |
| Returning wrong type in toJson | Ensure nested objects call their own `.toJson()` |
FAQ
What is the best way to convert JSON to Dart classes?
For small projects, manual conversion works well. For larger projects, use json_serializable or freezed packages to auto-generate the boilerplate code. Online tools like our JSON to Dart Converter can help generate the initial code.
How do I handle dynamic JSON keys in Dart?
Use Map<String, dynamic> for objects with unknown keys. For example: final Map<String, dynamic> metadata = json['metadata'] as Map<String, dynamic>;
Should I use json_serializable or freezed?
Use json_serializable for simple JSON serialization needs. Use freezed when you also want immutability, copyWith, union types, and sealed classes. Freezed internally uses json_serializable for JSON support.
How do I handle dates in JSON to Dart conversion?
JSON doesn't have a native date type. Parse ISO 8601 strings with DateTime.parse(json['date']). In toJson, use dateTime.toIso8601String(). With json_serializable, use @JsonKey(fromJson: DateTime.parse, toJson: _dateToString).