Skip to content
mac-system-tray

别再说 Kotlin Serialization 难用了!

我不止一次见到有开发者吐槽 Kotlin Serialization 难用。尤其是 Java 开发者将它与 Jackson \ Gson 来对比。这种印象主要源于对其工作原理的误解,Kotlin Serialization 并不依赖运行时反射机制来完成序列化/反序列化操作。

这个设计选择是经过深思熟虑的:Kotlin 是一个多平台语言,意味着同一份代码可以编译到 JVMAndroidNativeJavaScript 等不同平台。而反射机制在各个平台的实现和性能特征差异很大,有些平台甚至完全不支持反射。因此,Kotlin Serialization 选择了一个更优雅的解决方案:通过编译器插件在编译期生成序列化代码。

这种方案带来了几个显著优势:

  1. 跨平台兼容性:生成的代码可以在所有支持的平台上运行
  2. 更好的性能:避免了运行时反射带来的性能开销
  3. 编译期类型安全:序列化错误在编译期就能被发现

接下来让我们看看在 Compose Multiplatform 项目中如何使用 Kotlin Serialization

初始化 Compose Multiplatform 项目可以查看我之前的文章。所有源代码基于我开源项目 crosspaste-desktop

快速开始

1. Gradle 配置

首先需要在项目中添加 Kotlin Serialization 插件和依赖:

gradle/libs.versions.toml

toml
[versions]
kotlin = "2.0.21"

[libraries]
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }

[plugins]
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

composeApp/build.gradle.kts

kotlin
plugins {
    ...
    alias(libs.plugins.kotlinSerialization)
    ...
}

kotlin {

    sourceSets {
        commonMain.dependencies {
            implementation(libs.kotlinx.serialization.json)
        }
    }
}

2. 基础使用

让我们通过一个包含单元测试的示例来详细了解 Kotlin Serialization 的基础功能。这个示例不仅展示了基本用法,还通过测试用例确保了序列化和反序列化的正确性:

kotlin
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals

// @Serializable 注解告诉编译器需要为这个类生成序列化代码
@Serializable
data class User(
    // @SerialName 注解允许我们自定义序列化后的字段名
    // 这在与后端 API 对接时特别有用,比如 MongoDB 默认使用 _id 作为主键
    @SerialName("_id")
    val id: Int,
    val name: String,
    val email: String
)

class JsonTest {
    
    @Test
    fun testJson() {
        // 创建测试数据
        val user = User(1, "张三", "zhangsan@example.com")

        // 序列化为 JSON 字符串
        // 注意这里直接使用 Json.encodeToString(user),不需要显式传入序列化器
        // 这是因为 @Serializable 注解会在编译期自动生成所需的序列化器
        val jsonString = Json.encodeToString(user)
        
        // 验证序列化结果
        // 可以看到 id 字段被序列化为 _id,这是因为我们使用了 @SerialName 注解
        assertEquals(
            "{\"_id\":1,\"name\":\"张三\",\"email\":\"zhangsan@example.com\"}", 
            jsonString
        )

        // 从 JSON 字符串反序列化
        // 使用泛型函数 decodeFromString<User> 来指定目标类型
        // Kotlin 的类型推断能够自动处理大部分情况
        val decodedUser = Json.decodeFromString<User>(jsonString)
        
        // 验证反序列化结果
        // 通过比较原始对象和反序列化后的对象,确保整个序列化过程的正确性
        assertEquals(user, decodedUser)
    }
}

3. 自定义序列化

kotlin
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encodeToString
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals

@Serializable
data class Post(
    val id: Int,
    val title: String,
    // 使用 @Serializable 注解指定该字段使用自定义序列化器
    @Serializable(with = LocalDateTimeIso8601Serializer::class)
    val createTime: LocalDateTime
)

// 自定义序列化器需要实现 KSerializer 接口
object LocalDateTimeIso8601Serializer : KSerializer<LocalDateTime> {
    // descriptor 定义了这个类型在序列化时的基本信息
    // 这里我们将 LocalDateTime 序列化为字符串类型
    override val descriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)

    // 实现序列化逻辑:如何将对象转换为基本类型
    override fun serialize(encoder: Encoder, value: LocalDateTime) {
        encoder.encodeString(value.toString())
    }

    // 实现反序列化逻辑:如何从基本类型恢复对象
    override fun deserialize(decoder: Decoder): LocalDateTime {
        return LocalDateTime.parse(decoder.decodeString())
    }
}

