1. 概述
kotlinx-serialization 的一大亮点是支持多态序列化(polymorphic serialization)。尽管 Kotlin 序列化本质上是静态的,但它依然能优雅地处理运行时动态类型对象的场景。
我们已经在之前的文章中介绍了 kotlinx-serialization 的基本用法和项目配置方式。本文将聚焦于如何高效处理涉及继承关系的类层次结构,尤其是封闭式和开放式多态序列化的实践技巧。
2. 封闭式多态(Closed Polymorphism)
通过 Kotlin 的 sealed classes,我们可以实现所谓的“封闭式多态”或“静态多态”。这种机制限制了子类的数量和范围,使得编译器可以在编译期穷尽所有可能的子类型,从而保证类型安全,并支持编译期确定的多态序列化。
2.1 密封类(Sealed Classes)的应用
使用密封类时,必须显式为所有子类添加 @Serializable
注解。⚠️ 关键点在于:在序列化对象时,其编译期类型必须是父类(即密封类),而不是具体的子类实现。否则无法正确写入类型标识符,导致反序列化失败。
来看一个简单的例子:
@Serializable
sealed class Shape { abstract val edges: Int }
@Serializable
data class Circle(override val edges: Int, val radius: Double) : Shape()
当我们序列化一个 Circle
实例时,只有将其声明为 Shape
类型,才会自动注入类型元信息:
@Test
fun `serializes object with it's type discriminator`() {
// given
val circle: Shape = Circle(edges = 1, radius = 2.0)
// when
val serializedCircle = Json.encodeToString(circle)
val deserializedCircle = Json.decodeFromString<Shape>(serializedCircle)
// then
assertThat(serializedCircle)
.isEqualTo(
"""{"type":"com.baeldung.serialization.kotlinx.polymorphism.Circle","edges":1,"radius":2.0}"""
)
assertThat(deserializedCircle).isInstanceOf(Circle::class.java)
}
✅ 正确姿势:变量声明为 Shape
类型
❌ 踩坑示例:若写成 val circle = Circle(...)
,则不会生成 type
字段,后续无法反序列化回具体类型
2.2 类型标识字段(Type Discriminator)
序列化结果中会包含一个用于区分类型的字段,默认名为 "type"
,它的值是类的全限定名(FQN)。这个字段是反序列化时还原具体类型的关键。
我们可以自定义以下两点:
- 使用
@SerialName("xxx")
修改类在 JSON 中的类型标识值 - 配置
Json
实例,修改类型字段的 JSON 键名
示例如下:
@Serializable
@SerialName("SerialRectangle")
data class Rectangle(override val edges: Int, val width: Double, val height: Double): Shape()
更改类型字段名为 #customDiscriminatorProperty
:
@Test
fun `uses custom serial name and property for object's type discriminator`() {
// given
val jsonConfiguration = Json { classDiscriminator = "#customDiscriminatorProperty" }
val rectangle: Shape = Rectangle(edges = 4, width = 4.0, height = 6.0)
// when
val serializedRectangle = jsonConfiguration.encodeToString(rectangle)
// then
assertThat(serializedRectangle)
.contains(
"""{"#customDiscriminatorProperty":"SerialRectangle","edges":4,"width":4.0,"height":6.0}"""
)
}
📌 注意事项:
- 当前也可使用
@JsonClassDiscriminator
注解,但属于实验性 API ❌ 不建议生产使用 - 如果你的类本身就有名为
type
的属性 ⚠️ 必须通过classDiscriminator
更改默认字段名,否则会抛出IllegalStateException
3. 开放式多态(Open Polymorphism)
对于 open
或 abstract
类,kotlinx-serialization 同样支持多态序列化。但由于子类可能分布在任意模块中,编译期无法预知全部实现,因此需要在运行时显式注册子类关系。
这类设计更灵活,但也少了编译期检查的安全保障 —— 属于是典型的“灵活性 vs 安全性”权衡。
3.1 抽象类与接口的多态序列化
以 Article
抽象类为例:
@Serializable
abstract class Article { abstract val title: String }
@Serializable
@SerialName("KotlinLibraryArticle")
data class KotlinLibraryArticle(override val title: String, val library: String) : Article()
要启用多态序列化,必须通过 SerializersModule
显式注册继承关系:
val jsonFormat = Json {
serializersModule = SerializersModule {
polymorphic(Article::class) {
subclass(KotlinLibraryArticle::class)
}
}
}
之后即可正常序列化:
@Test
fun `serializes open polymorphic object`() {
// given
val article: Article = KotlinLibraryArticle(
title = "Class Inheritance with Kotlinx Serialization",
library = "kotlinx.serialization",
)
// when
val serializedArticle = jsonFormat.encodeToString(article)
// then
assertThat(serializedArticle)
.isEqualTo(
"""{"type":"KotlinLibraryArticle","title":"Class Inheritance with Kotlinx Serialization","library":"kotlinx.serialization"}"""
)
}
📌 接口(Interface)的小细节:
- 接口无需标注
@Serializable
- 因为接口默认被视为可序列化的抽象类型 ✅ 隐式支持多态
3.2 Any 类型的序列化
有时我们面对的是完全未知的类型,比如 Any
。由于 Any
是内置类型,不能加 @Serializable
,所以必须手动注册它所允许的子类型。
示例:
@Test
fun `serializes Any type`() {
// given
val jsonFormat = Json {
serializersModule = SerializersModule {
polymorphic(Any::class) {
subclass(KotlinLibraryArticle::class)
}
}
}
val article: Any = KotlinLibraryArticle(
title = "Class Inheritance with Kotlinx Serialization",
library = "kotlinx.serialization",
)
// when
val serializedArticle = jsonFormat.encodeToString(PolymorphicSerializer(Any::class), article)
// then
assertThat(serializedArticle)
.isEqualTo(
"""{"type":"KotlinLibraryArticle","title":"Class Inheritance with Kotlinx Serialization","library":"kotlinx.serialization"}"""
)
}
⚠️ 高风险提醒:
- 必须传入
PolymorphicSerializer(Any::class)
才能触发多态行为 - 若某个类未提前注册 ❌ 运行时会抛出
SerializationException
- 没有编译期检查,容易埋雷,建议谨慎使用,仅限于插件化、DSL 等动态场景
4. 总结
特性 | 封闭式多态(Sealed) | 开放式多态(Open) |
---|---|---|
安全性 | ✅ 编译期校验,类型安全 | ⚠️ 运行时注册,易出错 |
灵活性 | ❌ 子类固定 | ✅ 可跨模块扩展 |
配置方式 | 注解驱动,零配置 | 需手动注册 SerializersModule |
推荐场景 | 领域模型、状态机 | 插件系统、通用数据容器 |
- 优先使用 sealed class + 多态序列化:类型清晰、安全可靠,适合大多数业务建模场景
- 开放多态适用于高度动态的系统:如需要支持第三方扩展的 SDK 或框架
Any
多态要慎用:虽灵活但破坏类型安全,建议配合白名单机制控制风险
合理利用这些特性,可以让你的 Kotlin 数据模型在保持简洁的同时,具备强大的序列化能力。