随着 Dart 3 正式发布,密封类(sealed class) 成为类型系统的一大新宠。它让开发者能够精确限定类层次结构,编译器借此进行穷举检查,大幅提升代码安全性。然而,在从传统类迁移到密封类的过程中,一个实用问题迅速浮出水面:如何将密封类对象与 JSON 互相转换? 本文整理当前社区主流实践,为你提供一套清晰且可操作的指南。
密封类为何带来序列化挑战?
密封类本质上是一种受限的继承层级:sealed 修饰的父类只能在同一文件内被子类继承。这种设计非常适合表示有限状态(如网络请求状态 Loading、Success、Error)或代数数据类型。但标准的 json_serializable 库在设计时并未专门适配密封类,直接应用会引发代码生成失败或运行时类型判断错误。
核心难点在于:JSON 反序列化时,程序必须根据 JSON 内容动态选择正确的子类构造器。例如,一个名为 Animal 的密封类包含 Dog 和 Cat 子类,当收到包含 "type": "cat" 的 JSON 时,应该实例化 Cat 而非 Dog。这种多态反序列化逻辑,常规的序列化库不会自动生成。
方案一:利用 freezed 包一键搞定
freezed 是 Dart 社区最成熟的不可变数据类生成工具,从 3.x 版本开始原生支持密封类。你只需定义密封类并用 @freezed 注解,它就能自动生成 fromJson 与 toJson,并内置“联合类型”的分派机制:
@freezed
sealed class Animal with _$Animal {
const factory Animal.dog(String name, int age) = Dog;
const factory Animal.cat(String name, bool isIndoor) = Cat;
}
// 使用示例
final animal = Animal.dog('Buddy', 3);
final json = animal.toJson(); // {"type":"dog","name":"Buddy","age":3}
final restored = Animal.fromJson(json); // 自动返回 Dog 实例
优点:零手动模板,自动处理类型字段(默认使用 type 或自定义键名),且完美支持 copyWith、== 重写。
缺点:增加了对第三方包的依赖,且需要运行代码生成器。
方案二:手动实现分派(零依赖)
若希望完全控制序列化逻辑,或项目不允许额外代码生成,可手动实现。关键在于在 JSON 中显式添加“类型标识”,并在 fromJson 工厂方法中根据该标识分派给不同子类:
sealed class Animal {
Map<String, dynamic> toJson();
factory Animal.fromJson(Map<String, dynamic> json) {
switch (json['type']) {
case 'dog': return Dog.fromJson(json);
case 'cat': return Cat.fromJson(json);
default: throw ArgumentError('Unknown type: ${json['type']}');
}
}
}
class Dog implements Animal { /* ... */ }
class Cat implements Animal { /* ... */ }
优点:逻辑透明,无外部依赖,适合简单层级。
缺点:代码量快速膨胀,每增一个子类都要修改 switch;如果漏写某个类型,编译器不会自动提醒(须配合 sealed 的穷举检查手动实现)。
方案三:借助 json_serializable 与自定义 JsonConverter
如果你已经在项目中大量使用 json_serializable,可以为其添加自定义转换器来处理密封类多态。定义 AnimalConverter 继承 JsonConverter,在 fromJson 中根据 type 分派:
class AnimalConverter implements JsonConverter<Animal, Map<String, dynamic>> {
const AnimalConverter();
@override
Animal fromJson(Map<String, dynamic> json) {
// 同上 switch 分派
}
@override
Map<String, dynamic> toJson(Animal object) => object.toJson();
}
然后在密封类中引用该转换器:@JsonSerializable(converters: [AnimalConverter()])。
优点:保持与 json_serializable 生态一致,可与其他类混合序列化。
缺点:仍需要手动维护分派逻辑,且转换器不能被代码生成器自动生成。
社区趋势:为什么要优先考虑 freezed?
从官方 Dart 团队发布的技术博客以及 GitHub 上开源项目的使用统计来看,freezed 已成为处理密封类序列化的事实标准。其核心原因在于:
- 模式匹配友好:生成的类自动包含
when、map等方法,配合 Dart 3 的switch表达式,代码简洁且安全。 - 穷举性:当你新增一个子类时,
freezed会强制更新所有when调用,避免运行时遗漏。 - 性能:编译时生成代码,无反射开销。
当然,如果项目极度追求极致包体积(如 Flutter 插件),手动实现仍是一个可接受选项,但需要配合严格的测试。
总结
Dart 密封类的 JSON 序列化并非无法逾越的鸿沟。开发者可根据项目实际情况选择方案:
| 场景 | 推荐方案 |
|---|---|
| 新项目或可接受代码生成 | freezed |
| 已有 json_serializable 基础设施 | 自定义 JsonConverter |
| 零依赖、小范围使用 | 手动分派 |
未来,Dart 团队可能将密封类的自动化序列化纳入官方库,但在此之前,上述方案足以应对绝大多数生产环境需求。你准备好升级你的 Dart 代码了吗?欢迎在评论区分享你的迁移经验。