解锁Kotlin Serialization高阶玩法:详解4种自定义序列化器与动态上下文策略

点击跳转「搜狐技术产品」公众号原文

Kotlin Serialization 框架通过 @Serializable 注解提供自动序列化功能。与 Gson、Moshi 等基于运行时反射的框架不同,Kotlin Serialization 在编译期生成序列化代码,因此在类型安全、运行性能和混淆兼容性方面具有天然优势。

在控制序列化格式、处理第三方类或实现动态序列化策略等高级场景中,仅靠注解往往无法满足企业级需求。本文将系统性地介绍如何创建、绑定和使用自定义序列化器,涵盖从基本实现到高级上下文序列化的完整知识体系。全文在讲解 API 用法的同时,会适度展开编译期与运行期的协作机制,以帮助开发者建立更稳固的工程化认知。


01

序列化器基础

1. 什么是序列化器?

序列化器是实现了 KSerializer<T> 接口的核心组件。在 Kotlin Serialization 的设计中,框架的工作流可以清晰地划分为两个阶段:

  1. 编译期 :编译器扫描 @Serializable 注解,为每个类生成 KSerializer<T> 实现及其 SerialDescriptor(结构蓝图)。

  2. 运行期EncoderDecoder 负责与具体格式(如 JSON、Protobuf)交互,执行真正的 IO 操作。

需要特别澄清的是,SerialDescriptor不参与任何字节流的读写 ,它仅用于描述类型结构(字段名、顺序、类型等),供格式层进行映射和校验。每个 @Serializable 注解的类都会自动获得一个由编译器插件生成的序列化器,这也是 Kotlin Serialization 无需反射、混淆安全的核心原因。

理解这一点非常重要:序列化器不是"黑盒",它的行为在编译期就已经被固化下来,这既是性能的来源,也是稳定性的保障。

2. 获取序列化器实例

在 Kotlin Serialization 中,有多种方式可以获取序列化器实例,不同方式适用于不同的架构层级。理解这些方式,有助于在复杂工程中灵活应对泛型、集合及第三方类型的序列化需求。

(1)获取插件生成的序列化器

对于使用 @Serializable 注解的类,可以直接通过其伴生对象的 .serializer() 方法获取。这是最常用、最安全的方式,适用于所有可控的业务模型。

复制代码
@Serializable
@SerialName("Color")
class Color(val rgb: Int)

fun main() {
    // 获取由编译器插件自动生成的序列化器
    val colorSerializer: KSerializer<Color> = Color.serializer()
    // 打印描述符,查看编译器推导出的结构
    println(colorSerializer.descriptor)
    // 输出: Color(rgb: kotlin.Int)
}

工程意义:这种方式获取的序列化器是"零成本抽象"的典范,所有逻辑均在编译期生成,运行时无任何反射开销。

(2)获取泛型类的序列化器

泛型类的序列化器需要通过其 .serializer() 方法,并显式传入类型参数的序列化器来实例化。这是因为泛型擦除的存在,编译器无法在运行时自动推断 T 的实际类型。

复制代码
@Serializable
@SerialName("Box")
class Box<T>(val contents: T)

fun main() {
    // 必须显式传入 Color.serializer() 作为类型参数
    val boxedColorSerializer = Box.serializer(Color.serializer())
    println(boxedColorSerializer.descriptor)
    // 输出: Box(contents: Color)
}

工程意义 :在封装通用容器(如 Result<T>Page<T>)时必不可少,也是 SDK 设计中处理泛型响应的基础。注意,泛型序列化器本身也是一个普通的 KSerializer,可以被继续传递和组合。

(3)获取基本类型序列化器

Kotlin 基本类型(IntString 等)的序列化器可以通过扩展函数获取,常用于手写序列化器时的委托。

复制代码
fun main() {
    // Int 的序列化器
    val intSerializer: KSerializer<Int> = Int.serializer()
    println(intSerializer.descriptor)
    // 输出: PrimitiveDescriptor(kotlin.Int)

    // String 的序列化器
    val stringSerializer: KSerializer<String> = String.serializer()
    println(stringSerializer.descriptor)
    // 输出: PrimitiveDescriptor(kotlin.String)
}

