跳到主要内容

Kotlin 序列化实战:用 @Serializable 告别手写 JSON 解析的噩梦

· 阅读需 6 分钟
重庆 Kotlin User Group

你一定经历过这种痛苦:后端甩给你一个 JSON,你得手动解析每个字段,稍有不慎就 NullPointerException 或者类型转换错误。就算用 Gson 或 Moshi,也得写一堆 TypeAdapter,或者忍受反射带来的性能开销。

// 用 Gson 解析 JSON 的"经典"写法
val user = Gson().fromJson(jsonString, User::class.java)
// 如果 JSON 里多了个字段?Gson 默认忽略,你完全不知道
// 如果字段类型不对?运行时直接炸
// 如果要用 ProGuard 混淆?还得加 keep 规则

Kotlin 官方的 kotlinx.serialization 库,用一个 @Serializable 注解就能解决这些问题——编译期生成序列化代码,零反射,类型安全,还跨平台(JVM/JS/Native 通吃)。

5 分钟上手:一个注解搞定一切​

先看看最简单的用法:

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class User(val id: Int, val name: String, val email: String)

fun main() {
// 序列化:对象 → JSON 字符串
val user = User(1, "Alice", "alice@example.com")
val json = Json.encodeToString(user)
println(json)
// 输出:{"id":1,"name":"Alice","email":"alice@example.com"}

// 反序列化:JSON 字符串 → 对象
val parsed = Json.decodeFromString<User>(json)
println(parsed)
// 输出:User(id=1, name=Alice, email=alice@example.com)
}

就这么简单。@Serializable 告诉编译器:"帮我自动生成这个类的序列化和反序列化代码"。不需要反射,不需要手写 TypeAdapter,编译期就全部搞定。

处理真实世界的 JSON​

现实中的 JSON 往往没那么规矩。字段名可能是 snake_case,有些字段可能缺失,还可能有嵌套结构。kotlinx.serialization 都能优雅应对。

@SerialName:字段名映射​

后端给你的字段名是 user_name,但 Kotlin 里你想用 userName?用 @SerialName 注解:

@Serializable
data class GitHubUser(
val id: Int,
@SerialName("login") val userName: String,
@SerialName("avatar_url") val avatarUrl: String,
val bio: String? = null // 可空 + 默认值 = JSON 里没有也不报错
)

fun main() {
val json = """
{
"id": 12345,
"login": "octocat",
"avatar_url": "https://github.com/images/error/octocat_happy.gif"
}
""".trimIndent()

val user = Json.decodeFromString<GitHubUser>(json)
println(user.userName) // octocat
println(user.bio) // null(JSON 里没有,默认 null)
}

默认值:JSON 缺字段也不慌​

@Serializable
data class Config(
val host: String = "localhost",
val port: Int = 8080,
val debug: Boolean = false
)

fun main() {
// JSON 里只有 host,其他字段用默认值
val json = """{"host": "api.example.com"}"""
val config = Json.decodeFromString<Config>(json)
println(config) // Config(host=api.example.com, port=8080, debug=false)
}

这在处理后端接口升级时特别有用——新增的字段给个默认值,老客户端完全不用改。

嵌套结构和集合​

真实 API 返回的数据往往是嵌套的:

@Serializable
data class ApiResponse(
val code: Int,
val message: String,
val data: UserList?
)

@Serializable
data class UserList(
val users: List<UserItem>,
val total: Int,
val page: Int
)

@Serializable
data class UserItem(
val id: Int,
val name: String,
val tags: List<String> = emptyList()
)

fun main() {
val json = """
{
"code": 200,
"message": "success",
"data": {
"users": [
{"id": 1, "name": "Alice", "tags": ["admin", "vip"]},
{"id": 2, "name": "Bob"}
],
"total": 2,
"page": 1
}
}
""".trimIndent()

val response = Json.decodeFromString<ApiResponse>(json)
println(response.data?.users?.first()?.name) // Alice
println(response.data?.users?.last()?.tags) // [](默认空列表)
}

嵌套的数据类只要都标了 @Serializable,序列化框架会自动递归处理,不需要你写任何额外代码。

Json 配置:让它更宽容​

默认的 Json 实例很严格——JSON 里有类没定义的字段?直接报错。实际开发中,你通常需要更宽容的配置:

val lenientJson = Json {
ignoreUnknownKeys = true // 忽略 JSON 中多余的字段
isLenient = true // 宽松模式:允许非标准 JSON(如键名不加引号)
prettyPrint = true // 输出格式化后的 JSON
encodeDefaults = true // 序列化时包含默认值
explicitNulls = false // null 字段不输出到 JSON 中
coerceInputValues = true // 遇到不合法的值时回退到默认值
}

建议在全局定义一个配置好的 Json 实例,项目中统一使用:

// App.kt 或 NetworkModule.kt
object AppJson {
val instance = Json {
ignoreUnknownKeys = true
coerceInputValues = true
encodeDefaults = false
explicitNulls = false
}
}

// 使用
val user = AppJson.instance.decodeFromString<User>(jsonString)

实战:封装一个通用网络请求​

把序列化集成到一个简单的网络请求封装里:

@Serializable
data class Result<out T>(
val code: Int = 0,
val message: String = "",
val data: T? = null
) {
val isSuccess get() = code == 200
}

@Serializable
data class UserInfo(
val id: Long,
val nickname: String,
@SerialName("avatar_url") val avatar: String = ""
)

// 模拟网络请求
suspend fun fetchUser(userId: Long): Result<UserInfo> {
// 实际项目中这里用 Ktor 或 OkHttp 发请求
val mockJson = """
{
"code": 200,
"message": "ok",
"data": {
"id": $userId,
"nickname": "Kotlin爱好者",
"avatar_url": "https://example.com/avatar.png"
}
}
""".trimIndent()

return AppJson.instance.decodeFromString(mockJson)
}

// 使用
suspend fun main() {
val result = fetchUser(1001)
if (result.isSuccess) {
val user = result.data!!
println("欢迎, ${user.nickname}!") // 欢迎, Kotlin爱好者!
println("头像: ${user.avatar}")
} else {
println("请求失败: ${result.message}")
}
}

这段代码展示了 kotlinx.serialization 的几个核心优势:

  1. 泛型支持:Result<T> 可以包装任何可序列化的类型
  2. 编译期安全:字段类型不对,编译时就报错,不是运行时炸
  3. 零反射:序列化代码在编译期生成,性能比 Gson 好

小结​

特性说明
@Serializable一个注解,自动生成序列化代码
@SerialName映射 JSON 字段名,适配后端命名风格
默认值JSON 缺字段时自动使用,接口升级零负担
可空类型正确处理 JSON 中的 null
嵌套结构递归自动处理,无需额外代码
Json { } 配置宽容模式、格式化、忽略未知字段等

一句话总结:只要你在 Kotlin 里处理 JSON,就用 kotlinx.serialization。 它比 Gson 更安全(编译期检查),比 Moshi 更灵活(支持 KMP),比手写解析更不容易出错。


今日小挑战:假设你有一个天气 API,返回如下 JSON:

{
"city": "重庆",
"current": {"temp": 28.5, "humidity": 65, "weather": "多云"},
"forecast": [
{"date": "2026-11-06", "high": 30, "low": 22},
{"date": "2026-11-07", "high": 28, "low": 20}
]
}

请定义对应的数据类(提示:需要嵌套结构和集合),然后用 Json.decodeFromString 解析它。进阶:给 current 里的 weather 字段加一个 @SerialName("weather_desc"),看看解析时会发生什么——怎么修复?