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)

对于 openabstract 类,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 数据模型在保持简洁的同时,具备强大的序列化能力。


原始标题:Class Inheritance with Kotlinx Serialization