工程意义:基本类型序列化器是所有复杂序列化器的基石。在编写自定义序列化器时,我们通常通过组合这些基本序列化器来降低实现复杂度。

(4)构建集合序列化器

列表、集合、映射等内置集合的序列化器需要显式构造,这一点常被初学者忽视。集合序列化器本质上是对元素序列化器的包装。

复制代码
fun main() {
    // 构造一个 List<String> 的序列化器
    val stringListSerializer: KSerializer<List<String>> =
        ListSerializer(String.serializer())
    println(stringListSerializer.descriptor)
}

输出:

复制代码
kotlin.collections.ArrayList(PrimitiveDescriptor(kotlin.String))

工程意义 :在动态数据结构或手写复合序列化器中,集合序列化器常作为委托对象使用。例如,当你需要将一个对象序列化为数组时,往往会委托给 ListSerializerMapSerializer

(5)使用顶层序列化器函数

在类型可被编译器推断的位置,可以使用顶层的泛型函数 serializer<T>() 来获取任意类型的序列化器。这是一个非常便利的语法糖。

复制代码
fun main() {
    // 编译器根据赋值目标自动推断类型
    val stringToColorMapSerializer: KSerializer<Map<String, Color>> = serializer()
    println(stringToColorMapSerializer.descriptor)
}

输出:

复制代码
kotlin.collections.LinkedHashMap(PrimitiveDescriptor(kotlin.String), Color(rgb: kotlin.Int))

注意事项 :该函数依赖编译器类型推断,不适合作为跨模块或 Java 调用方的公开 API。在复杂的泛型嵌套场景下,显式使用 serializer() 方法往往比顶层函数更具可读性和安全性。

3. 序列化器描述符 (Descriptor) 的重要性

每个 KSerializer 都必须提供准确的 descriptor 属性。它是一个 SerialDescriptor 实例,描述了类型的结构和每个元素的序列化信息。

关键原则descriptor 必须与 serialize / deserialize 方法实际调用的编码/解码方法严格对应 。例如,若 descriptor 声明为 PrimitiveKind.STRING,则实现中必须调用 encodeString / decodeString

任何偏差虽然在 JSON 等宽松格式下可能"看似正常",但在 Protobuf、CBOR 等严格格式下会直接导致运行时异常,甚至在库升级后引发隐蔽的兼容性问题。因此,保持描述符与实现的一致性,是序列化稳定性的工程红线。

此外,描述符还在以下场景发挥关键作用:

多态序列化:通过描述符识别具体的子类型。

Schema 生成:根据描述符自动生成 JSON Schema 或 Protobuf IDL。

验证:在解码前验证数据结构的合法性。


02

自定义序列化器

当自动生成的序列化器不符合需求时,我们需要编写自定义序列化器。这适用于控制输出格式、序列化第三方类或实现特殊业务逻辑。

1. 基本类型序列化器 (Primitive Serializer)

基本类型序列化器适用于需要将复杂对象"扁平化"为单个值的场景,如将 Color 对象转换为十六进制字符串。

复制代码
object ColorAsStringSerializer : KSerializer<Color> {
    // 描述符必须准确声明为字符串类型
    override val descriptor: SerialDescriptor =
        PrimitiveSerialDescriptor("my.app.Color", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: Color) {
        // 将 RGB 整数转换为十六进制字符串,并补齐至6位
        val string = value.rgb.toString(16).padStart(6, '0')
        encoder.encodeString(string) // 调用 encodeString
    }

    override fun deserialize(decoder: Decoder): Color {
        val string = decoder.decodeString() // 调用 decodeString
        return Color(string.toInt(16))
    }
}

// 绑定到类
@Serializable(with = ColorAsStringSerializer::class)
class Color(val rgb: Int)

fun main() {
    val green = Color(0x00ff00)
    println(Json.encodeToString(green)) // 输出(含 JSON 引号): "00ff00"

    // decodeFromString 接收的是完整 JSON 文本
    // JSON 字符串必须用双引号包裹,因此在 Kotlin 字符串中需要转义
    val decoded = Json.decodeFromString<Color>("\"00ff00\"")
    println(decoded.rgb) // 输出: 65280
}

关键点:

