Kotlin Serialization 入门指南 – wiki基地


Kotlin Serialization 入门指南:告别反射,拥抱现代 Kotlin 数据处理

在现代应用程序开发中,数据交换和持久化是不可或缺的一环。无论是与后端 API 通信、在本地存储用户配置,还是在不同模块间传递数据,我们都需要一种机制来将内存中的对象转换为可传输或可存储的格式(序列化),以及将这些格式的数据转换回内存中的对象(反序列化)。

在 Java 生态中,像 Jackson 和 Gson 这样的库长期以来一直是序列化的主流选择。它们功能强大、社区成熟,但主要依赖于 Java 的反射机制。虽然反射提供了极大的灵活性,但它也带来了一些固有的缺点:

  1. 性能开销: 反射操作通常比直接代码调用慢,尤其是在启动时和处理大量小对象时。
  2. 运行时错误: 很多与序列化相关的问题(如类型不匹配、缺少构造函数)只有在运行时才能发现。
  3. 安全性问题: 反射可能破坏封装性,并可能在某些受限环境中(如 Android 的 R8/ProGuard 优化或 GraalVM Native Image)需要复杂的配置才能正常工作。
  4. 与 Kotlin 特性集成不佳: 对 Kotlin 的空安全、默认参数、data class 等特性的支持并非原生,有时需要额外的模块或配置。

为了解决这些问题,并提供一个与 Kotlin 语言特性深度集成、类型安全且高性能的解决方案,JetBrains 推出了官方的序列化库:kotlinx.serialization

什么是 Kotlinx Serialization?

kotlinx.serialization 是一个 Kotlin 官方支持的、跨平台(Multiplatform)、类型安全且高性能的序列化框架。它与 Kotlin 编译器深度集成,通过编译器插件在编译时生成序列化所需的代码,从而完全避免了运行时的反射开销。

核心优势:

  1. Kotlin 优先: 完美支持 Kotlin 的语言特性,如 data classenum classsealed class、泛型、空安全和默认参数。
  2. 类型安全: 编译时代码生成确保了序列化和反序列化过程的类型安全,许多错误可以在编译阶段就被捕获。
  3. 高性能: 由于避免了反射,其运行时性能通常优于基于反射的库,尤其是在启动时间和内存占用方面。
  4. 跨平台(Multiplatform): 设计之初就考虑了 Kotlin Multiplatform,可以在 JVM、JavaScript、Native(iOS, macOS, Linux, Windows 等)以及 Wasm 等多个平台上共享序列化逻辑。
  5. 可扩展格式: 不仅仅局限于 JSON,还支持 Protobuf (Protocol Buffers)、CBOR (Concise Binary Object Representation),并且可以扩展支持其他格式。
  6. 无缝集成: 与 Ktor、Exposed 等其他 Kotlin 库有良好的集成。

本指南将带你一步步入门 kotlinx.serialization,重点关注最常用的 JSON 格式。

1. 环境设置

要在项目中使用 kotlinx.serialization,你需要进行以下设置:

a. 添加 Gradle 插件

在你的项目根目录下的 build.gradle.kts (或 build.gradle) 文件中添加 Kotlin Serialization 插件的依赖:

kotlin
// build.gradle.kts (Root project)
plugins {
kotlin("jvm") version "1.9.20" // Use your Kotlin version
// Apply the serialization plugin *after* the Kotlin plugin
kotlin("plugin.serialization") version "1.9.20" // Use the same version as Kotlin
}

b. 应用插件

在你需要使用序列化的模块(通常是 app 或某个 library 模块)的 build.gradle.kts (或 build.gradle) 文件中应用该插件:

kotlin
// build.gradle.kts (Module level)
plugins {
id("org.jetbrains.kotlin.jvm") // Or kotlin("multiplatform"), kotlin("android"), etc.
// Apply the serialization plugin *here* as well
id("org.jetbrains.kotlin.plugin.serialization")
}

c. 添加运行时库依赖

同样在模块的 build.gradle.kts (或 build.gradle) 文件中,添加核心运行时库和特定格式(如 JSON)的库:

