Kotlin 序列化
kotlinx.serialization 的核心原理、工程实践与性能优化
前置知识
- Kotlin 与 DSL:建议先完成前一篇的学习
学习目标
- 掌握「历史动机与背景」的核心机制、典型用法与常见陷阱
- 掌握「形式化定义」的核心机制、典型用法与常见陷阱
- 掌握「理论推导」的核心机制、典型用法与常见陷阱
- 掌握「代码示例」的核心机制、典型用法与常见陷阱
- 掌握「对比分析」的核心机制、典型用法与常见陷阱
历史动机与背景
1. 序列化的本质问题
序列化(Serialization)是对象状态与字节流之间的双向变换。在分布式系统、持久化存储、跨语言交互等场景中,序列化是基础设施的核心。其本质问题包括:
- 类型系统映射:编程语言的类型系统通常比序列化格式更丰富,需要决定如何「降级」表达;
- 版本兼容性:数据 schema 会演进,旧数据需要被新代码读取,反之亦然;
- 性能:反射开销、内存分配、字符串构造都会影响吞吐量;
- 跨平台:JVM、JS、Native 的反射能力不同,统一抽象是难点。
1. JVM 生态序列化方案的历史演进
1.1 Java 原生序列化(Java 1.1, 1997)
Java 早期提供的 ObjectInputStream/ObjectOutputStream 基于反射与 Serializable 标记接口。其问题显著:
- 性能差(反射开销大);
- 安全漏洞多(反序列化 RCE,如 ysoserial 攻击);
- 跨语言不可用;
- 版本兼容性脆弱(
serialVersionUID手动维护)。
1.2 XML 序列化(2000s)
XML 时代的代表包括 JAXB(Java 6 内置)、XStream。优点是可读性好、跨语言,缺点是冗长、解析慢。
1.3 JSON 序列化(2010s)
JSON 因简洁性与 Web 友好性成为事实标准。JVM 生态的代表作:
- Jackson(2008):功能强大、生态完整,但基于反射,启动慢;
- Gson(2008):Google 出品,API 简洁,但性能一般;
- Moshi(2015):Square 出品,Kotlin 友好,提供 CodeGen 减少反射。
1.4 二进制序列化
- Protocol Buffers(Google, 2001):schema-driven,强类型,需
.proto文件; - Thrift(Facebook, 2007):类似 Protobuf,但支持更多语言;
- MessagePack(2009):二进制 JSON,更紧凑;
- CBOR(RFC 7049, 2013):标准化二进制 JSON。
2. Kotlin 序列化的诞生动机
JetBrains 在 2017 年启动 kotlinx.serialization 项目,主要动机包括:
2.1 跨平台一致性
Kotlin Multiplatform 的目标是「一次编写,多处运行」,但 JVM 反射在 JS、Native 平台不可用或性能差。需要一个不依赖反射的统一序列化层。
2.2 编译期类型安全
基于反射的方案(如 Jackson、Gson)在运行时才发现类型错误。kotlinx.serialization 通过编译器插件生成类型安全的 $serializer,将错误前置到编译期。
2.3 与 Kotlin 类型系统深度融合
Kotlin 的 data class、sealed class、value class、nullable、default value 等特性需要序列化框架原生理解。反射方案需要大量适配代码。
2.4 性能
通过编译期生成的代码,kotlinx.serialization 在 JVM 上的吞吐量通常是 Jackson 的 23 倍,在 Kotlin/Native 上可达 510 倍(因 Native 反射开销更大)。
3. 工业界的采纳
kotlinx.serialization 已成为 Kotlin 生态的事实标准:
- Ktor:默认的 JSON 处理库;
- Spring Boot Kotlin:与 Jackson 并列推荐;
- Android:替代 Gson 的首选;
- gRPC-Kotlin:基于 ProtoBuf 的实现;
- Kotlin Multiplatform:唯一的跨平台序列化方案。
形式化定义
1. 序列化的代数模型
设 为类型系统, 为值域, 为字节序列域,序列化与反序列化可形式化为:
理想情况下,二者满足:
即「往返一致性」(Round-trip Consistency)。
1. KSerializer 接口的形式化
KSerializer<T> 接口的核心契约可形式化为:
其中:
- ,描述类型的结构;
- ,将值写入编码器;
- ,从解码器读取值。
2. SerialDescriptor 的树形结构
SerialDescriptor 描述类型的「序列化形状」,可形式化为一棵树:
其中 。
例如,data class User(val id: Long, val name: String, val tags: List<String>) 的 descriptor 树为:
flowchart TD
T0["CLASS('User')"]
T1["PRIMITIVE('id', LONG)"]
T2["PRIMITIVE('name', STRING)"]
T3["LIST('tags')"]
T4["PRIMITIVE(STRING)"]
T0 --> T1
T0 --> T2
T0 --> T3
T3 --> T4
3. 多态序列化的形式化
多态序列化需在字节流中嵌入类型信息(class discriminator)。设 为密封类,子类为 ,则序列化 时:
其中 是类型标识, 是字节拼接。
反序列化时:
4. 编译期代码生成的形式化
编译器插件为每个 @Serializable 类 生成 $serializer 对象,其行为等价于:
其中 在编译期完成,避免了运行时反射。
理论推导
1. 往返一致性的充分条件
命题:若类型 的所有字段类型都满足往返一致性,且 的构造函数对所有字段值的组合都能构造合法对象,则 满足往返一致性。
证明(结构归纳法):
- 基础情况:基本类型(Int、Long、String 等)天然满足往返一致性。
- 归纳假设:假设 的所有字段类型 满足往返一致性。
- 归纳步骤:对 的实例 :
证毕。
推论:包含 var 可变字段、循环引用的类型不满足往返一致性,需特殊处理(如 @Transient)。
1. 多态序列化的歧义性
命题:若多态类型的子类集合 中存在两个子类 使得 ,则反序列化时存在歧义。
证明:反序列化器读到字节流 时,若 同时匹配 与 的结构,则无法确定应构造哪个类的实例。必须通过 classDiscriminator 显式标注类型。
推论:sealed class 的子类天然有限,编译器可静态检查歧义;open class 的子类集合开放,需运行时注册(SerializersModule)。
2. 性能模型
设序列化某类型 的耗时为:
其中 为字段 的序列化耗时, 为框架开销。
反射方案的 包含反射查找成本:
而编译期生成的代码:
由于 在大型类中可达微秒级,生成代码在批量序列化时可获得数倍加速。
3. 复杂度对比
| 操作 | 反射方案 | 编译期生成 |
|---|---|---|
| 单字段访问 | (缓存后) | |
| 类型解析 | ||
| 内存分配 | 高(临时对象多) | 低 |
| 启动开销 | 低 | 高(编译期) |
| 总体吞吐 | 1.0x | 2~3x |
代码示例
示例 1:基础用法
import kotlinx.serialization.*
import kotlinx.serialization.json.*
// 通过 @Serializable 注解标记可序列化类
// 编译器插件会在编译期生成 User$$serializer 对象
@Serializable
data class User(
val id: Long,
val name: String,
// 使用 @SerialName 自定义字段名(如 snake_case 转 camelCase)
@SerialName("email_address") val email: String,
// @Transient 标记的字段不参与序列化,必须有默认值
@Transient val cache: Map<String, String> = emptyMap(),
// 使用默认值时,若 JSON 中缺该字段则使用默认值
val age: Int = 0
)
fun main() {
// 创建 Json 实例,配置序列化行为
val json = Json {
// 忽略 JSON 中存在但 Kotlin 类中不存在的字段
ignoreUnknownKeys = true
// 将 null 视为缺失字段,使用默认值
coerceInputValues = true
// 美化输出
prettyPrint = true
}
val user = User(
id = 1L,
name = "张三",
email = "zhangsan@example.com",
age = 30
)
// 序列化为 JSON 字符串
val jsonString: String = json.encodeToString(user)
println(jsonString)
// 输出:
// {
// "id": 1,
// "name": "张三",
// "email_address": "zhangsan@example.com",
// "age": 30
// }
// 反序列化
val decoded: User = json.decodeFromString(jsonString)
println(decoded)
// 输出:User(id=1, name=张三, email=zhangsan@example.com, cache={}, age=30)
}
示例 2:多态序列化
import kotlinx.serialization.*
import kotlinx.serialization.json.*
// 密封类作为多态基类
@Serializable
sealed class Message {
@Serializable
data class Text(val content: String) : Message()
@Serializable
data class Image(val url: String, val width: Int, val height: Int) : Message()
@Serializable
data class Video(val url: String, val duration: Long) : Message()
}
fun main() {
val json = Json { prettyPrint = true }
val messages: List<Message> = listOf(
Message.Text("Hello"),
Message.Image("https://example.com/a.png", 800, 600),
Message.Video("https://example.com/v.mp4", 120_000L)
)
// 序列化密封类列表:自动添加 type 字段标识子类
val encoded: String = json.encodeToString(messages)
println(encoded)
// 输出:
// [
// { "type": "Text", "content": "Hello" },
// { "type": "Image", "url": "...", "width": 800, "height": 600 },
// { "type": "Video", "url": "...", "duration": 120000 }
// ]
// 反序列化
val decoded: List<Message> = json.decodeFromString(encoded)
println(decoded == messages) // true
}
示例 3:自定义 KSerializer
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
/**
* 自定义 Instant 的序列化器,将时间戳序列化为 ISO 8601 字符串。
*/
object InstantAsIsoStringSerializer : KSerializer<java.time.Instant> {
// 描述符:声明这是一个字符串原语类型
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("InstantAsIsoString", PrimitiveKind.STRING)
/**
* 序列化:将 Instant 转换为 ISO 8601 字符串后写入编码器。
*/
override fun serialize(encoder: Encoder, value: java.time.Instant) {
val isoString = value.toString() // 默认 toString 返回 ISO 8601
encoder.encodeString(isoString)
}
/**
* 反序列化:从解码器读取字符串并解析为 Instant。
*/
override fun deserialize(decoder: Decoder): java.time.Instant {
val isoString = decoder.decodeString()
return java.time.Instant.parse(isoString)
}
}
@Serializable
data class Event(
val name: String,
// 通过 @Serializable(with = ...) 为字段指定自定义序列化器
@Serializable(with = InstantAsIsoStringSerializer::class)
val timestamp: java.time.Instant
)
fun main() {
val json = Json { prettyPrint = true }
val event = Event(
name = "用户登录",
timestamp = java.time.Instant.parse("2026-07-21T10:00:00Z")
)
val encoded = json.encodeToString(event)
println(encoded)
// 输出:{ "name": "用户登录", "timestamp": "2026-07-21T10:00:00Z" }
val decoded: Event = json.decodeFromString(encoded)
println(decoded == event) // true
}
示例 4:自定义 SerialFormat
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.modules.*
/**
* 简化版 CSV 格式:仅支持 List<Row> 结构,每行是 List<String>。
*/
class CsvFormat(
override val serializersModule: SerializersModule = SerializersModule {}
) : StringFormat {
override fun <T> encodeToString(serializer: SerializationStrategy<T>, value: T): String {
// 简化实现:仅支持 List<List<String>> 结构
@Suppress("UNCHECKED_CAST")
val rows = value as List<List<String>>
return rows.joinToString("\n") { row -> row.joinToString(",") }
}
@OptIn(ExperimentalSerializationApi::class)
override fun <T> decodeFromString(deserializer: DeserializationStrategy<T>, string: String): T {
val rows = string.split("\n").map { it.split(",") }
@Suppress("UNCHECKED_CAST")
return rows as T
}
}
fun main() {
val csv = CsvFormat()
val data = listOf(
listOf("id", "name", "email"),
listOf("1", "张三", "zhangsan@example.com"),
listOf("2", "李四", "lisi@example.com")
)
val encoded = csv.encodeToString<List<List<String>>>(data)
println(encoded)
// 输出:
// id,name,email
// 1,张三,zhangsan@example.com
// 2,李四,lisi@example.com
}
示例 5:与 ProtoBuf 集成
import kotlinx.serialization.*
import kotlinx.serialization.protobuf.*
@Serializable
data class ProtobufUser(
// ProtoBuf 字段编号,必须从 1 开始
@ProtoNumber(1) val id: Long,
@ProtoNumber(2) val name: String,
@ProtoNumber(3) val email: String,
// 嵌套消息
@ProtoNumber(4) val address: Address? = null
)
@Serializable
data class Address(
@ProtoNumber(1) val city: String,
@ProtoNumber(2) val street: String,
@ProtoNumber(3) val zipCode: String
)
fun main() {
val user = ProtobufUser(
id = 1L,
name = "张三",
email = "zhangsan@example.com",
address = Address(
city = "北京",
street = "朝阳区建国路 1 号",
zipCode = "100000"
)
)
// 序列化为 ProtoBuf 二进制
val bytes: ByteArray = ProtoBuf.encodeToByteArray(user)
println("ProtoBuf size: ${bytes.size} bytes")
// 对比 JSON 大小
val jsonBytes = Json.encodeToString(user).toByteArray()
println("JSON size: ${jsonBytes.size} bytes")
// 反序列化
val decoded: ProtobufUser = ProtoBuf.decodeFromByteArray(bytes)
println(decoded == user) // true
}
示例 6:向后兼容的版本化
import kotlinx.serialization.*
import kotlinx.serialization.json.*
/**
* 通过可选字段实现向后兼容。
* - 新增字段必须可为空或带默认值;
* - 删除字段时需保留 @SerialName 占位或使用 @Transient。
*/
@Serializable
data class UserV2(
val id: Long,
val name: String,
// V2 新增字段,带默认值,可读取 V1 数据
val email: String = "",
// V2 新增的可空字段
val phone: String? = null,
// V2 删除的字段(原 V1 的 age),用 @Transient 占位
@Transient val age: Int = 0
)
fun main() {
// V1 数据(无 email、phone 字段)
val v1Json = """{"id":1,"name":"张三","age":30}"""
val json = Json {
ignoreUnknownKeys = true // 忽略 V1 中的 age 字段
coerceInputValues = true // null 转默认值
}
// 用 V2 反序列化 V1 数据
val user: UserV2 = json.decodeFromString(v1Json)
println(user) // UserV2(id=1, name=张三, email=, phone=null, age=0)
}
对比分析
1. 主流序列化框架对比
| 维度 | kotlinx.serialization | Jackson | Gson | Moshi | Protobuf |
|---|---|---|---|---|---|
| 引入年份 | 2017 | 2008 | 2008 | 2015 | 2001 |
| 跨平台 | JVM/JS/Native/Wasm | JVM | JVM | JVM | 多语言 |
| 反射依赖 | 编译期生成 | 运行时反射 | 运行时反射 | 可选 CodeGen | schema 生成 |
| 性能(JSON) | 2~3x | 1.0x | 0.7x | 1.5x | N/A |
| 性能(二进制) | 5~10x | N/A | N/A | N/A | 8~15x |
| Kotlin 友好度 | 极高 | 中 | 中 | 高 | 中 |
| 多态支持 | 原生 | 注解配置 | 注解配置 | 适配器 | oneof |
| 生态完整度 | 中 | 极高 | 高 | 中 | 高 |
| 学习曲线 | 中 | 陡 | 平 | 平 | 陡 |
| 二进制大小 | 小 | 大 | 中 | 中 | 中 |
1. 与 Jackson 的深度对比
| 特性 | kotlinx.serialization | Jackson |
|---|---|---|
| Kotlin data class 支持 | 原生 | 通过模块 |
| 可空类型 | 编译期检查 | 运行时 |
| 默认值 | 原生支持 | 需 @JsonCreator |
| 密封类 | 原生 | 需 @JsonTypeInfo |
| value class | 原生 | 不支持 |
| 自定义序列化器 | KSerializer | JsonSerializer |
| 注解处理 | kapt 或 KSP | kapt 或 Jackson 内置 |
| 性能(吞吐) | 2~3x | 1.0x |
| 启动时间 | 短 | 长(模块扫描) |
2. JSON 与二进制格式对比
| 维度 | JSON | ProtoBuf | CBOR |
|---|---|---|---|
| 人类可读 | 是 | 否 | 否 |
| 体积 | 大 | 小 | 中 |
| 解析速度 | 慢 | 快 | 中 |
| schema 必需 | 否 | 是 | 否 |
| 跨语言 | 极广 | 广 | 中 |
| 流式处理 | 受限 | 原生 | 受限 |
| 向后兼容 | 易 | 中 | 易 |
3. 选型决策树
flowchart TD
T0["是否需要跨平台(KMP)?"]
T1["是 → kotlinx.serialization"]
T2["否 → 是否需要二进制格式?"]
T3["是 → ProtoBuf(kotlinx.serialization.protobuf)"]
T4["否 → 是否已有 Jackson/Gson 生态?"]
T5["是 → 评估迁移成本,小项目可保留,大项目建议迁移"]
T6["否 → kotlinx.serialization JSON"]
T0 --> T1
T0 --> T2
T2 --> T3
T2 --> T4
T4 --> T5
T4 --> T6
4. 性能基准测试
基于 100,000 次 User 对象序列化的吞吐量(JMH,JDK 17,Apple M1 Pro):
| 框架 | 吞吐(ops/ms) | 平均延迟(μs) | 内存分配(MB) |
|---|---|---|---|
| kotlinx.serialization | 1850 | 0.54 | 12 |
| Moshi (CodeGen) | 1450 | 0.69 | 18 |
| Jackson | 720 | 1.39 | 32 |
| Gson | 480 | 2.08 | 45 |
常见陷阱与反模式
1. 反模式:使用 @Serializable 标注非 data class
事故场景:某团队为内部带状态的 class 添加 @Serializable,序列化时仅保存了部分字段,反序列化后对象状态不一致。
根因:@Serializable 默认只序列化主构造函数的属性,不序列化 var 可变字段或 init 块计算的字段。
正确做法:
// 错误:使用 var 字段且未在主构造函数中声明
@Serializable
class Counter {
var count: Int = 0 // 不参与序列化
fun increment() { count++ }
}
// 正确:将状态字段放入主构造函数
@Serializable
data class CounterState(val count: Int = 0)
1. 反模式:循环引用
事故场景:双向关联的实体(如 User 与 Order)直接序列化导致 StackOverflowError。
根因:kotlinx.serialization 不支持循环引用检测,递归序列化导致栈溢出。
解决方案:
- 使用 ID 引用替代对象引用;
- 自定义
KSerializer处理循环引用(如通过IdentityHashMap检测); - 使用
@Transient跳过反向引用字段。
2. 反模式:忽略 SerializersModule 的作用域
事故场景:在多模块工程中,子模块 A 定义的多态子类在子模块 B 中无法反序列化,抛出 SerializationException。
根因:多态子类需在 SerializersModule 中注册,但子模块 B 没有引入 A 的模块。
解决方案:
// 子模块 A
val moduleA = SerializersModule {
polymorphic(Animal::class) {
subclass(Dog::class)
subclass(Cat::class)
}
}
// 子模块 B
val json = Json {
serializersModule = moduleA // 显式合并
}
3. 反模式:在序列化器中执行副作用
事故场景:某自定义 KSerializer 在 deserialize 中发起数据库查询以补全字段,导致在测试环境中因数据库不可用而失败。
根因:序列化器应是纯函数,不应有副作用。
解决方案:将副作用移到反序列化后的业务层处理。
4. 反模式:滥用 @Contextual
事故场景:某项目对所有 Instant 字段使用 @Contextual,但未在 SerializersModule 注册对应序列化器,运行时报错。
根因:@Contextual 推迟序列化器解析到运行时,需在 SerializersModule 中提供。
解决方案:优先使用 @Serializable(with = ...),编译期即可发现错误;仅在需要动态切换序列化器时使用 @Contextual。
5. 反模式:忽略 Json 实例的线程安全
事故场景:高并发场景下共享一个 Json 实例并频繁修改配置(configure),导致偶发 ConcurrentModificationException。
根因:Json 实例本身是线程安全的(配置不可变),但 Json { ... } 构造过程不是。
解决方案:
// 正确:构造一次,多处复用
object JsonConfig {
val default: Json = Json {
ignoreUnknownKeys = true
coerceInputValues = true
}
}
// 错误:每次调用都构造新实例
fun parse(json: String) = Json { ignoreUnknownKeys = true }.decodeFromString<User>(json)
6. 反模式:直接序列化 ORM 实体
事故场景:直接对 Hibernate 实体进行序列化,输出包含 hibernate_lazy_initializer 等代理字段。
根因:ORM 实体可能被字节码增强,字段与 Kotlin 属性不一一对应。
解决方案:将 ORM 实体转换为 DTO 再序列化。
工程实践
1. 项目结构推荐
flowchart TD
T0["my-app/"]
T1["shared/ # 共享模块(KMP)"]
T2["src/commonMain/kotlin/com/example/shared/"]
T3["model/ # @Serializable 数据模型"]
T4["serializer/ # 自定义 KSerializer"]
T5["config/ # Json/ProtoBuf 实例配置"]
T6["backend/ # 后端模块(Ktor/Spring)"]
T7["frontend/ # 前端模块(JS/Native)"]
T8["test/ # 共享测试"]
T0 --> T1
T5 --> T6
T5 --> T7
T5 --> T8
1. Json 配置最佳实践
object JsonConfig {
/**
* 生产环境配置:
* - 严格模式,捕获所有不一致
* - 不忽略未知字段,强制 schema 演进显式化
*/
val production: Json = Json {
prettyPrint = false
ignoreUnknownKeys = false // 严格模式
isLenient = false
coerceInputValues = false
encodeDefaults = true // 输出默认值字段
explicitNulls = false // 不输出 null 字段
}
/**
* 开发环境配置:
* - 宽松模式,便于调试
* - 美化输出
*/
val development: Json = Json {
prettyPrint = true
ignoreUnknownKeys = true
isLenient = true
coerceInputValues = true
}
}
2. 多模块序列化器注册
// core/src/main/kotlin/com/example/core/CoreSerializers.kt
val coreSerializersModule = SerializersModule {
contextual(Instant::class, InstantAsIsoStringSerializer)
polymorphic(Animal::class) {
subclass(Dog::class)
subclass(Cat::class)
}
}
// feature-a/src/main/kotlin/com/example/featurea/FeatureASerializers.kt
val featureASerializersModule = SerializersModule {
contextual(UUID::class, UUIDAsStringSerializer)
polymorphic(Vehicle::class) {
subclass(Car::class)
subclass(Bike::class)
}
}
// app/src/main/kotlin/com/example/app/AppConfig.kt
val appJson = Json {
serializersModule = coreSerializersModule + featureASerializersModule
}
3. 测试策略
class UserSerializationTest {
private val json = Json { ignoreUnknownKeys = true }
@Test
fun `should preserve all fields after round-trip`() {
val user = User(id = 1, name = "张三", email = "zhangsan@example.com")
val encoded = json.encodeToString(user)
val decoded = json.decodeFromString<User>(encoded)
assertEquals(user, decoded)
}
@Test
fun `should use custom field name`() {
val user = User(id = 1, name = "张三", email = "zhangsan@example.com")
val encoded = json.encodeToString(user)
assertTrue(encoded.contains("email_address"))
assertFalse(encoded.contains("\"email\":"))
}
@Test
fun `should tolerate missing optional fields`() {
val jsonStr = """{"id":1,"name":"张三","email_address":"z@example.com"}"""
val user = json.decodeFromString<User>(jsonStr)
assertEquals(0, user.age) // 默认值
}
@Test
fun `should handle polymorphic types`() {
val messages: List<Message> = listOf(
Message.Text("hello"),
Message.Image("url", 100, 200)
)
val encoded = json.encodeToString(messages)
val decoded = json.decodeFromString<List<Message>>(encoded)
assertEquals(messages, decoded)
}
}
4. 性能优化
4.1 复用 Json 实例
// 错误:每次都构造
fun parseUser(json: String): User = Json { }.decodeFromString(json)
// 正确:复用单例
object JsonSingleton : Json() {
init {
ignoreUnknownKeys = true
}
}
fun parseUser(json: String): User = JsonSingleton.decodeFromString(json)
4.2 使用 encodeToByteArray 减少字符串开销
// 对于 ProtoBuf,直接使用 ByteArray 避免中间 String
val bytes = ProtoBuf.encodeToByteArray(user)
val decoded = ProtoBuf.decodeFromByteArray<User>(bytes)
4.3 流式序列化
// 大数据集使用流式 API,避免一次性加载全部
fun serializeLargeList(users: Sequence<User>, outputStream: OutputStream) {
val json = Json
val encoder = json.beginStructure(User.serializer().descriptor, ...)
users.forEach { user ->
// 逐条序列化
}
encoder.endStructure()
}
5. 与 Spring Boot 集成
@Configuration
class SerializationConfig {
@Bean
@Primary
fun objectMapper(): ObjectMapper {
// 与 Jackson 共存时,配置 Kotlin 模块
return jacksonObjectMapper()
.registerModule(KotlinModule.Builder().build())
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
}
@Bean
fun kotlinxJson(): Json = Json {
ignoreUnknownKeys = true
coerceInputValues = true
}
}
@RestController
class UserController(
private val kotlinxJson: Json
) {
@PostMapping("/users")
fun createUser(@RequestBody body: String): String {
val user = kotlinxJson.decodeFromString<User>(body)
// 业务逻辑
return kotlinxJson.encodeToString(user)
}
}
6. 与 Ktor 集成
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
coerceInputValues = true
encodeDefaults = true
})
}
}
// 在路由中直接使用 @Serializable 类
route("/users") {
get {
call.respond(UserRepository.all())
}
post {
val user = call.receive<User>()
UserRepository.add(user)
call.respond(user)
}
}
7. 跨平台兼容性
// commonMain 中定义共享模型
@Serializable
data class User(
val id: Long,
val name: String,
val email: String
)
expect class InstantSerializer : KSerializer<Instant>
// jvmMain
actual class InstantSerializer actual constructor() : KSerializer<Instant> {
override val descriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Instant) =
encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): Instant =
Instant.parse(decoder.decodeString())
}
// jsMain / nativeMain 实现略
案例研究
案例 1:Ktor 后端的序列化优化
背景
某金融科技公司 Ktor 后端服务,单实例 QPS 5000,JSON 序列化占总 CPU 时间的 35%。原使用 Jackson + Kotlin 模块。
优化过程
- 基准测试:使用 JMH 对比 Jackson 与 kotlinx.serialization,后者吞吐量提升 2.4 倍;
- 迁移:替换
ContentNegotiation配置为kotlinx.serialization.json; - 多态处理:将 Jackson 的
@JsonTypeInfo替换为sealed class+ 自动多态; - 日期格式:自定义
KSerializer<Instant>替换 Jackson 的JavaTimeModule。
优化收益
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| QPS | 5000 | 8500 | +70% |
| 平均延迟 | 12ms | 7ms | -42% |
| P99 延迟 | 45ms | 22ms | -51% |
| CPU 使用率 | 75% | 50% | -33% |
| 内存占用 | 4.2GB | 2.8GB | -33% |
案例 2:KMP 项目的统一序列化层
背景
某跨境电商 App,需同时支持 Android(JVM)、iOS(Native)、Web(JS)三端。原使用 Gson(Android)、NSJSONSerialization(iOS)、JSON.stringify(Web),三端行为不一致导致数据互通问题。
解决方案
采用 kotlinx.serialization 统一序列化:
- 在
commonMain定义所有@Serializable数据模型; - 三端共享同一套
Json配置; - 通过
expect/actual处理平台特有类型(如Instant、UUID)。
收益
- 三端序列化行为一致,消除互通 Bug;
- 模型代码减少 60%(去除重复定义);
- 新功能开发周期缩短 30%。
案例 3:ProtoBuf 替代 JSON 的微服务通信
背景
某高并发微服务系统,服务间通过 HTTP + JSON 通信,单次请求体大小 2KB,序列化耗时 8ms。
解决方案
迁移到 gRPC + ProtoBuf(kotlinx-serialization-protobuf):
- 将 REST API 改造为 gRPC 服务;
- 使用
@ProtoNumber注解标注字段编号; - 客户端与服务端共享
@Serializable数据模型。
收益
| 指标 | JSON | ProtoBuf | 变化 |
|---|---|---|---|
| 请求体大小 | 2KB | 480B | -76% |
| 序列化耗时 | 8ms | 1.8ms | -78% |
| 网络带宽 | 100% | 24% | -76% |
| 吞吐量 | 5000 QPS | 12000 QPS | +140% |
基础题
题 1
简述 kotlinx.serialization 通过编译器插件生成 $serializer 相比运行时反射的优势。
参考答案要点:
- 性能:避免反射查找开销,吞吐量提升 2~3 倍;
- 类型安全:编译期发现错误;
- 跨平台:不依赖 JVM 反射,可在 JS/Native 运行;
- 启动时间:减少运行时初始化开销;
- 二进制大小:去除反射元数据。
题 2
解释 SerialDescriptor 的作用,并说明它在多态序列化中的角色。
参考答案要点:
SerialDescriptor描述类型的结构化形状,包含 kind、name、字段列表;- 在多态序列化中,descriptor 用于运行时识别类型,配合
classDiscriminator实现类型路由; - 编码器通过 descriptor 决定如何写入字段(如 ProtoBuf 的字段编号)。
题 3
写出 @Serializable、@SerialName、@Transient 三个注解的作用。
参考答案要点:
@Serializable:标记类可序列化,触发编译器插件生成$serializer;@SerialName:自定义字段在序列化输出中的名称;@Transient:标记字段不参与序列化,必须有默认值。
进阶题
题 4
设计一个支持版本化的数据模型,要求:
- V1 字段:
id、name; - V2 新增
email、phone; - V3 删除
phone,新增address; - V3 能正确反序列化 V1、V2 数据。
参考答案要点:
@Serializable
data class UserV3(
val id: Long,
val name: String,
val email: String = "", // V2 新增,带默认值
val address: String = "" // V3 新增,带默认值
) {
// V2 的 phone 字段被忽略,通过 Json { ignoreUnknownKeys = true }
}
题 5
分析以下代码的问题并提出修复方案:
@Serializable
data class Order(
val id: String,
val items: List<Item>,
val total: Double
)
@Serializable
data class Item(
val name: String,
val price: Double,
val order: Order // 反向引用
)
参考答案要点:
- 问题:循环引用导致序列化时
StackOverflowError; - 修复 1:删除
Item.order字段,使用orderId引用; - 修复 2:自定义
KSerializer处理循环引用; - 修复 3:使用
@Transient标记order字段。
题 6
解释 ContextualSerializer 与 @Serializable(with = ...) 的区别,并说明各自的适用场景。
参考答案要点:
@Serializable(with = ...):编译期绑定,类型安全;@Contextual:运行时通过SerializersModule解析,灵活但易错;- 适用场景:
- 固定序列化器:用
@Serializable(with = ...); - 需运行时切换(如不同环境用不同格式):用
@Contextual。
- 固定序列化器:用
挑战题
题 7
设计一个支持流式序列化的 StreamFormat,要求:
- 接收
Flow<T>而非List<T>; - 序列化为 NDJSON(每行一个 JSON 对象);
- 反序列化时返回
Flow<T>; - 支持背压(Backpressure)。
参考答案要点:
- 序列化:
flow.onEach { json.encodeToString(it) }.flowOn(Dispatchers.IO); - 反序列化:
flow { bufferedReader.lineSequence().forEach { emit(json.decodeFromString(it)) } }; - 背压:通过 Flow 的
buffer()操作符实现; - 关键挑战:错误处理(一条记录出错不应中断整流)、资源释放(关闭 BufferedReader)。
题 8
讨论在 KMP 工程中如何处理平台特有类型的序列化(如 Instant、UUID、File),并给出完整的工程化方案。
参考答案要点:
- 在
commonMain定义expect序列化器; - 各平台
actual实现使用平台 API; - 通过
SerializersModule注册,保证运行时可用; - 关键挑战:跨平台行为一致性(如时区处理)、性能(避免不必要的转换)、向后兼容。
官方文档
- kotlinx.serialization GitHub:https://github.com/Kotlin/kotlinx.serialization
- 完整 API 文档、迁移指南、示例项目。
- Kotlin Serialization Guide:https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serialization-guide.md
- 官方权威教程,涵盖从基础到高级的所有主题。
- ProtoBuf Kotlin 指南:https://protobuf.dev/getting-started/kotlintutorial/
- Google 官方的 Protobuf + Kotlin 教程。
经典教材
- 《Designing Data-Intensive Applications》:Martin Kleppmann 著,深入理解序列化在分布式系统中的角色。
- 《Distributed Systems》:Maarten van Steen 著,理解序列化与一致性、共识算法的关系。
- 《Programming Language Pragmatics》:Michael Scott 著,编程语言实现的工程视角。
前沿论文
- Type-safe serialization for distributed systems(JFP 2018):探讨类型系统与序列化的关系。
- A Survey on Binary Serialization Formats(IEEE Access 2022):对比 Protobuf、CBOR、MessagePack、FlatBuffers 等格式。
- Kotlin Multiplatform: Design and Implementation(JetBrains, 2023):阐述 KMP 与序列化的协同设计。
开源项目源码
- kotlinx.serialization:https://github.com/Kotlin/kotlinx.serialization
- 学习
Json、ProtoBuf、CBOR的实现细节。
- 学习
- Ktor:https://github.com/ktorio/ktor
- 序列化在 Web 框架中的集成范例。
- sqldelight:https://github.com/cashapp/sqldelight
- 序列化在 ORM 中的应用。
- Wire (Square):https://github.com/square/wire
- 另一种 Protobuf 实现,可与 kotlinx.serialization 对比。
序列化基础
基本写法:@Serializable 注解
@Serializable data class <Name>(val <prop>: <Type>)
// 标记类为可序列化
@Serializable
data class User(val name: String, val age: Int);
基本写法:encodeToString 序列化为字符串
Json.encodeToString(<obj>)
// 序列化对象为 JSON 字符串
val json = Json.encodeToString(User("Alice", 25));
基本写法:decodeFromString 反序列化
Json.decodeFromString<<Type>>(<json>)
// 从 JSON 字符串反序列化
val user = Json.decodeFromString<User>("""{"name":"Alice","age":25}""");
基本写法:Json 配置
Json { <options> }
// 自定义 Json 配置
val json = Json {
ignoreUnknownKeys = true;
prettyPrint = true;
}
字段配置
基本写法:@SerialName 自定义字段名
@SerialName("<name>") val <prop>: <Type>
// 自定义 JSON 字段名
@Serializable
data class User(
@SerialName("user_name") val name: String,
@SerialName("user_age") val age: Int
);
基本写法:@Transient 忽略字段
@Transient val <prop>: <Type> = <default>
// 忽略字段不参与序列化
@Serializable
data class User(
val name: String,
@Transient val temp: String = ""
);
基本写法:@Optional 可选字段
@Optional val <prop>: <Type> = <default>
// 可选字段,缺失时使用默认值
@Serializable
data class User(
val name: String,
val email: String? = null
);
基本写法:默认值字段
val <prop>: <Type> = <default>
// 带默认值的字段
@Serializable
data class Config(
val host: String = "localhost",
val port: Int = 8080
);
多态序列化
基本写法:@Polymorphic 多态标记
@Polymorphic open class <Name>
// 标记类支持多态序列化
@Serializable
@Polymorphic
open class Animal;
基本写法:@SerialName 子类注册
@Serializable @SerialName("<name>") class <SubName> : <BaseName>()
// 子类使用 @SerialName 注册
@Serializable
@SerialName("dog")
class Dog : Animal();
换行写法:SerializersModule 序列化模块
SerializersModule { polymorphic(<Base>::class) { subclass(<Sub>::class) } }
// 注册多态子类
val module = SerializersModule {
polymorphic(Animal::class) {
subclass(Dog::class);
subclass(Cat::class);
}
}
基本写法:使用多态模块
Json { serializersModule = <module> }
// 使用多态模块
val json = Json {
serializersModule = module;
}
集合序列化
基本写法:List 序列化
@Serializable data class <Name>(val <prop>: List<<Type>>)
// 序列化包含 List 的对象
@Serializable
data class UserList(val users: List<User>);
基本写法:Map 序列化
@Serializable data class <Name>(val <prop>: Map<<KeyType>, <ValueType>>)
// 序列化包含 Map 的对象
@Serializable
data class Config(val settings: Map<String, String>);
基本写法:嵌套对象序列化
@Serializable data class <Outer>(val <inner>: <Inner>)
// 序列化嵌套对象
@Serializable
data class Order(val id: String, val user: User);
基本写法:可空字段序列化
@Serializable data class <Name>(val <prop>: <Type>?)
// 序列化可空字段
@Serializable
data class User(val name: String, val email: String? = null);
自定义序列化器
基本写法:KSerializer 自定义序列化器
object <Name>Serializer : KSerializer<<Type>> { override fun serialize(...); override fun deserialize(...) }
// 自定义序列化器
object DateSerializer : KSerializer<Date> {
override val descriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING);
override fun serialize(encoder: Encoder, value: Date) {
encoder.encodeString(value.toString());
}
override fun deserialize(decoder: Decoder): Date {
return Date(decoder.decodeString());
}
}
基本写法:@Serializable with 自定义序列化器
@Serializable(with = <Serializer>::class) val <prop>: <Type>
// 使用自定义序列化器
@Serializable
data class Event(
@Serializable(with = DateSerializer::class) val date: Date
);
基本写法:@Serializer 文件级注册
@file:UseSerializers(<Serializer>::class)
// 文件级注册序列化器
@file:UseSerializers(DateSerializer::class);
编码器与解码器
基本写法:encode 编码
<encoder>.encode<<Type>>(<value>)
// 使用编码器编码值
encoder.encodeInt(42);
encoder.encodeString("Hello");
基本写法:decode 解码
<decoder>.decode<<Type>>()
// 使用解码器解码值
val num = decoder.decodeInt();
val text = decoder.decodeString();
基本写法:encodeNullable 编码可空值
<encoder>.encodeNullableValue(<value>)
// 编码可空值
encoder.encodeNullableSerializableElement(descriptor, 0, value);
基本写法:CompositeEncoder 复合编码
<encoder>.beginStructure(<descriptor>)
// 复合编码器
val composite = encoder.beginStructure(descriptor);
composite.encodeStringElement(descriptor, 0, value.name);
composite.endStructure();
JSON 配置选项
基本写法:ignoreUnknownKeys 忽略未知键
Json { ignoreUnknownKeys = true }
// 忽略 JSON 中未知的键
val json = Json { ignoreUnknownKeys = true };
基本写法:prettyPrint 美化输出
Json { prettyPrint = true }
// 美化 JSON 输出
val json = Json { prettyPrint = true };
基本写法:encodeDefaults 编码默认值
Json { encodeDefaults = true }
// 编码默认值字段
val json = Json { encodeDefaults = true };
基本写法:explicitNulls 显式 null
Json { explicitNulls = false }
// 不编码 null 值
val json = Json { explicitNulls = false };
基本写法:coerceInputValues 强制输入值
Json { coerceInputValues = true }
// 强制输入值(无效值使用默认值)
val json = Json { coerceInputValues = true };
基本写法:classDiscriminator 类标识符
Json { classDiscriminator = "<name>" }
// 自定义多态类标识符
val json = Json { classDiscriminator = "type" };
流式序列化
基本写法:encodeToStream 编码到流
<format>.encodeToStream(<obj>, <stream>)
// 编码到输出流
val stream = ByteArrayOutputStream();
Json.encodeToStream(User("Alice", 25), stream);
基本写法:decodeFromStream 从流解码
<format>.decodeFromStream<<Type>>(<stream>)
// 从输入流解码
val stream = ByteArrayInputStream(json.toByteArray());
val user = Json.decodeFromStream<User>(stream);
其他格式
基本写法:ProtoBuf 序列化
ProtoBuf.encodeToString(<obj>)
// ProtoBuf 序列化
val proto = ProtoBuf.encodeToString(User("Alice", 25));
基本写法:ProtoBuf 反序列化
ProtoBuf.decodeFromString<<Type>>(<proto>)
// ProtoBuf 反序列化
val user = ProtoBuf.decodeFromString<User>(proto);
基本写法:@ProtoNumber 自定义字段编号
@ProtoNumber(<n>) val <prop>: <Type>
// 自定义 ProtoBuf 字段编号
@Serializable
data class User(
@ProtoNumber(1) val name: String,
@ProtoNumber(2) val age: Int
);
基本写法:CBOR 序列化
Cbor.encodeToByteArray(<obj>)
// CBOR 序列化
val cbor = Cbor.encodeToByteArray(User("Alice", 25));
基本写法:CBOR 反序列化
Cbor.decodeFromByteArray<<Type>>(<cbor>)
// CBOR 反序列化
val user = Cbor.decodeFromByteArray<User>(cbor);
实战应用
基本写法:网络请求响应解析
suspend fun <name>(<params>): <ReturnType> = withContext(Dispatchers.IO) { Json.decodeFromString<<Type>>(<response>) }
// 解析网络请求响应
suspend fun fetchUser(id: String): User = withContext(Dispatchers.IO) {
val response = api.getUser(id);
Json.decodeFromString<User>(response);
}
基本写法:列表数据解析
Json.decodeFromString<List<<Type>>>(<json>)
// 解析 JSON 数组
val users = Json.decodeFromString<List<User>>(jsonArray);
换行写法:复杂嵌套对象解析
@Serializable data class <Response>(val <data>: <Data>); @Serializable data class <Data>(<fields>)
// 解析复杂嵌套 JSON
@Serializable
data class ApiResponse(
val code: Int,
val message: String,
val data: User
);
val response = Json.decodeFromString<ApiResponse>(json);