23. 密封类的序列化:与Gson/Moshi的兼容性处理
密封类在 Kotlin 里是个好东西。它帮我们限死了类型范围,让 when 表达式变得安全又优雅。但一碰到序列化,事情就没那么美了。
我最早在项目里用密封类做 UI 状态管理时,就踩过这个坑。当时用 Gson 直接序列化一个密封类对象,结果运行时直接崩了——Gson 根本不认识 sealed class 的子类型。嗯,今天我们就来聊聊怎么搞定这个问题。
为什么密封类序列化会出问题?
说白了,Gson 和 Moshi 默认都不知道你的密封类有哪些子类。它们只能看到父类,看不到子类的具体结构。
举个例子:
sealed class UiState {
data class Loading(val progress: Int) : UiState()
data class Success(val data: String) : UiState()
data class Error(val message: String) : UiState()
}
你用 Gson 直接序列化一个 UiState.Success("hello"),Gson 会尝试序列化 UiState 这个抽象类。它不知道具体是哪个子类,也不知道该用哪个构造器反序列化。结果就是抛异常。
IllegalArgumentException,提示无法实例化抽象类。这不是 bug,是 Gson 的设计如此。
解决方案一:自定义 TypeAdapter(Gson)
我个人习惯用自定义 TypeAdapter 来处理。思路很简单:在序列化时,把子类的类型信息也写进去。反序列化时,根据类型信息找到对应的子类来解析。
class UiStateAdapter : JsonSerializer<UiState>, JsonDeserializer<UiState> {
override fun serialize(src: UiState, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
val jsonObject = context.serialize(src, src.javaClass).asJsonObject
jsonObject.addProperty("type", src.javaClass.simpleName)
return jsonObject
}
override fun deserialize(json: JsonElement, typeOfType: T, context: JsonDeserializationContext): UiState {
val jsonObject = json.asJsonObject
val typeName = jsonObject.get("type").asString
return when (typeName) {
"Loading" -> context.deserialize(json, Loading::class.java)
"Success" -> context.deserialize(json, Success::class.java)
"Error" -> context.deserialize(json, Error::class.java)
else -> throw JsonParseException("Unknown type: $typeName")
}
}
}
注册到 Gson 里:
val gson = GsonBuilder()
.registerTypeAdapter(UiState::class.java, UiStateAdapter())
.create()
这样序列化出来的 JSON 会多一个 type 字段,用来标记子类类型。反序列化时就能准确还原了。
__sealed_type__,避免和业务字段冲突。我在项目里就吃过字段名冲突的亏,排查了半天才发现是 type 字段被覆盖了。
解决方案二:Moshi 的 PolymorphicJsonAdapterFactory
如果你用 Moshi,事情就简单多了。Moshi 官方提供了 PolymorphicJsonAdapterFactory,专门处理这种多态序列化场景。
val moshi = Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(UiState::class.java, "type")
.withSubtype(Loading::class.java, "loading")
.withSubtype(Success::class.java, "success")
.withSubtype(Error::class.java, "error"))
.build()
你看,代码量少了很多。Moshi 会自动在 JSON 里加一个 type 字段,值就是你在 withSubtype 里指定的标签。
序列化结果大概长这样:
{
"type": "success",
"data": "hello"
}
反序列化时,Moshi 根据 type 字段找到对应的子类,然后解析剩下的字段。整个过程很丝滑。
方案对比:Gson vs Moshi
| 维度 | Gson | Moshi |
|---|---|---|
| 配置复杂度 | 需要手写 TypeAdapter | 一行工厂方法搞定 |
| 类型安全 | 弱(运行时反射) | 强(编译期代码生成) |
| 性能 | 中等(反射开销) | 较好(无反射) |
| Kotlin 支持 | 需要额外适配 | 原生支持 |
| 推荐场景 | 遗留项目迁移 | 新项目首选 |
我个人更推荐 Moshi。尤其是新项目,直接用 Moshi 省心很多。但如果你在维护老项目,Gson 方案也能用,就是代码量多一些。
避坑指南:我曾经踩过的坑
我曾经在项目里用密封类做网络请求的状态管理,结果序列化时出了几个问题,分享出来给大家避避坑。
坑一:子类字段名冲突
如果两个子类有同名字段但类型不同,反序列化时会出问题。比如 Loading 有个 message: String,Error 也有个 message: Int。Gson 和 Moshi 都会懵。我建议子类字段名尽量保持唯一,或者用 @Json(name = "...") 注解区分。
坑二:嵌套密封类
密封类里嵌套密封类,序列化复杂度会指数级上升。我遇到过三层嵌套的密封类,调试反序列化花了一整天。后来我重构了设计,把嵌套层级控制在两层以内。
坑三:默认值问题
密封类的 data class 子类如果有默认参数,反序列化时可能不会按预期工作。Moshi 对默认参数的支持比 Gson 好一些,但也不是完美的。我建议在子类里不要依赖默认参数,显式写出所有字段。
核心逻辑图
下面这张图展示了密封类序列化的核心流程:
总结一下
密封类的序列化,说白了就是解决「父类不知道子类是谁」的问题。Gson 需要你手写 TypeAdapter,Moshi 提供了现成的工厂方法。我个人更倾向 Moshi,代码量少,类型安全,性能也好。
如果你还在用 Gson,也别急着换。手写 TypeAdapter 虽然麻烦,但胜在可控。只要注意我前面提到的几个坑,基本不会出大问题。
嗯,关于密封类序列化就聊这么多。记住一点:不管用哪个库,核心思路都是在 JSON 里加一个类型标识字段。这个思路搞清楚了,后面遇到再复杂的场景也能应对。