“`kotlin
// build.gradle.kts (Module level)
dependencies {
// Core serialization runtime library (mandatory)
implementation(“org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2”) // Check for the latest version

// JSON format support (add this for JSON serialization)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") // Use the same version as core

// Add other dependencies as needed...

}
“`

注意: 请务必将版本号(示例中的 1.9.201.6.2)替换为当前最新的稳定版本。Kotlin 插件版本通常应与你的 Kotlin 版本保持一致,kotlinx.serialization 库版本则独立更新。

完成这些步骤并同步 Gradle 项目后,你就可以开始使用 Kotlin Serialization 了。

2. 基本用法:序列化和反序列化

kotlinx.serialization 的核心是 @Serializable 注解和处理特定格式的类(如 Json)。

a. 使类可序列化

要让一个类能够被序列化和反序列化,你需要在其声明前添加 @Serializable 注解。通常,这会用于 data class,但也可以用于普通类、objectenum class 等。

“`kotlin
import kotlinx.serialization.
import kotlinx.serialization.json.

// Apply the @Serializable annotation
@Serializable
data class User(
val id: Long,
val username: String,
val email: String?, // Nullable properties are supported
val isActive: Boolean = true // Default values are handled
)

fun main() {
// — Serialization (Object to JSON String) —

val user = User(id = 1, username = "kotlin_dev", email = "[email protected]")

// Create a Json instance (default configuration)
val jsonFormat = Json

// Encode the object to a JSON string
val jsonString = jsonFormat.encodeToString(user)

println("Serialized JSON:")
println(jsonString)
// Output: {"id":1,"username":"kotlin_dev","email":"[email protected]","isActive":true}

// --- Deserialization (JSON String to Object) ---

val receivedJson = """
    {"id":2, "username":"serialization_fan", "email":null, "isActive":false}
""".trimIndent()

// Decode the JSON string back into a User object
val decodedUser = jsonFormat.decodeFromString<User>(receivedJson)

println("\nDeserialized User:")
println(decodedUser)
// Output: User(id=2, username=serialization_fan, email=null, isActive=false)

}
“`

工作原理简述:

  1. @Serializable 注解告诉 Kotlin 编译器插件:“为这个类生成序列化/反序列化所需的代码”。
  2. 编译器插件在编译期间会为 User 类生成一个伴生 serializer() 方法(或者一个独立的 User.serializer() 对象),这个 serializer 包含了如何读写 User 对象所有字段的逻辑。
  3. Json.encodeToString(user) 方法内部会查找 Userserializer(),并使用它将 user 对象转换为 JSON 字符串。
  4. Json.decodeFromString<User>(receivedJson) 方法同样会找到 Userserializer(),并用它根据 JSON 字符串的内容创建一个新的 User 实例。

3. 处理复杂数据结构

kotlinx.serialization 自然支持嵌套对象、列表、映射等复杂结构。

“`kotlin
import kotlinx.serialization.
import kotlinx.serialization.json.

@Serializable
data class Address(
val street: String,
val city: String,
val zipCode: String
)

@Serializable
enum class Role {
ADMIN, USER, GUEST
}

@Serializable
data class Project(
val name: String,
val owner: User, // Nested @Serializable object
val contributors: List, // List of @Serializable objects
val settings: Map, // Map of primitives
val accessRole: Role // Enum support
)

fun main() {
val owner = User(1, “project_owner”, “[email protected]”)
val contributor1 = User(2, “contributor_1”, null)
val contributor2 = User(3, “contributor_2”, “[email protected]”, false)

val project = Project(
    name = "Kotlinx Serialization Demo",
    owner = owner,
    contributors = listOf(contributor1, contributor2),
    settings = mapOf("theme" to "dark", "notifications" to "enabled"),
    accessRole = Role.ADMIN
)

val jsonFormat = Json { prettyPrint = true } // Use pretty printing for readability

// Serialize Project object
val projectJson = jsonFormat.encodeToString(project)
println("Serialized Project JSON:")
println(projectJson)

// Deserialize Project JSON
val deserializedProject = jsonFormat.decodeFromString<Project>(projectJson)
println("\nDeserialized Project:")
println(deserializedProject)
println("Owner's username: ${deserializedProject.owner.username}")
println("Number of contributors: ${deserializedProject.contributors.size}")
println("Theme setting: ${deserializedProject.settings["theme"]}")
println("Access Role: ${deserializedProject.accessRole}")

}

// Assume User class from previous example is defined and @Serializable
@Serializable
data class User(
val id: Long,
val username: String,
val email: String?,
val isActive: Boolean = true
)
“`

