JSON serialization in Flutter with json_serializable
By John · 4 September 2025
REST APIs return JSON. You need to turn that JSON into Dart objects and back. json_serializable generates the boilerplate so you don't write fromJson/toJson by hand.
Setup
dependencies:
json_annotation: ^4.8.1
dev_dependencies:
json_serializable: ^6.7.1
build_runner: ^2.4.8
Annotate your model
import 'package:json_annotation/json_annotation.dart';
part 'product.g.dart';
@JsonSerializable()
class Product {
final String id;
final String name;
final double price;
// API uses snake_case, Dart uses camelCase
@JsonKey(name: 'image_url')
final String imageUrl;
// Field might be absent in older API versions
@JsonKey(defaultValue: true)
final bool isAvailable;
// Nullable field
final String? description;
const Product({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
required this.isAvailable,
this.description,
});
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
Map<String, dynamic> toJson() => _$ProductToJson(this);
}
flutter pub run build_runner build --delete-conflicting-outputs
Global snake_case → camelCase
Instead of @JsonKey(name:) on every field, configure it globally:
@JsonSerializable(fieldRename: FieldRename.snake)
class Product {
final String id;
final String name;
final double price;
final String imageUrl; // Automatically maps to 'image_url'
final bool isAvailable; // Automatically maps to 'is_available'
// ...
}
Nested objects
@JsonSerializable()
class Order {
final String id;
final User customer;
final List<OrderItem> items;
final Address shippingAddress;
const Order({
required this.id,
required this.customer,
required this.items,
required this.shippingAddress,
});
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
Map<String, dynamic> toJson() => _$OrderToJson(this);
}
// User, OrderItem, Address all need @JsonSerializable() annotations too
Custom converters
For types that need special handling:
class DateTimeConverter implements JsonConverter<DateTime, String> {
const DateTimeConverter();
@override
DateTime fromJson(String json) => DateTime.parse(json);
@override
String toJson(DateTime object) => object.toIso8601String();
}
class MoneyConverter implements JsonConverter<Money, int> {
const MoneyConverter();
@override
Money fromJson(int json) => Money.fromCents(json);
@override
int toJson(Money object) => object.cents;
}
@JsonSerializable()
class Order {
@DateTimeConverter()
final DateTime createdAt;
@MoneyConverter()
final Money total;
// ...
}
Enums
enum OrderStatus { pending, processing, shipped, delivered, cancelled }
// json_serializable 6.x maps 'pending' → OrderStatus.pending automatically
@JsonSerializable()
class Order {
@JsonKey(unknownEnumValue: OrderStatus.pending) // fallback for unknown values
final OrderStatus status;
// ...
}
Handling a list of objects from an API
// API: { "products": [ {...}, {...} ] }
final Map<String, dynamic> response = jsonDecode(responseBody);
final products = (response['products'] as List)
.map((e) => Product.fromJson(e as Map<String, dynamic>))
.toList();
// API: [ {...}, {...} ] (top-level array)
final List<dynamic> response = jsonDecode(responseBody);
final products = response
.map((e) => Product.fromJson(e as Map<String, dynamic>))
.toList();
With Dio
future<List<Product>> getProducts() async {
final response = await dio.get<List<dynamic>>('/products');
return response.data!
.map((e) => Product.fromJson(e as Map<String, dynamic>))
.toList();
}
Future<Order> getOrder(String id) async {
final response = await dio.get<Map<String, dynamic>>('/orders/$id');
return Order.fromJson(response.data!);
}
Common pitfalls
Not running build_runner after changing model fields. Changing a field name or type and forgetting to regenerate leaves .g.dart out of sync. The generated file still maps the old name and the new code fails at runtime.
dynamic instead of Map<String, dynamic>. jsonDecode() returns dynamic. Casting immediately to Map<String, dynamic> before passing to fromJson prevents hard-to-debug type errors.
Using int for monetary values. Floating-point arithmetic (double) loses precision with money. Either store prices as integers (cents) and convert at display time, or use a Money type. json_serializable supports both patterns via a custom converter.
Sign in to like, dislike, or report.