PrimitiveKind 必须与实际调用的 encode/decode 方法匹配。

• 描述符的名称建议包含应用包名,避免与库中其他描述符冲突。

• 这种方式的优点是简单直观,缺点是丧失了对象的结构信息(如 RGB 分量),不适合复杂对象的序列化。

2. 委托序列化器 (Delegating Serializers)

委托序列化器适用于将对象转换为另一种 Kotlin 内置或已可序列化的结构,然后将实际的序列化工作"委托"给这个中间结构的序列化器。这是一种"组合优于继承"思想的体现。

复制代码
import kotlinx.serialization.builtins.IntArraySerializer

class ColorIntArraySerializer : KSerializer<Color> {
    // 使用 IntArraySerializer 作为委托
    private val delegateSerializer = IntArraySerializer()

    // 创建新的描述符,不能直接使用委托者的描述符
    // 因为我们的类型是 Color,而不是 IntArray
    override val descriptor =        SerialDescriptor("my.app.Color", delegateSerializer.descriptor)

    overridefun serialize(encoder: Encoder, value: Color) {
        // 将 Color 转换为 IntArray
        val data = intArrayOf(            (value.rgb shr 16) and 0xFF, // Red            (value.rgb shr 8) and 0xFF,  // Green            value.rgb and 0xFF           // Blue        )
        // 委托给 IntArraySerializer 进行实际的编码        encoder.encodeSerializableValue(delegateSerializer, data)    }

    overridefun deserialize(decoder: Decoder): Color {
        // 委托给 IntArraySerializer 进行实际的解码
        val array = decoder.decodeSerializableValue(delegateSerializer)
        // 防御性校验:Int 数组长度由实际数据决定,此处确保至少 3 个元素
        if (array.size < 3) {
            throw SerializationException(
                "Invalid IntArray length for Color: expected at least 3, got ${array.size}"            )        }
        // 将 IntArray 还原为 Color
        return Color((array[0] shl 16) or (array[1] shl 8) or array[2])    }}

@Serializable(with = ColorIntArraySerializer::class)
class Color(val rgb: Int)fun main() {
    val red = Color(0xFF0000)    println(Json.encodeToString(red)) // 输出: [255,0,0]
}

工程意义:委托模式极大地减少了重复代码。当你发现自定义序列化逻辑与某个现有序列化器高度相似时,优先考虑委托。它也使得序列化格式与业务逻辑解耦,便于后期维护。

3. 通过代理类的复合序列化器 (Composite serializer via surrogate) - 推荐

这是实现复杂对象序列化最简便、推荐的方法。定义一个临时"代理类"(Surrogate Class),其属性结构就是你想要的序列化格式。这种方式复用了编译器为代理类生成的序列化逻辑。

复制代码
// 1. 定义私有代理类
// 代理类通常标记为 private,避免污染命名空间
// 注意:@SerialName 在多态序列化中用于区分子类型,在代理类模式下通常无实际效果,
// 因为 JSON 输出由 ColorSerializer 控制,而非代理类本身。此处保留仅为演示。
@Serializable
private class ColorSurrogate(val r: Int, val g: Int, val b: Int) {
    init {        require(r in 0..255 && g in 0..255 && b in 0..255)    }}

// 2. 创建序列化器,委托给代理类的序列化器
object ColorSerializer : KSerializer<Color> {
    override val descriptor: SerialDescriptor =        SerialDescriptor("my.app.Color", ColorSurrogate.serializer().descriptor)

    overridefun serialize(encoder: Encoder, value: Color) {
        // 将领域对象转换为代理对象
        val surrogate = ColorSurrogate(            (value.rgb shr 16) and 0xff,            (value.rgb shr 8) and 0xff,            value.rgb and 0xff        )
        // 委托给代理类的序列化器        encoder.encodeSerializableValue(ColorSurrogate.serializer(), surrogate)    }

    overridefun deserialize(decoder: Decoder): Color {
        // 委托给代理类的序列化器
        val surrogate = decoder.decodeSerializableValue(ColorSurrogate.serializer())
        // 将代理对象转换回领域对象
        return Color((surrogate.r shl 16) or (surrogate.g shl 8) or surrogate.b)    }}

@Serializable(with = ColorSerializer::class)
data class Color(val rgb: Int)fun main() {
    val blue = Color(0x0000FF)    println(Json.encodeToString(blue)) // 输出: {"r":0,"g":0,"b":255}
}

优势

代码简洁 :避免了手动调用 encodeStructuredecodeElementIndex 的繁琐。

安全性高:复用编译器生成的代码,减少了手写逻辑引入 Bug 的风险。

易于维护:当代理类需要新增字段时,编译器会自动处理大部分序列化逻辑。

这是兼顾安全性与开发效率的最佳实践,除非有特殊需求,否则应优先选择此模式。

4. 手动编写的复合序列化器 (Handwritten composite serializer)

当需要完全控制序列化过程,或代理类模式不适用时(如需要动态结构),可以完全手动编写序列化器。这是最复杂但最灵活的方式。

复制代码
object ColorAsObjectSerializer : KSerializer<Color> {
    // 1. 手动构建类描述符
    override val descriptor: SerialDescriptor =        buildClassSerialDescriptor("my.app.Color") {            element<Int>("r")            element<Int>("g")            element<Int>("b")        }

    // 2. 序列化:必须严格按照描述符定义的顺序写入元素
    overridefun serialize(encoder: Encoder, value: Color) =        encoder.encodeStructure(descriptor) {
            // 注意:索引必须与 buildClassSerialDescriptor 中的顺序一致            encodeIntElement(descriptor, 0, (value.rgb shr 16) and 0xff)            encodeIntElement(descriptor, 1, (value.rgb shr 8) and 0xff)            encodeIntElement(descriptor, 2, value.rgb and 0xff)        }

    // 3. 反序列化:需要处理属性可能以任意顺序出现的情况
    overridefun deserialize(decoder: Decoder): Color =        decoder.decodeStructure(descriptor) {
            var r: Int? = null
            var g: Int? = null
            var b: Int? = null
            // 循环解码,直到所有元素处理完毕
            while (true) {
                when (val index = decodeElementIndex(descriptor)) {
                    0 -> r = decodeIntElement(descriptor, 0)
                    1 -> g = decodeIntElement(descriptor, 1)
                    2 -> b = decodeIntElement(descriptor, 2)                    CompositeDecoder.DECODE_DONE -> break
                    else -> error("Unexpected index: $index")                }            }
            // 显式检查,意图清晰            require(r != null) { "Missing field 'r'" }            require(g != null) { "Missing field 'g'" }            require(b != null) { "Missing field 'b'" }
            // 数据完整性校验            require(r in 0..255 && g in 0..255 && b in 0..255) { "Color values out of range" }            Color((r shl 16) or (g shl 8) or b)        }
}

工程考量

顺序敏感性:手动序列化对字段顺序极其敏感,任何修改都可能导致不兼容。

容错性decodeElementIndex 的设计允许格式在扩展字段时保持向后兼容。

适用场景:通常用于序列化框架开发、极高性能要求的场景或对接遗留系统。

5. 实验性顺序解码协议 (Sequential decoding protocol)

对于支持顺序访问的格式(如 JSON 数组),可以使用更高效的解码方式。这利用了格式的特性,避免了 while 循环和 decodeElementIndex 的开销。

复制代码
override fun deserialize(decoder: Decoder): Color =        decoder.decodeStructure(descriptor) {
            var r: Int? = null
            var g: Int? = null
            var b: Int? = null
            @OptIn(ExperimentalSerializationApi::class)
            if (decodeSequentially()) { // 格式支持顺序访问
                // 顺序读取,性能更优
                // 注意:顺序解码虽理论上字段齐全,但工程上仍需校验                r = decodeIntElement(descriptor, 0)                g = decodeIntElement(descriptor, 1)                b = decodeIntElement(descriptor, 2)            } else {
                // 回退到原有的循环逻辑,处理乱序情况
                while (true) {
                    when (val index = decodeElementIndex(descriptor)) {
                        0 -> r = decodeIntElement(descriptor, 0)
                        1 -> g = decodeIntElement(descriptor, 1)
                        2 -> b = decodeIntElement(descriptor, 2)                        CompositeDecoder.DECODE_DONE -> break
                        else -> error("Unexpected index: $index")                    }                }            }
            // 统一校验:确保字段非空且值域合法            require(r != null) { "Missing field 'r'" }            require(g != null) { "Missing field 'g'" }            require(b != null) { "Missing field 'b'" }            require(r in 0..255 && g in 0..255 && b in 0..255) { "Color values out of range" }            Color((r shl 16) or (g shl 8) or b)
        }

性能提示 :虽然顺序解码能带来微小的性能提升,但它依赖于具体的 Decoder 实现。在编写通用序列化器时,保留回退逻辑(Fallback)是更稳健的做法。


03

绑定和使用自定义序列化器

定义好序列化器后,需要告诉框架何时使用它。绑定策略从具体到全局,提供了多种选择,合理运用可以显著降低模块间的耦合度。

1. 类级绑定

最直接的方式,适用于你可以修改源码且序列化策略全局统一的类。通过 @Serializable(with = ...) 注解直接关联。

复制代码
@Serializable(with = ColorAsStringSerializer::class)
class Color(val rgb: Int)

特点:意图明确,集中管理,但缺乏灵活性。

2. 属性级绑定

为特定属性指定序列化器,不影响类的其他属性。这在集成第三方 API 时尤为常见。

复制代码

@Serializable
class ProgrammingLanguage(
val name: String,
@Serializable(with = DateAsLongSerializer::class) // 仅为这个属性指定
val stableReleaseDate: java.util.Date
)

特点:精准控制,侵入性低,但配置较为分散。

3. 泛型类型参数绑定

为集合或泛型类中的类型参数指定序列化器,解决容器内元素的序列化问题。

复制代码
@Serializable
class Timeline(
    val events: List<@Serializable(DateAsLongSerializer::class) java.util.Date>
)

特点:解决了泛型擦除带来的序列化难题,是处理集合数据的利器。

4. 文件级绑

使用文件级注解,为该文件中的所有相关类统一指定序列化器,适合模块化架构。

复制代码
@file:UseSerializers(DateAsLongSerializer::class)

package com.example.model

@Serializable
class Event(val name: String, val date: java.util.Date) // date 自动使用 DateAsLongSerializer

特点:模块内统一策略,减少了重复的注解噪音。

5. 通过类型别名全局配置

创建带注解的类型别名,实现优雅、类型安全的全局配置,常用于区分同一类型的不同业务含义。

复制代码
// 定义类型别名
typealias ApiDate = @Serializable(DateAsLongSerializer::class) java.util.Date
typealias IsoDate = @Serializable(DateAsIsoStringSerializer::class) java.util.Date

// 使用
@Serializable
data class ApiResponse(
    val data: String,
    val createdAt: ApiDate,      // 序列化为时间戳
    val updatedAt: IsoDate       // 序列化为 ISO 字符串
)

特点:语义清晰,类型安全,是大型项目中管理复杂序列化策略的推荐做法。

6. 手动传递序列化器

在调用序列化/反序列化函数时,显式提供序列化器实例。这适用于序列化顶级对象或无法修改类定义的情况。

复制代码
fun main() {
    val date = java.util.Date()
    // 序列化
    println(Json.encodeToString(DateAsLongSerializer, date))
    // 反序列化
    val decoded = Json.decodeFromString(DateAsLongSerializer, "1672531200000")
}

特点:灵活性最高,但破坏了 API 的简洁性,通常用于一次性或临时的序列化需求。


04

高级主题

1. 为泛型类创建自定义序列化器

泛型类的自定义序列化器必须是类(而非对象),其主构造函数需要接收与类型参数数量对应的 KSerializer 参数。这是框架为了绕过 JVM 泛型擦除所做的设计。

复制代码
@Serializable(with = BoxSerializer::class)
data class Box<T>(val contents: T)

class BoxSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Box<T>> {
    override val descriptor: SerialDescriptor =
        SerialDescriptor("my.app.Box", dataSerializer.descriptor)

    override fun serialize(encoder: Encoder, value: Box<T>) =
        encoder.encodeSerializableValue(dataSerializer, value.contents)

    override fun deserialize(decoder: Decoder): Box<T> {
        return Box(dataSerializer.deserialize(decoder))
    }
}

@Serializable
data class Project(val name: String)

fun main() {
    val box = Box(Project("kotlinx.serialization"))
    val string = Json.encodeToString(box)
    println(string)
    println(Json.decodeFromString<Box<Project>>(string))
}

深度解析 :编译器在处理 Box.serializer(...) 时,实际上是在内部创建了 BoxSerializer 的实例。理解这一点有助于你阅读编译后的字节码,排查泛型序列化相关的问题。

2. 同时使用插件生成和自定义序列化器

通过实验性的 @KeepGeneratedSerializer 注解,可以强制编译器在指定自定义序列化器的同时,保留自动生成的序列化器 。否则,在使用 @Serializable(with = ...) 时,编译器插件会丢弃默认实现。

通过该注解保留的默认序列化器,可通过 generatedSerializer() 显式调用,从而实现自定义与默认行为的共存与切换。

复制代码
@OptIn(ExperimentalSerializationApi::class)
@KeepGeneratedSerializer
@Serializable(with = ColorAsStringSerializer::class)
class Color(val rgb: Int)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    val green = Color(0x00ff00)
    // 使用自定义序列化器
    println(Json.encodeToString(green)) // 输出: "00ff00"
    // 使用插件生成的序列化器(generatedSerializer 为实验性 API)
    println(Json.encodeToString(Color.generatedSerializer(), green)) // 输出: {"rgb":65280}
}

用途

回退策略:当自定义序列化器出现问题时,可以快速切回默认实现。

继承体系:在复杂的类继承结构中,有时需要调用父类的默认序列化逻辑。

测试:对比自定义行为与默认行为的差异。

3. 上下文序列化 (Contextual Serialization)

上下文序列化是一种动态策略,允许在运行时决定使用哪个序列化器,而不是在编译时静态绑定。这是构建灵活库和框架的关键技术。

(1)声明上下文依赖

在需要使用动态策略的字段上,使用 @Contextual 注解。这相当于告诉编译器:"这个字段的序列化器请在运行时环境中查找"。

@Serializable
class Event(
val name: String,
@Contextual // 表示序列化器将在运行时上下文中查找
val timestamp: java.util.Date
)

(2)定义不同的序列化策略

定义多个序列化器,例如将 Date 序列化为时间戳或 ISO 字符串。

复制代码
object DateAsLongSerializer : KSerializer<java.util.Date> {
    override val descriptor: SerialDescriptor =
        PrimitiveSerialDescriptor("my.app.DateAsLong", PrimitiveKind.LONG)
    override fun serialize(encoder: Encoder, value: java.util.Date) =
        encoder.encodeLong(value.time)
    override fun deserialize(decoder: Decoder): java.util.Date =
        java.util.Date(decoder.decodeLong())
}

object DateAsIsoStringSerializer : KSerializer<java.util.Date> {
    // ⚠️ 示例用代码:SimpleDateFormat 非线程安全,生产环境建议替换为 DateTimeFormatter
    private val isoFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").apply {
        timeZone = java.util.TimeZone.getTimeZone("UTC")
    }
    override val descriptor: SerialDescriptor =
        PrimitiveSerialDescriptor("my.app.DateAsIso", PrimitiveKind.STRING)
    override fun serialize(encoder: Encoder, value: java.util.Date) =
        encoder.encodeString(isoFormat.format(value))
    override fun deserialize(decoder: Decoder): java.util.Date =
        isoFormat.parse(decoder.decodeString())
}
(3)创建序列化模块并配置格式

构建不同的 SerializersModule,并在创建 Json 实例时注入。这是连接"声明"与"实现"的桥梁。

复制代码
// 创建支持不同策略的模块
val timestampModule = SerializersModule {
    contextual(java.util.Date::class, DateAsLongSerializer) // 注册时间戳策略
}

val isoModule = SerializersModule {
    contextual(java.util.Date::class, DateAsIsoStringSerializer) // 注册 ISO 字符串策略
}

// 创建配置好的 Json 实例
val timestampJson = Json { serializersModule = timestampModule }
val isoJson = Json { serializersModule = isoModule }

fun main() {
    val event = Event("Release", java.util.Date())
    println(timestampJson.encodeToString(event))
    // 输出: {"name":"Release","timestamp":1672531200000}
    println(isoJson.encodeToString(event))
    // 输出: {"name":"Release","timestamp":"2023-01-01T00:00:00Z"}
}
(4)为泛型类注册上下文序列化器

为泛型类注册上下文序列化器时,不能直接注册实例,必须使用工厂函数。因为编译器需要知道如何处理泛型参数。

复制代码
val module = SerializersModule {
    // 普通类
    contextual(java.util.Date::class, DateAsLongSerializer)
    // 泛型类:使用工厂函数注册
    contextual(Box::class) { args ->
        // args: List<KSerializer<*>>
        // 框架保证 args 的顺序与泛型参数顺序一致(如 Box<T> 对应 args[0])
        // 注意:此处发生了隐式的 unchecked cast(KSerializer<*> -> KSerializer<T>),
        // 这是 SerializersModule 的标准用法,由框架在调用时保证类型安全。
        BoxSerializer(args[0])
    }
}

应用场景 :SDK/库开发,为下游开发者提供可配置的序列化行为;支持多版本 API 协议,实现平滑升级。例如,你可以根据用户请求的 API 版本(V1 或 V2)动态切换 SerializersModule

复制代码
enum class ApiVersion { V1, V2 }

// V1:Date 序列化为时间戳;V2:Date 序列化为 ISO 字符串
fun resolveJson(version: ApiVersion): Json {
    val module = when (version) {
        ApiVersion.V1 -> SerializersModule {
            contextual(java.util.Date::class, DateAsLongSerializer)
        }
        ApiVersion.V2 -> SerializersModule {
            contextual(java.util.Date::class, DateAsIsoStringSerializer)
        }
    }
    return Json { serializersModule = module }
}

fun main() {
    val event = Event("Release", java.util.Date())

    // 客户端请求 V1 API
    println(resolveJson(ApiVersion.V1).encodeToString(event))
    // 输出: {"name":"Release","timestamp":1672531200000}

    // 客户端请求 V2 API
    println(resolveJson(ApiVersion.V2).encodeToString(event))
    // 输出: {"name":"Release","timestamp":"2023-01-01T00:00:00Z"}
}

4. 为第三方 Kotlin 类生成外部序列化器 (实验性)

对于结构简单(具有属性、主构造函数)但无法添加 @Serializable 注解的第三方 Kotlin 类,可以使用实验性的外部序列化功能。

复制代码
// 第三方类(无法修改源码)
class Project(val name: String, val stars: Int) {
    val description: String // 只有 getter
        get() = "$name has $stars stars"
    private var internalId: Long = 0L // 私有字段
}

// 生成外部序列化器
@OptIn(ExperimentalSerializationApi::class)
@Serializer(forClass = Project::class)
object ProjectSerializer // 空对象即可,框架负责生成逻辑

fun main() {
    val project = Project("Kotlin", 50000)
    val json = Json.encodeToString(ProjectSerializer, project)
    println(json) // 输出: {"name":"Kotlin","stars":50000}
    // 注意:description 和 internalId 不会被序列化
}

重要差异 :外部序列化与 @Serializable 的自动生成存在本质区别:

  1. 序列化范围有限

    • 仅序列化主构造函数的 val / var 参数

    • 仅序列化具有 getter 和 setter 的属性

    • 不会序列化仅有 getter 的属性或私有字段

  2. 字段顺序不保证

    • 字段顺序依赖编译器插件实现,建议不依赖顺序

    • 在 JSON 等 key-value 格式中通常无影响

    • 但在 Protobuf 等位置敏感格式中,可能导致 schema 错位或解码失败

  3. 反序列化会触发构造逻辑

    • 外部序列化通过调用主构造函数完成反序列化

    • 这意味着 init 块、属性校验逻辑会被执行

    • 若构造函数包含副作用(如日志、统计、IO),可能在反序列化阶段被意外触发

这一限制是由编译器插件的实现机制决定的,在涉及 Protobuf、跨语言通信或纯数据模型时应格外注意。


05

总结与最佳实践

1. 核心要点回顾

序列化器定义结构:由编译期生成,控制对象如何分解为基本元素。

格式控制编码 :由 Encoder / Decoder 负责具体格式的读写,运行期执行。

描述符必须准确descriptor 属性必须与 serialize / deserialize 方法严格对应,否则在严格格式下会引发崩溃。

优先使用代理类:对于复合序列化,代理类模式是最简洁、推荐的方式,兼顾安全性与开发效率。

上下文序列化提供动态性:允许运行时根据上下文选择序列化策略,是构建灵活库和框架的关键。

2. 工程化视角

在实际项目中,序列化层往往是性能瓶颈和 Bug 高发区。不合理的序列化策略可能导致频繁的 GC(例如在自定义序列化器中反复创建中间对象,或使用动态扩容的集合而不用预分配容量的数组),而描述符与实现的不一致则会在跨平台或版本升级时引发灾难性故障。因此,掌握自定义序列化器不仅是应对复杂格式的需要,更是构建高稳定性、高可维护性系统的必备技能。特别是在 KMP(Kotlin Multiplatform)项目中,统一的序列化策略是保障 iOS、Android 和后端数据一致性的关键防线。

3. 选择指南

|----------------------------|------------------------|
| 场景 | 推荐方案 |
| 控制简单对象输出为基本类型(字符串、数字) | 基本类型序列化器 |
| 将对象转换为另一种内置结构(数组、列表) | 委托序列化器 |
| 实现复杂对象的自定义 JSON/YAML 结构 | 代理类序列化器 (首选) |
| 需要极致控制或动态结构 | 手动复合序列化器 |
| 序列化 java.util.Date 等第三方类 | 自定义序列化器 + 属性/文件/类型别名绑定 |
| SDK/框架需要支持可配置的序列化格式 | 上下文序列化 |
| 序列化结构简单的第三方 Kotlin 类 | 外部序列化器 (实验性) |

4. 性能与兼容性提示

向后兼容:更改序列化格式时,必须严格考虑现有数据的反序列化兼容性。

字段策略:避免使用位置参数(Positional Arguments),始终使用字段名(Named Arguments)进行映射。例如 JSON 中应依赖 Key 而非数组下标,防止因字段增删或重排导致历史数据解析失败。

版本控制 :建议在载荷中加入 version 字段,根据版本号动态选择反序列化逻辑,这是支持多版本 API 共存的经典方案。

防御性解码 :自定义序列化器(特别是手写 decodeElementIndex 逻辑时)应允许未知字段(Skip Unknown Keys),避免因后端字段扩展导致旧版客户端崩溃。

通过掌握从基本获取到高级上下文序列化的完整知识体系,你将能够应对 Kotlin 项目中的数据序列化挑战,构建出健壮、灵活且高性能的数据处理层。

相关推荐
甲维斯1 小时前
Jev到底是个什么东西?举个形象的例子!
人工智能
jingli91 小时前
多平台多店铺客服怎么统一接待?千牛/拼多多/抖音/京东/闲鱼/微信/快手切窗口太累,一个后台集中接管的七步落地方案(2026 实操版)
人工智能·自动化·用户运营
workflower1 小时前
SSH(Struts+Spring+Hibernate)
人工智能·机器学习·机器人·云计算·无人机
武子康1 小时前
自己做一个 Mini Reviewer:让 AI 审到本次准备提交的代码
人工智能·llm·agent
木子算法1 小时前
不是重心也不是中点:费马—韦伯点、它的最优性条件与迭代求解
人工智能·算法·目标跟踪
正经教主1 小时前
【FDE系列】阶段2:Day 43:Docker Compose — 多容器一键编排
人工智能·docker·fde
YOLO数据集集合1 小时前
天空红外小目标检测数据集 | 红外小目标 天空目标检测 无人机检测 鸟类识别 低空安防9099期
人工智能·目标检测·计算机视觉·目标跟踪·无人机·无人机视角·直升机检测
徐健峰1 小时前
Windows 安装 Codex CLI 教程:npm 安装、首次登录与常见报错排查【无图精华版】
人工智能·gpt
微三云 - 廖会灵 (私域系统开发)1 小时前
企业大模型应用工程化思考:跳出 Demo 陷阱,构建「业务‑资产‑Agent」三层落地架构
人工智能·矩阵·重构