要点:

  • 只要嵌套对象(如 User)、列表/映射中的元素类型(如 User, String, Role)本身是 @Serializable 的,kotlinx.serialization 就能自动处理它们。
  • 枚举类型默认按其名称(ADMIN, USER, GUEST)进行序列化。

4. 定制化 JSON 处理

Json 对象可以通过一个配置块来进行定制,以满足不同的需求。

“`kotlin
import kotlinx.serialization.
import kotlinx.serialization.json.

@Serializable
data class Config(
val featureEnabled: Boolean,
val timeoutMillis: Long = 5000L, // Has a default value
// This field might exist in JSON but not in our class
// val legacyField: String? = null
val logLevel: String = “INFO”
)

fun main() {
// — Configuration Options —

// 1. Pretty Printing (human-readable output with indentation)
val prettyJson = Json { prettyPrint = true }
val config = Config(featureEnabled = true)
println("Pretty JSON:\n${prettyJson.encodeToString(config)}\n")

// 2. Ignore Unknown Keys (robustness against extra fields in JSON)
val lenientJson = Json { ignoreUnknownKeys = true }
val jsonWithExtraField = """
    {"featureEnabled": false, "timeoutMillis": 10000, "newUnexpectedField": "someValue"}
""".trimIndent()
// This would throw an exception with default Json, but works with ignoreUnknownKeys = true
val decodedConfig = lenientJson.decodeFromString<Config>(jsonWithExtraField)
println("Decoded with unknown key ignored: $decodedConfig\n")

// 3. Encode Default Values (include properties even if they have default values)
val jsonWithDefaults = Json { encodeDefaults = true }
// Default Json would omit timeoutMillis and logLevel if they match defaults
println("JSON with defaults encoded:\n${jsonWithDefaults.encodeToString(config)}\n")
// Default Json output for comparison: {"featureEnabled":true}

// 4. Lenient Parsing (allow non-standard JSON, e.g., unquoted strings - use with caution)
val veryLenientJson = Json { isLenient = true; ignoreUnknownKeys = true }
val nonStandardJson = "{featureEnabled: true, timeoutMillis: 3000}" // Keys/boolean not quoted
val lenientlyDecoded = veryLenientJson.decodeFromString<Config>(nonStandardJson)
println("Decoded leniently: $lenientlyDecoded\n")

// 5. Class Discriminator (for polymorphic serialization, see later section)
// val polymorphicJson = Json { classDiscriminator = "type" }

// Combine configurations
val customJson = Json {
    prettyPrint = true
    ignoreUnknownKeys = true
    encodeDefaults = false // Explicitly set to false (which is the default anyway)
}
println("Custom JSON config output:\n${customJson.encodeToString(decodedConfig)}")

}
“`

常用配置选项:

  • prettyPrint: Boolean: 输出格式化的 JSON 字符串,易于阅读。默认为 false
  • ignoreUnknownKeys: Boolean: 反序列化时,如果 JSON 中包含 Kotlin 类中没有的字段,是否忽略它们。默认为 false(会抛出异常)。设置为 true 可以增强对 API 变化的容错性。
  • encodeDefaults: Boolean: 序列化时,是否包含那些值等于其在类定义中声明的默认值的属性。默认为 false(不包含,以减小输出体积)。
  • isLenient: Boolean: 是否允许解析一些不完全符合 JSON 规范的格式(如无引号的键或字符串)。默认为 false。谨慎使用。
  • classDiscriminator: String: 用于多态序列化时,指定 JSON 中表示具体类型的字段名称。默认为 "type"
  • explicitNulls: Boolean: 控制是否将值为 null 的属性显式写入 JSON("key": null)。默认为 true。设为 false 则会省略这些键值对,但需配合 encodeDefaults = true 理解其行为,通常保持默认。