class CustomJsonTest {
    @Test
    fun testCustomSerializer() {
        val post = Post(1, "Hello", LocalDateTime(2021, 1, 1, 12, 0))

        val jsonString = Json.encodeToString(post)
        assertEquals(
            "{\"id\":1,\"title\":\"Hello\",\"createTime\":\"2021-01-01T12:00\"}", 
            jsonString
        )

        val decodedPost = Json.decodeFromString<Post>(jsonString)
        assertEquals(post, decodedPost)
    }
}

实现自定义序列化器主要需要以下步骤:

  1. 定义序列化器类:
  • 实现 KSerializer<T> 接口,其中 T 是要序列化的类型
  • 通常定义为 object,因为序列化器通常是无状态的
  1. 提供序列化描述符:
  • 实现 descriptor 属性
  • 描述符定义了序列化后的数据类型(如字符串、数字等)
  • 使用 PrimitiveSerialDescriptor 表示基本类型
  1. 实现序列化/反序列化方法:
  • serialize:将对象转换为基本类型
  • deserialize:将基本类型转换回对象
  • 使用 encoder/decoder 提供的方法进行基本类型的编解码
  1. 应用序列化器:
  • 使用 @Serializable(with = ...) 注解指定序列化器
  • 可以针对特定字段使用不同的序列化策略

这种方式让我们能够:

  • 完全控制序列化和反序列化的过程
  • 将复杂类型转换为可序列化的基本类型
  • 保持类型安全和编译时检查

4. 多态序列化

多态序列化是处理继承关系时的一个关键功能。当我们需要序列化一个可能包含多个子类型的基类或接口时,就需要用到多态序列化。这在处理插件系统、数据存储、网络传输等场景下特别有用。

composeApp/src/desktopMain/kotlin/com/crosspaste/utils /JsonUtils.desktop.kt

kotlin
object DesktopJsonUtils : JsonUtils {

    override val JSON: Json =
        Json {
            encodeDefaults = true
            ignoreUnknownKeys = true
            serializersModule =
                SerializersModule {

                    polymorphic(PasteItem::class) {
                        subclass(ColorPasteItem::class)
                        subclass(FilesPasteItem::class)
                        subclass(HtmlPasteItem::class)
                        subclass(ImagesPasteItem::class)
                        subclass(RtfPasteItem::class)
                        subclass(TextPasteItem::class)
                        subclass(UrlPasteItem::class)
                    }
                }
        }
}

composeApp/src/commonMain/kotlin/com/crosspaste/dto/paste/SyncPasteCollection.kt

kotlin
@Serializable
data class SyncPasteCollection(
    val pasteItems: List<PasteItem>,
)

在这个例子中:

  1. 类型体系设计:
  • PasteItem 作为基类,统一抽象了不同类型的粘贴板内容
  • 各种具体实现(如 ColorPasteItemFilesPasteItem 等)处理不同的数据类型
  • @Serializable 注解和多态配置让这个类型体系可以被序列化和反序列化
  1. 应用场景:
  • 网络同步:当用户在设备 A 复制内容时,可以将 PasteItem 序列化后发送到设备 B
  • 本地存储:可以将不同类型的粘贴板内容统一保存到本地数据库
  1. 实现优势:
  • 类型安全:完整保留了类型信息,接收方可以安全地还原出正确的类型
  • 扩展性好:添加新的粘贴板类型只需创建新的 PasteItem 子类并注册到 SerializersModule
  • 代码简洁:使用 List<PasteItem> 这样简单的数据结构就能处理所有类型的粘贴板内容

这种设计让 CrossPaste 能够优雅地处理各种类型的粘贴板内容,无论是文本、图片、文件还是富文本,都能在不同设备间可靠地传输和还原。这充分展示了 Kotlin Serialization 在实际项目中的应用价值。

总结

Kotlin Serialization 的设计选择反映了它的核心目标:为 Kotlin 多平台项目提供统一的序列化解决方案。它通过编译期代码生成而不是运行时反射来实现序列化,这带来了跨平台兼容性、类型安全性和优秀的性能表现。 然而,选择序列化库时需要根据具体场景来权衡:

  • 如果你的项目是纯 JVM 环境:

    • Jackson/Gson 可能是更好的选择
    • 它们拥有更成熟的生态系统
    • 使用起来更简单直观
    • 有更丰富的功能支持和社区资源
  • 如果你的项目涉及多平台开发:

    • Kotlin Serialization 是理想选择
    • 一套代码可以运行在所有平台
    • 编译期保证类型安全
    • Kotlin 语言特性深度整合
    • 更适合现代的 Kotlin-first 架构

归根结底,Kotlin Serialization 不是为了取代 Jackson/Gson,而是为了解决多平台序列化的问题。理解这一点,我们就不会把它简单地与 Java 序列化库进行比较,而是应该在合适的场景下使用合适的工具。选择技术栈时,项目的具体需求永远是最重要的考虑因素。