第22章:序列化——Kotlin序列化库(kotlinx.serialization)、JSON解析、自定义序列化器
序列化,说白了就是把内存里的对象变成一串字节或字符串。反过来叫反序列化。你想想看,App要跟服务器通信、要把数据存到本地文件,哪一步离得开它?
Kotlin官方搞了一套自己的序列化方案——kotlinx.serialization。我最早接触它的时候还有点抵触,毕竟Gson用了好多年。但用下来发现,它跟Kotlin的配合确实更丝滑,尤其是空安全、默认值、密封类这些特性,原生支持得非常好。
22.1 为什么选择 kotlinx.serialization?
市面上JSON解析库不少:Gson、Moshi、Jackson……每个我都踩过坑。Gson用反射,运行时慢,而且对泛型支持不够好。Moshi好一些,但需要额外写适配器。Jackson功能强大,但配置起来有点重。
kotlinx.serialization不一样。它在编译期就生成了序列化代码,没有反射,性能更好。而且它是JetBrains官方出品,跟Kotlin语言特性深度绑定。
核心优势:
- 编译期代码生成,零反射
- 原生支持Kotlin特性:空安全、默认值、数据类、密封类
- 多平台支持:JVM、JS、Native都能用
- 格式可扩展:JSON、CBOR、ProtoBuf、Properties……
22.2 快速上手:添加依赖与基本使用
先加依赖。在build.gradle.kts里:
plugins {
kotlin("plugin.serialization") version "1.9.0"
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}
然后给数据类加上@Serializable注解:
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
@Serializable
data class User(
val name: String,
val age: Int,
val email: String? = null
)
fun main() {
val user = User("张三", 28, "zhangsan@example.com")
val json = Json.encodeToString(user)
println(json) // {"name":"张三","age":28,"email":"zhangsan@example.com"}
val decoded = Json.decodeFromString<User>(json)
println(decoded) // User(name=张三, age=28, email=zhangsan@example.com)
}
就这么简单。没有反射,没有TypeToken,干净利落。
22.3 JSON配置:灵活控制输出格式
实际项目中,JSON的格式往往需要定制。比如服务器返回的字段名是下划线风格,而Kotlin里是驼峰命名。或者有些字段为null时不想输出。
Json对象支持配置:
val json = Json {
prettyPrint = true // 格式化输出
ignoreUnknownKeys = true // 忽略未知字段
coerceInputValues = true // 强制转换输入值
encodeDefaults = false // 不输出默认值
namingStrategy = JsonNamingStrategy.SnakeCase // 下划线命名
}
@Serializable
data class UserInfo(
val userName: String,
val userAge: Int
)
fun main() {
val user = UserInfo("李四", 30)
println(json.encodeToString(user))
// 输出: {"user_name":"李四","user_age":30}
}
我的习惯:在项目里定义一个全局的Json实例,放在依赖注入容器里。这样所有地方都用同一个配置,避免不一致。
22.4 处理复杂场景:泛型、默认值、密封类
泛型序列化是很多库的痛点。在kotlinx.serialization里,泛型需要显式传递序列化器:
@Serializable
data class Response<T>(
val code: Int,
val message: String,
val data: T
)
fun <T> parseResponse(jsonString: String, serializer: KSerializer<T>): Response<T> {
return Json.decodeFromString(Response.serializer(serializer), jsonString)
}
// 使用
val jsonStr = """{"code":200,"message":"成功","data":{"name":"王五"}}"""
val userResponse = parseResponse(jsonStr, User.serializer())
密封类序列化是另一个亮点。我做过一个消息推送模块,消息类型有文本、图片、链接三种。用密封类加@Serializable,完美解决:
@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 Link(val title: String, val url: String) : Message()
}
val json = Json {
classDiscriminator = "type" // 指定类型标识字段名
}
fun main() {
val msg: Message = Message.Text("你好")
val jsonStr = json.encodeToString(msg)
println(jsonStr) // {"type":"Text","content":"你好"}
val decoded = json.decodeFromString<Message>(jsonStr)
println(decoded) // Text(content=你好)
}
注意:密封类的子类必须全部标记@Serializable,否则编译会报错。我曾经漏掉一个子类,排查了半天才发现。
22.5 自定义序列化器:掌控序列化过程
有些场景默认的序列化行为满足不了需求。比如日期格式、加密字段、或者第三方类的序列化。这时候就需要自定义序列化器。
举个例子,项目中经常遇到LocalDateTime,默认序列化出来是一串数字,可读性差。我习惯把它格式化成yyyy-MM-dd HH:mm:ss:
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
override val descriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalDateTime) {
encoder.encodeString(value.format(formatter))
}
override fun deserialize(decoder: Decoder): LocalDateTime {
return LocalDateTime.parse(decoder.decodeString(), formatter)
}
}
@Serializable
data class Event(
val name: String,
@Serializable(with = LocalDateTimeSerializer::class)
val time: LocalDateTime
)
还有一种情况:某个字段需要加密存储。比如用户的手机号,序列化时自动加密,反序列化时自动解密:
object EncryptedStringSerializer : KSerializer<String> {
override val descriptor = PrimitiveSerialDescriptor("EncryptedString", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: String) {
// 实际项目中用真正的加密算法
val encrypted = value.reversed() // 简单演示:反转字符串
encoder.encodeString(encrypted)
}
override fun deserialize(decoder: Decoder): String {
val encrypted = decoder.decodeString()
return encrypted.reversed()
}
}
@Serializable
data class UserAccount(
val username: String,
@Serializable(with = EncryptedStringSerializer::class)
val phone: String
)
避坑指南:自定义序列化器一定要保证serialize和deserialize是对称的。我曾经写反了加密和解密逻辑,结果数据存进去就读不出来了。测试用例一定要覆盖双向转换。
22.6 知识体系总览
下面这张图梳理了本章的核心知识点,方便你快速回顾:
22.7 实战中的注意事项
最后分享几个我在项目中积累的经验:
- 版本兼容性:kotlinx.serialization的版本要和Kotlin编译器版本匹配。我遇到过升级Kotlin版本后序列化插件报错的情况,查了文档才发现有对应关系。
- 性能测试:虽然它比Gson快,但如果你序列化超大对象(比如几百KB的JSON),还是建议做一下性能测试。我在一个日志上报模块里就发现序列化耗时偏高,后来加了缓存才解决。
- 多平台注意:如果你用Kotlin Multiplatform,记得
java.time在iOS上不可用。需要换成kotlinx-datetime库。 - 不要滥用自定义序列化器:能用注解解决的,就别手写序列化器。我见过一个项目,几乎每个字段都写了自定义序列化器,维护起来非常痛苦。
一句话总结:kotlinx.serialization是Kotlin生态里序列化的首选方案。它编译期生成代码、原生支持Kotlin特性、配置灵活。掌握它,你的Kotlin项目在数据交换这块就能游刃有余。
公众号:蓝海资料掘金营,微信deep3321