Answer
JSON Serialization in Flutter
JSON serialization is the process of converting Dart objects to JSON format (serialization) and JSON data back to Dart objects (deserialization). This is essential for working with REST APIs, local storage, and data persistence in Flutter applications.
Two Main Approaches
| Approach | Method | Best For |
|---|---|---|
| Manual Serialization | Write text text | Small projects, simple models |
| Code Generation | Use text | Large projects, complex models |
1. Manual Serialization
Write serialization logic manually for each model class.
Basic Example
dartclass User { final int id; final String name; final String email; final bool isActive; User({ required this.id, required this.name, required this.email, required this.isActive, }); // Serialization: Dart object → JSON Map<String, dynamic> toJson() { return { 'id': id, 'name': name, 'email': email, 'is_active': isActive, }; } // Deserialization: JSON → Dart object 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['is_active'] as bool, ); } } // Usage void main() { // Deserialization example String jsonString = '{"id": 1, "name": "John", "email": "john@example.com", "is_active": true}'; Map<String, dynamic> jsonMap = jsonDecode(jsonString); User user = User.fromJson(jsonMap); print(user.name); // John // Serialization example User newUser = User( id: 2, name: 'Jane', email: 'jane@example.com', isActive: false, ); String encoded = jsonEncode(newUser.toJson()); print(encoded); // {"id":2,"name":"Jane","email":"jane@example.com","is_active":false} }
Handling Nested Objects
dartclass Address { final String street; final String city; final String country; Address({ required this.street, required this.city, required this.country, }); Map<String, dynamic> toJson() { return { 'street': street, 'city': city, 'country': country, }; } factory Address.fromJson(Map<String, dynamic> json) { return Address( street: json['street'] as String, city: json['city'] as String, country: json['country'] as String, ); } } class UserWithAddress { final int id; final String name; final Address address; // Nested object UserWithAddress({ required this.id, required this.name, required this.address, }); Map<String, dynamic> toJson() { return { 'id': id, 'name': name, 'address': address.toJson(), // Serialize nested object }; } factory UserWithAddress.fromJson(Map<String, dynamic> json) { return UserWithAddress( id: json['id'] as int, name: json['name'] as String, address: Address.fromJson(json['address'] as Map<String, dynamic>), // Deserialize nested ); } }
Handling Lists
dartclass Post { final int id; final String title; final List<String> tags; Post({ required this.id, required this.title, required this.tags, }); Map<String, dynamic> toJson() { return { 'id': id, 'title': title, 'tags': tags, // List serializes automatically }; } factory Post.fromJson(Map<String, dynamic> json) { return Post( id: json['id'] as int, title: json['title'] as String, tags: List<String>.from(json['tags'] as List), // Convert to List<String> ); } } // Handling list of objects class BlogResponse { final List<Post> posts; BlogResponse({required this.posts}); Map<String, dynamic> toJson() { return { 'posts': posts.map((post) => post.toJson()).toList(), }; } factory BlogResponse.fromJson(Map<String, dynamic> json) { return BlogResponse( posts: (json['posts'] as List) .map((postJson) => Post.fromJson(postJson as Map<String, dynamic>)) .toList(), ); } }
2. Code Generation with json_serializable
Automatically generate serialization code using the
text
json_serializableSetup
yaml# pubspec.yaml dependencies: json_annotation: ^4.8.1 dev_dependencies: build_runner: ^2.4.6 json_serializable: ^6.7.1
Model Class with Annotations
dartimport 'package:json_annotation/json_annotation.dart'; // This is required for code generation part 'user.g.dart'; () class User { final int id; final String name; final String email; (name: 'is_active') // Map JSON key to Dart field final bool isActive; (defaultValue: 0) // Default value if missing final int age; (ignore: true) // Don't serialize this field String? tempPassword; User({ required this.id, required this.name, required this.email, required this.isActive, required this.age, }); // Generated methods factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json); Map<String, dynamic> toJson() => _$UserToJson(this); }
Generate Code
bash# Generate serialization code flutter pub run build_runner build # Watch for changes and regenerate automatically flutter pub run build_runner watch # Delete conflicting outputs and rebuild flutter pub run build_runner build --delete-conflicting-outputs
Generated Code (user.g.dart)
dart// GENERATED CODE - DO NOT MODIFY BY HAND part of 'user.dart'; User _$UserFromJson(Map<String, dynamic> json) { return User( id: json['id'] as int, name: json['name'] as String, email: json['email'] as String, isActive: json['is_active'] as bool, age: json['age'] as int? ?? 0, ); } Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{ 'id': instance.id, 'name': instance.name, 'email': instance.email, 'is_active': instance.isActive, 'age': instance.age, };
Advanced Features
Custom Converters
dart// Custom DateTime converter class DateTimeConverter implements JsonConverter<DateTime, String> { const DateTimeConverter(); DateTime fromJson(String json) => DateTime.parse(json); String toJson(DateTime object) => object.toIso8601String(); } () class Event { final String name; () final DateTime date; Event({required this.name, required this.date}); factory Event.fromJson(Map<String, dynamic> json) => _$EventFromJson(json); Map<String, dynamic> toJson() => _$EventToJson(this); }
Nullable Fields
dart() class Product { final int id; final String name; final String? description; // Nullable field final double? discount; // Nullable field Product({ required this.id, required this.name, this.description, this.discount, }); factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json); Map<String, dynamic> toJson() => _$ProductToJson(this); }
Generic Types
dart(genericArgumentFactories: true) class ApiResponse<T> { final bool success; final T data; final String? error; ApiResponse({ required this.success, required this.data, this.error, }); factory ApiResponse.fromJson( Map<String, dynamic> json, T Function(Object? json) fromJsonT, ) => _$ApiResponseFromJson(json, fromJsonT); Map<String, dynamic> toJson(Object Function(T value) toJsonT) => _$ApiResponseToJson(this, toJsonT); } // Usage ApiResponse<User> response = ApiResponse.fromJson( jsonData, (json) => User.fromJson(json as Map<String, dynamic>), );
Real-World API Integration
dartimport 'dart:convert'; import 'package:http/http.dart' as http; class ApiService { static const String baseUrl = 'https://api.example.com // Fetch list of users Future<List<User>> fetchUsers() async { final response = await http.get(Uri.parse('$baseUrl/users')); if (response.statusCode == 200) { final List<dynamic> jsonList = jsonDecode(response.body); return jsonList .map((json) => User.fromJson(json as Map<String, dynamic>)) .toList(); } else { throw Exception('Failed to load users'); } } // Create new user Future<User> createUser(User user) async { final response = await http.post( Uri.parse('$baseUrl/users'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(user.toJson()), ); if (response.statusCode == 201) { return User.fromJson(jsonDecode(response.body)); } else { throw Exception('Failed to create user'); } } // Update user Future<User> updateUser(int id, User user) async { final response = await http.put( Uri.parse('$baseUrl/users/$id'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(user.toJson()), ); if (response.statusCode == 200) { return User.fromJson(jsonDecode(response.body)); } else { throw Exception('Failed to update user'); } } }
Error Handling
dartUser? parseUser(String jsonString) { try { final json = jsonDecode(jsonString); return User.fromJson(json); } on FormatException catch (e) { print('Invalid JSON format: $e'); return null; } on TypeError catch (e) { print('Type mismatch: $e'); return null; } catch (e) { print('Unknown error: $e'); return null; } }
Best Practices
- Use Code Generation for Large Projects: Reduces boilerplate and errors
- Handle Null Safety Properly: Use nullable types and default values
- Validate Data: Check for null and invalid values before deserialization
- Use Consistent Naming: Match JSON keys with Dart field names or use text
@JsonKey - Test Serialization: Write unit tests for toJson/fromJson methods
- Handle Dates Properly: Use custom converters for DateTime
- Separate Models from UI: Keep data models in separate files
Comparison Table
| Feature | Manual Serialization | Code Generation |
|---|---|---|
| Setup Complexity | Low | Medium |
| Code Maintainability | Low (repetitive) | High (automated) |
| Compile Time | Fast | Slower (generation step) |
| Type Safety | Manual | Automatic |
| Best For | Small projects | Large projects |
| Learning Curve | Easy | Moderate |
Popular JSON Packages
- json_serializable: Most popular code generation tool
- freezed: Immutable data classes with JSON serialization
- built_value: Alternative code generation with builders
yamldependencies: freezed_annotation: ^2.4.1 dev_dependencies: freezed: ^2.4.5 build_runner: ^2.4.6
Important: Choose manual serialization for simple apps and code generation for production apps with complex models. Always handle errors and validate JSON data.
Documentation: Flutter JSON and Serialization