5. 处理属性名称差异:@SerialName

有时,Kotlin 属性名(通常是 camelCase)与 JSON 字段名(可能是 snake_case 或其他约定)不一致。可以使用 @SerialName 注解来指定 JSON 中的名称。

“`kotlin
import kotlinx.serialization.
import kotlinx.serialization.json.

@Serializable
data class Product(
val id: Int,
@SerialName(“product_name”) // Maps ‘productName’ Kotlin property to ‘product_name’ JSON key
val productName: String,
@SerialName(“unit_price”)
val unitPrice: Double,
// This property name matches the JSON key, no annotation needed
val available: Boolean
)

fun main() {
val product = Product(id = 101, productName = “Kotlin Book”, unitPrice = 29.99, available = true)

val jsonFormat = Json { prettyPrint = true }

// Serialization: Kotlin names -> JSON names
val jsonString = jsonFormat.encodeToString(product)
println("Serialized with @SerialName:")
println(jsonString)
/* Output:
{
    "id": 101,
    "product_name": "Kotlin Book",
    "unit_price": 29.99,
    "available": true
}
*/

// Deserialization: JSON names -> Kotlin names
val incomingJson = """
    {
        "id": 102,
        "product_name": "IDE License",
        "unit_price": 99.0,
        "available": false
    }
""".trimIndent()
val deserializedProduct = jsonFormat.decodeFromString<Product>(incomingJson)
println("\nDeserialized with @SerialName:")
println(deserializedProduct)
// Output: Product(id=102, productName=IDE License, unitPrice=99.0, available=false)

}
“`

6. 多态序列化(Polymorphism)

当需要序列化一个继承层次结构或接口实现,并且在反序列化时需要恢复到具体的子类型时,就需要多态序列化。

“`kotlin
import kotlinx.serialization.
import kotlinx.serialization.json.

// Base interface or abstract class must be @Serializable
@Serializable
sealed interface ApiResponse {
val success: Boolean
}

// Concrete implementations must also be @Serializable
// Use @SerialName to specify the value for the class discriminator field
@Serializable
@SerialName(“success_data”) // This value will be used for the ‘type’ field
data class SuccessResponse(
val data: String,
override val success: Boolean = true
) : ApiResponse

@Serializable
@SerialName(“error_info”) // This value will be used for the ‘type’ field
data class ErrorResponse(
val errorCode: Int,
val message: String,
override val success: Boolean = false
) : ApiResponse

@Serializable
data class ResponseWrapper(
val id: String,
// The field containing the polymorphic object
val response: ApiResponse
)

fun main() {
// Configure Json to use a class discriminator
// The ‘classDiscriminator’ property name (“type” by default) will be added to the JSON
val polymorphicJson = Json {
prettyPrint = true
classDiscriminator = “response_type” // Custom discriminator name
// Register subtypes within a serializers module (recommended for sealed classes/interfaces)
serializersModule = SerializersModule {
polymorphic(ApiResponse::class) {
subclass(SuccessResponse::class)
subclass(ErrorResponse::class)
}
}
}

val successWrapper = ResponseWrapper("req-123", SuccessResponse("Data loaded successfully"))
val errorWrapper = ResponseWrapper("req-456", ErrorResponse(404, "Resource not found"))

// Serialize polymorphic objects
val successJson = polymorphicJson.encodeToString(successWrapper)
val errorJson = polymorphicJson.encodeToString(errorWrapper)

println("Serialized Success Response:")
println(successJson)
/* Output:
{
    "id": "req-123",
    "response": {
        "response_type": "success_data", // Discriminator field added
        "data": "Data loaded successfully",
        "success": true
    }
}
*/

println("\nSerialized Error Response:")
println(errorJson)
/* Output:
{
    "id": "req-456",
    "response": {
        "response_type": "error_info", // Discriminator field added
        "errorCode": 404,
        "message": "Resource not found",
        "success": false
    }
}
*/

// Deserialize polymorphic objects
val deserializedSuccess = polymorphicJson.decodeFromString<ResponseWrapper>(successJson)
val deserializedError = polymorphicJson.decodeFromString<ResponseWrapper>(errorJson)

println("\nDeserialized Response 1 Type: ${deserializedSuccess.response::class.simpleName}") // SuccessResponse
println("Deserialized Response 2 Type: ${deserializedError.response::class.simpleName}")   // ErrorResponse

when (val response = deserializedSuccess.response) {
    is SuccessResponse -> println("Success data: ${response.data}")
    is ErrorResponse -> println("Error message: ${response.message}")
}

}
“`

