那些年,我们在 toJson 上踩过的坑,以及最终的破局之道
写在前面
在 Flutter 开发中,JSON 序列化是一个绕不开的话题。对于简单的数据结构,json_serializable 可以轻松应对。但当你的数据结构中出现 List 这种动态类型时,一切就变得不那么优雅了。
本文将深入探讨动态 List 的 JSON 序列化问题,并给出多种解决方案,帮助你在不同场景下做出最优选择。
一、问题的起源:为什么 toJson 会失效?
先来看一个典型场景:
dart
class Order {
String orderId;
List<dynamic> items; // 可能包含商品、优惠券、备注等多种类型
Order(this.orderId, this.items);
// 直接使用 json_serializable 生成的 toJson 会怎样?
}
当你尝试序列化这个 Order 对象时,可能会遇到以下错误:
Converting object to an encodable object failed:
Instance of 'Product'
根本原因:Dart 的 jsonEncode 函数只知道如何处理基础类型(String、int、bool、Map、List),当遇到自定义对象时,它不知道该如何处理。
二、解决方案全景图
根据不同的业务复杂度,我们有四种解决方案:
方案一:手动类型映射(适合类型较少的场景)
最直观的方式是在 toJson 中遍历列表,根据运行时类型进行转换:
dart
class Order {
String orderId;
List<dynamic> items;
Map<String, dynamic> toJson() {
return {
'orderId': orderId,
'items': items.map((item) {
if (item is Product) {
return {'type': 'product', 'data': item.toJson()};
} else if (item is Coupon) {
return {'type': 'coupon', 'data': item.toJson()};
} else if (item is String || item is num || item is bool) {
return item; // 基础类型直接返回
} else {
throw Exception('Unsupported type: ${item.runtimeType}');
}
}).toList(),
};
}
}
优点:逻辑清晰,易于理解
缺点:每次新增类型都需要修改代码
方案二:接口约束(适合类型可预期的场景)
定义一个统一的序列化接口,所有可序列化的对象都实现它:
dart
// 1. 定义接口
abstract class JsonSerializable {
Map<String, dynamic> toJson();
}
// 2. 子类实现
class Product implements JsonSerializable {
String name;
double price;
@override
Map<String, dynamic> toJson() => {
'name': name,
'price': price,
};
}
// 3. 列表声明为接口类型
class Order {
String orderId;
List<JsonSerializable> items; // 类型约束
Map<String, dynamic> toJson() {
return {
'orderId': orderId,
'items': items.map((item) => item.toJson()).toList(),
};
}
}
优点:类型安全,编译期检查
缺点:无法存储基础类型(除非也包装成接口实现)
方案三:json_serializable + 自定义转换(适合大型项目)
利用 @JsonKey 注解的 toJson 和 fromJson 参数:
dart
import 'package:json_annotation/json_annotation.dart';
part 'order.g.dart';
@JsonSerializable()
class Order {
String orderId;
@JsonKey(toJson: _itemsToJson, fromJson: _itemsFromJson)
List<dynamic> items;
Order(this.orderId, this.items);
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
Map<String, dynamic> toJson() => _$OrderToJson(this);
}
// 自定义序列化逻辑
List<dynamic> _itemsToJson(List<dynamic> items) {
return items.map((item) {
if (item is Product) {
return {'__type__': 'Product', ...item.toJson()};
} else if (item is Coupon) {
return {'__type__': 'Coupon', ...item.toJson()};
}
return item;
}).toList();
}
List<dynamic> _itemsFromJson(List<dynamic> items) {
return items.map((item) {
final type = item['__type__'];
switch (type) {
case 'Product':
return Product.fromJson(item);
case 'Coupon':
return Coupon.fromJson(item);
default:
return item;
}
}).toList();
}
优点:与现有工具链完美集成,支持复杂类型
缺点:需要维护类型标识字段
方案四:递归转换器(适合深度嵌套场景)
当动态 List 中还嵌套着其他 List 或 Map 时,需要一个递归解决方案:
dart
dynamic convertToJson(dynamic obj) {
if (obj == null) return null;
// 处理自定义对象
if (obj is JsonSerializable) {
return obj.toJson();
}
// 递归处理列表
if (obj is List) {
return obj.map(convertToJson).toList();
}
// 递归处理 Map
if (obj is Map) {
return obj.map((key, value) => MapEntry(key, convertToJson(value)));
}
// 基础类型直接返回
return obj;
}
// 使用示例
class Order {
String orderId;
List<dynamic> items;
Map<String, dynamic> toJson() {
return {
'orderId': orderId,
'items': convertToJson(items),
};
}
}
优点:能处理任意复杂的嵌套结构
缺点:性能略差,不适合超大列表
三、实战案例:电商订单系统
假设我们正在开发一个电商 App 的订单系统,订单中的 items 可能包含:
· 商品信息(Product)
· 优惠券信息(Coupon)
· 普通文本备注(String)
· 赠品信息(Gift)
来看一个完整的实现:
dart
// 基类
abstract class OrderItem {
String get type;
Map<String, dynamic> toJson();
}
// 商品类
class Product extends OrderItem {
@override
String get type => 'product';
String name;
int quantity;
double price;
Product({required this.name, required this.quantity, required this.price});
@override
Map<String, dynamic> toJson() => {
'type': type,
'name': name,
'quantity': quantity,
'price': price,
};
}
// 优惠券类
class Coupon extends OrderItem {
@override
String get type => 'coupon';
String code;
double discount;
Coupon({required this.code, required this.discount});
@override
Map<String, dynamic> toJson() => {
'type': type,
'code': code,
'discount': discount,
};
}
// 订单类
class Order {
String orderId;
List<OrderItem> items; // 使用基类类型
String? remark; // 备注(普通字符串)
Order({required this.orderId, required this.items, this.remark});
Map<String, dynamic> toJson() {
return {
'orderId': orderId,
'items': items.map((item) => item.toJson()).toList(),
'remark': remark,
};
}
// 反序列化
factory Order.fromJson(Map<String, dynamic> json) {
return Order(
orderId: json['orderId'],
items: (json['items'] as List).map((item) {
switch (item['type']) {
case 'product':
return Product(
name: item['name'],
quantity: item['quantity'],
price: item['price'],
);
case 'coupon':
return Coupon(
code: item['code'],
discount: item['discount'],
);
default:
throw Exception('Unknown item type');
}
}).toList(),
remark: json['remark'],
);
}
}
// 使用示例
void main() {
final order = Order(
orderId: 'ORD-001',
items: [
Product(name: 'iPhone 15', quantity: 1, price: 5999),
Coupon(code: 'SAVE100', discount: 100),
],
remark: '请在工作日送货',
);
final json = order.toJson();
print(jsonEncode(json));
// 输出:{"orderId":"ORD-001","items":[{"type":"product","name":"iPhone 15","quantity":1,"price":5999},{"type":"coupon","code":"SAVE100","discount":100}],"remark":"请在工作日送货"}
}
四、性能优化建议
- 缓存转换结果:如果同一个对象需要多次序列化,考虑缓存其 JSON 表示
- 使用 const 构造函数:对于不变的配置类,使用 const 可以减少内存分配
- 避免深度递归:对于深度嵌套超过 100 层的结构,考虑迭代方案替代递归
五、总结与选型建议
场景 推荐方案 理由
小型项目,类型固定 方案一(手动映射) 简单直接,无需引入复杂模式
中型项目,类型可控 方案二(接口约束) 类型安全,易于维护
大型项目,需求多变 方案三(json_serializable) 工具链完善,减少样板代码
遗留系统,结构复杂 方案四(递归转换) 兼容性强,适应各种数据结构
写在最后
动态类型的 JSON 序列化是 Flutter 开发中的一个经典难题,但通过合理的设计模式,我们可以优雅地解决它。核心思想是:明确类型边界,统一序列化接口,善用递归处理。
希望这篇文章能帮助你少踩一些坑。如果你有更好的解决方案,欢迎在评论区分享讨论!