第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
)

避坑指南:自定义序列化器一定要保证serializedeserialize是对称的。我曾经写反了加密和解密逻辑,结果数据存进去就读不出来了。测试用例一定要覆盖双向转换。

22.6 知识体系总览

下面这张图梳理了本章的核心知识点,方便你快速回顾:

Kotlin序列化知识体系 kotlinx.serialization 基础使用 @Serializable 注解 Json.encodeToString Json.decodeFromString JSON配置 prettyPrint / ignoreUnknownKeys coerceInputValues / encodeDefaults namingStrategy / classDiscriminator 高级特性 泛型序列化 密封类序列化 自定义序列化器 编译期代码生成 · 零反射 · 多平台支持

22.7 实战中的注意事项

最后分享几个我在项目中积累的经验:

  • 版本兼容性:kotlinx.serialization的版本要和Kotlin编译器版本匹配。我遇到过升级Kotlin版本后序列化插件报错的情况,查了文档才发现有对应关系。
  • 性能测试:虽然它比Gson快,但如果你序列化超大对象(比如几百KB的JSON),还是建议做一下性能测试。我在一个日志上报模块里就发现序列化耗时偏高,后来加了缓存才解决。
  • 多平台注意:如果你用Kotlin Multiplatform,记得java.time在iOS上不可用。需要换成kotlinx-datetime库。
  • 不要滥用自定义序列化器:能用注解解决的,就别手写序列化器。我见过一个项目,几乎每个字段都写了自定义序列化器,维护起来非常痛苦。

一句话总结:kotlinx.serialization是Kotlin生态里序列化的首选方案。它编译期生成代码、原生支持Kotlin特性、配置灵活。掌握它,你的Kotlin项目在数据交换这块就能游刃有余。


公众号:蓝海资料掘金营,微信deep3321