多态序列化关键点:

  1. 基类/接口需要 @Serializable
  2. 所有具体的子类也需要 @Serializable
  3. 使用 @SerialName("unique_identifier") 注解在子类上指定它们在 JSON 中表示类型的唯一标识符。或者,如果使用 sealed classsealed interface,编译器可以自动推断,但通常仍建议显式注册。
  4. 配置 Json 实例时:
    • 设置 classDiscriminator 属性,指定 JSON 中包含类型标识符的字段名称(如 "type", "kind", "response_type")。
    • (推荐) 使用 serializersModule 明确注册多态基类及其所有子类。这有助于编译器插件验证和查找,尤其对于非 sealed 的层次结构是必需的。

7. 自定义序列化器(Custom Serializers)

有时,你需要对序列化过程进行更精细的控制,或者需要序列化那些无法直接添加 @Serializable 注解的类(如第三方库的类或 Java 标准库类,像 java.util.Date)。这时可以创建自定义序列化器。

实现 KSerializer<T> 接口,并重写 descriptorserializedeserialize 方法。

“`kotlin
import kotlinx.serialization.
import kotlinx.serialization.descriptors.

import kotlinx.serialization.encoding.
import kotlinx.serialization.json.

import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter

// Example: Custom serializer for java.time.Instant to ISO 8601 string

object InstantSerializer : KSerializer {
// Define a descriptor for the type – here it’s a primitive string
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(“java.time.Instant”, PrimitiveKind.STRING)

private val formatter = DateTimeFormatter.ISO_INSTANT

// Logic to convert Instant object to JSON (a string in this case)
override fun serialize(encoder: Encoder, value: Instant) {
    encoder.encodeString(formatter.format(value))
}

// Logic to convert JSON string back to Instant object
override fun deserialize(decoder: Decoder): Instant {
    return Instant.parse(decoder.decodeString())
}

}

// Apply the custom serializer using @Serializable(with = …)
@Serializable
data class Event(
val name: String,
@Serializable(with = InstantSerializer::class)
val timestamp: Instant
)

fun main() {
val event = Event(“UserLoggedIn”, Instant.now())

val jsonFormat = Json { prettyPrint = true }

// Serialization using the custom serializer
val jsonString = jsonFormat.encodeToString(event)
println("Serialized with Custom Serializer:")
println(jsonString)
/* Example Output:
{
    "name": "UserLoggedIn",
    "timestamp": "2023-10-27T10:30:00.123456Z" // ISO 8601 format
}
*/

// Deserialization using the custom serializer
val incomingJson = """
    {
        "name": "DataProcessed",
        "timestamp": "2023-10-27T11:00:00Z"
    }
""".trimIndent()
val deserializedEvent = jsonFormat.decodeFromString<Event>(incomingJson)
println("\nDeserialized with Custom Serializer:")
println(deserializedEvent)
println("Timestamp Year: ${deserializedEvent.timestamp.atZone(ZoneId.systemDefault()).year}")

}
“`

何时使用自定义序列化器?

  • 序列化/反序列化无法修改的第三方类。
  • 需要非标准的序列化格式(如将 Color 对象序列化为 #RRGGBB 字符串)。
  • 处理非常复杂或有特殊约束的数据结构。
  • 需要与仅支持特定格式(如 ISO 8601 日期)的外部系统交互。

8. 上下文序列化器(Contextual Serializers)

如果不想或不能在每个使用点都用 @Serializable(with = ...) 注解,或者想为某个类型(尤其是第三方类型)全局定义一个序列化器,可以使用上下文序列化器

“`kotlin
import kotlinx.serialization.
import kotlinx.serialization.modules.

import kotlinx.serialization.json.*
import java.util.UUID // Example: A class without @Serializable

// Assume InstantSerializer from the previous example exists

// A class using UUID and Instant without direct @Serializable(with=…) annotation
@Serializable
data class Transaction(
val description: String,
// We want to serialize UUID contextually
val transactionId: UUID,
// We want to serialize Instant contextually
val occurredAt: java.time.Instant
)

// Custom serializer for UUID
object UUIDSerializer : KSerializer {
override val descriptor = PrimitiveSerialDescriptor(“java.util.UUID”, PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: UUID) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString())
}

fun main() {
// Create a SerializersModule to register contextual serializers
val appSerializersModule = SerializersModule {
// Register UUIDSerializer for the UUID class
contextual(UUID::class, UUIDSerializer)
// Register InstantSerializer for the Instant class
contextual(java.time.Instant::class, InstantSerializer)
// You can register more contextual or polymorphic serializers here
}

// Create a Json instance configured with the module
val contextualJson = Json {
    serializersModule = appSerializersModule
    prettyPrint = true
}

val transaction = Transaction(
    description = "Payment received",
    transactionId = UUID.randomUUID(),
    occurredAt = java.time.Instant.now()
)

// Now, serialization and deserialization of Transaction will automatically use
// the registered contextual serializers for UUID and Instant fields.
val jsonString = contextualJson.encodeToString(transaction)
println("Serialized with Contextual Serializers:")
println(jsonString)

val deserializedTransaction = contextualJson.decodeFromString<Transaction>(jsonString)
println("\nDeserialized with Contextual Serializers:")
println(deserializedTransaction)

}
“`

上下文序列化器通过 SerializersModule 进行注册,并将该模块提供给 Json 配置。这样,当 kotlinx.serialization 遇到需要序列化 UUIDInstant 类型时,它会查找模块中注册的序列化器,而无需在 Transaction 类中显式指定。

9. 超越 JSON:其他格式

虽然本指南重点是 JSON,但 kotlinx.serialization 的架构是可插拔的。你可以轻松切换到其他格式,只需添加相应的依赖并使用对应的格式化器对象即可。

  • CBOR: 添加 org.jetbrains.kotlinx:kotlinx-serialization-cbor 依赖,使用 Cbor 对象。CBOR 是一种二进制格式,通常比 JSON 更紧凑、解析更快。
  • Protobuf: 添加 org.jetbrains.kotlinx:kotlinx-serialization-protobuf 依赖,使用 ProtoBuf 对象。Protobuf 是 Google 开发的二进制格式,特别适合 RPC 和数据持久化,需要配合 .proto 文件定义模式(尽管 kotlinx.serialization 也可以在没有 .proto 的情况下使用,但通常不推荐)。

核心的 @Serializable 注解、自定义序列化器等概念在所有格式中都是通用的。

10. 总结与展望

kotlinx.serialization 是 Kotlin 生态系统中用于数据序列化的现代化、强大且高效的解决方案。它通过编译时代码生成,提供了类型安全和高性能,完美契合 Kotlin 语言特性,并具备出色的跨平台能力。

关键优势回顾:

  • 原生 Kotlin 支持: 完美处理 Kotlin 的数据类、空安全、默认值等。
  • 类型安全: 编译时检查减少运行时错误。
  • 高性能: 避免反射开销。
  • 跨平台: 一套逻辑,多平台运行(JVM, JS, Native, Wasm)。
  • 灵活配置: 可通过 Json 配置块轻松定制行为。
  • 可扩展性: 支持多态、自定义序列化器和多种数据格式。

对于新的 Kotlin 项目,尤其是需要跨平台支持或关注性能和类型安全的应用,kotlinx.serialization 是一个非常值得选择的序列化库。它代表了 Kotlin 在数据处理方面的发展方向,告别了传统基于反射的方案的诸多限制。

随着你对 kotlinx.serialization 的深入使用,你还会发现更多高级特性,如处理泛型、更复杂的自定义序列化器编写、与 Ktor 等框架的集成等。希望本入门指南为你打下了坚实的基础,助你开始在 Kotlin 项目中高效、安全地处理数据序列化。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部