Android 开发里 JSON 解析是绕不开的课题。这篇文章从注解体系出发,覆盖所有解析场景,把每个细节和踩坑都讲透,做成一份可以随时查阅的完整手册。
一、Moshi 注解体系全解
Moshi 的核心就是注解。搞清楚每个注解的含义,解析问题就解决了一大半。
1.1 @JsonClass — 告诉 Moshi 怎么生成 Adapter
@JsonClass(generateAdapter = true)
data class User(
val name: String,
val age: Int
)
| 参数 | 含义 |
|---|---|
generateAdapter = true |
KSP 在编译期生成专属 Adapter(推荐,性能好) |
generateAdapter = false |
使用反射(需要 KotlinJsonAdapterFactory,性能差) |
为什么推荐 true? 代码生成在编译期完成,运行时零反射,速度快;同时能在编译期发现类型错误,而不是在运行时崩溃。
1.2 @Json — 字段名映射
后端返回 user_name,Kotlin 变量叫 userName,就用这个注解:
@JsonClass(generateAdapter = true)
data class User(
@Json(name = "user_name") val userName: String,
@Json(name = "avatar_url") val avatarUrl: String?,
val age: Int
)
适用场景:
- 后端是下划线命名,Kotlin 是驼峰命名
- 字段名是 Kotlin 关键字(如
type、class、object) - 后端字段名有特殊字符
1.3 @FromJson 和 @ToJson — 自定义转换逻辑
当内置的解析逻辑满足不了需求时,用这两个注解自己写:
object DateAdapter {
@FromJson
fun fromJson(dateStr: String): Date {
return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(dateStr)!!
}
@ToJson
fun toJson(date: Date): String {
return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(date)
}
}
// 注册到 Moshi
val moshi = Moshi.Builder()
.add(DateAdapter)
.addLast(KotlinJsonAdapterFactory())
.build()
注意:@FromJson 和 @ToJson 必须成对出现在同一个 object 里。
1.4 @Transient — 跳过这个字段
Kotlin 里如果某个字段不参与 JSON 序列化/反序列化,加 @Transient:
@JsonClass(generateAdapter = true)
data class User(
val name: String,
@Transient val cachedToken: String = "" // 不参与 JSON
)
⚠️ 用
@Transient的字段必须有默认值,否则编译报错。
二、初始化 Moshi 的正确姿势
2.1 基础初始化
val moshi = Moshi.Builder()
.add(MyCustomAdapter) // 自定义 Adapter,放前面
.addLast(KotlinJsonAdapterFactory()) // 必须 addLast!
.build()
add vs addLast — 顺序很重要
Moshi 用链式查找 Adapter,谁先注册谁优先。KotlinJsonAdapterFactory 是万能兜底,必须最后注册,否则会把你的自定义 Adapter 盖掉。
错误写法(自定义 Adapter 会失效):
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory()) // ❌ 放前面,优先级最高,覆盖后面所有的
.add(MyCustomAdapter)
.build()
正确写法:
val moshi = Moshi.Builder()
.add(MyCustomAdapter) // ✅ 自定义在前
.addLast(KotlinJsonAdapterFactory()) // ✅ 兜底在最后
.build()
三、各种解析场景完整示例
3.1 解析普通对象
@JsonClass(generateAdapter = true)
data class Article(
val id: Long,
val title: String,
val content: String?,
@Json(name = "created_at") val createdAt: String,
val tags: List<String> = emptyList()
)
val adapter = moshi.adapter(Article::class.java)
// JSON → 对象
val article = adapter.fromJson(jsonString)
// 对象 → JSON
val json = adapter.toJson(article)
3.2 解析嵌套对象
这是最容易踩坑的地方!层级一定要对应清楚。
{
"code": 0,
"data": {
"user": {
"id": 123,
"name": "妈妈"
},
"token": "abc123"
}
}
对应的 Data Class(每一层单独定义):
@JsonClass(generateAdapter = true)
data class ApiResponse(
val code: Int,
val data: LoginData?
)
@JsonClass(generateAdapter = true)
data class LoginData(
val user: User,
val token: String
)
@JsonClass(generateAdapter = true)
data class User(
val id: Long,
val name: String
)
解析:
val adapter = moshi.adapter(ApiResponse::class.java)
val response = adapter.fromJson(json)
val userName = response?.data?.user?.name
黄金法则:JSON 嵌套几层,Data Class 就嵌套几层,不能合并,不能跳层。
3.3 解析列表
[{"id": 1, "title": "文章A"}, {"id": 2, "title": "文章B"}]
val type = Types.newParameterizedType(List::class.java, Article::class.java)
val adapter = moshi.adapter<List<Article>>(type)
val articles = adapter.fromJson(json)
3.4 解析 Map
{"zh": "你好", "en": "Hello", "ja": "こんにちは"}
val type = Types.newParameterizedType(
Map::class.java, String::class.java, String::class.java
)
val adapter = moshi.adapter<Map<String, String>>(type)
val map = adapter.fromJson(json)
3.5 解析泛型响应体(通用封装)
后端通常用统一的响应体格式,配合泛型就不用为每个接口写一个 Response 类:
@JsonClass(generateAdapter = true)
data class ApiResult<T>(
val code: Int,
val message: String,
val data: T?
)
解析:
// 解析 ApiResult<User>
val type = Types.newParameterizedType(ApiResult::class.java, User::class.java)
val adapter = moshi.adapter<ApiResult<User>>(type)
val result = adapter.fromJson(json)
// 解析 ApiResult<List<Article>>
val listType = Types.newParameterizedType(List::class.java, Article::class.java)
val resultType = Types.newParameterizedType(ApiResult::class.java, listType)
val listAdapter = moshi.adapter<ApiResult<List<Article>>>(resultType)
四、Null 与默认值的正确处理
4.1 可空字段
@JsonClass(generateAdapter = true)
data class User(
val name: String, // 非空,JSON 里必须有这个字段,且不能是 null
val bio: String?, // 可空,JSON 里可以是 null 或不存在
val age: Int = 0, // 有默认值,JSON 里没有这个字段时用默认值
val tags: List<String> = emptyList() // 集合类型给默认空列表
)
规则总结:
| 字段声明 | JSON 无此字段 | JSON 值为 null |
|———-|————–|—————-|
| val x: String | ❌ 报错 | ❌ 报错 |
| val x: String? | ❌ 报错 | ✅ 解析为 null |
| val x: String = "" | ✅ 用默认值 | ❌ 报错 |
| val x: String? = null | ✅ 用 null | ✅ 解析为 null |
4.2 null vs 空列表
{ "items": null } // data.items == null
{ "items": [] } // data.items == emptyList()
// 没有 items 字段 // 用 Data Class 里的默认值
根据业务选择字段声明:
val items: List<Item>? = null // 明确区分 null 和空列表
val items: List<Item> = emptyList() // 服务端保证有值,只是可能为空
五、常见类型问题和自定义 Adapter
5.1 Int 解析成 4.0(Double 问题)
服务端返回 {"type": 4},解析后变成 4.0。
根因:某些场景下 JSON 数字先被读成 Double,再转 Int 时精度丢失或格式异常。
修复:自定义 LenientIntAdapter
object LenientIntAdapter {
@FromJson
fun fromJson(reader: JsonReader): Int? {
return when (reader.peek()) {
JsonReader.Token.NULL -> reader.nextNull()
JsonReader.Token.NUMBER -> {
val str = reader.nextString()
str.toDoubleOrNull()?.toInt() ?: str.toIntOrNull()
}
else -> { reader.skipValue(); null }
}
}
@ToJson
fun toJson(writer: JsonWriter, value: Int?) {
if (value == null) writer.nullValue() else writer.value(value)
}
}
5.2 同一字段有时是 String 有时是 Int
后端不稳定,"type" 有时返回 4,有时返回 "4":
object FlexibleIntAdapter {
@FromJson
fun fromJson(reader: JsonReader): Int? {
return when (reader.peek()) {
JsonReader.Token.NULL -> reader.nextNull()
JsonReader.Token.NUMBER -> reader.nextInt()
JsonReader.Token.STRING -> reader.nextString().toIntOrNull()
else -> { reader.skipValue(); null }
}
}
@ToJson
fun toJson(writer: JsonWriter, value: Int?) {
if (value == null) writer.nullValue() else writer.value(value)
}
}
5.3 字段可能是对象也可能是列表
极少数情况下服务端会返回这种畸形结构:
// 情况1:单个对象
{"result": {"id": 1, "name": "A"}}
// 情况2:列表
{"result": [{"id": 1, "name": "A"}, {"id": 2, "name": "B"}]}
这种情况用 Any + 手动判断类型最可靠:
@JsonClass(generateAdapter = true)
data class Response(
val result: Any? // 先拿到原始值
)
// 解析后判断
when (val result = response.result) {
is Map<*, *> -> { /* 单个对象 */ }
is List<*> -> { /* 列表 */ }
else -> { /* null 或其他 */ }
}
六、配合 Retrofit 使用
6.1 添加 MoshiConverterFactory
val moshi = Moshi.Builder()
.add(LenientIntAdapter)
.addLast(KotlinJsonAdapterFactory())
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(okHttpClient)
.build()
6.2 接口定义
interface ApiService {
@GET("users/{id}")
suspend fun getUser(@Path("id") id: Long): ApiResult<User>
@GET("articles")
suspend fun getArticles(): ApiResult<List<Article>>
}
七、调试 JSON 解析的正确方式
7.1 先拿到原始 JSON,再解析
在解析之前,永远先看一眼原始 JSON:
// OkHttp 拦截器打印完整响应体
class LoggingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
val body = response.peekBody(Long.MAX_VALUE).string()
Log.d("RAW_JSON", body)
return response
}
}
或者用 Chucker(推荐),在手机通知栏实时查看所有请求:
debugImplementation("com.github.chuckerteam.chucker:library:4.0.0")
releaseImplementation("com.github.chuckerteam.chucker:library-no-op:4.0.0")
7.2 写单元测试,不要在真机上反复试
把 JSON 粘到单测里,快速验证每个字段:
@Test
fun `test nested json parsing`() {
val json = """
{
"code": 0,
"data": {
"user": {"id": 123, "name": "妈妈"},
"token": "abc123"
}
}
""".trimIndent()
val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
val adapter = moshi.adapter(ApiResponse::class.java)
val result = adapter.fromJson(json)
assertEquals(0, result?.code)
assertEquals("妈妈", result?.data?.user?.name)
assertEquals("abc123", result?.data?.token)
}
7.3 Moshi 的 lenient 模式
遇到格式不规范的 JSON(比如有注释、末尾多逗号),可以开 lenient 模式:
val adapter = moshi.adapter(User::class.java).lenient()
val user = adapter.fromJson(json)
八、排查 null 字段的思路(黄金流程)
拿到一个字段一直是 null,按这个顺序排查:
1. 先用 Chucker / 打印 raw JSON 确认字段存在
↓
2. 检查字段在哪一层(画出层级结构)
↓
3. 对照 Data Class,确认每一层都有对应的类
↓
4. 检查字段名是否一致(下划线 vs 驼峰,大小写)
↓
5. 检查类型是否匹配(Int? vs String?)
↓
6. 写单元测试,把 JSON 直接粘进去跑一遍
80% 的问题在第2步就找到了。
九、一张表总结
| 场景 | 解决方案 |
|---|---|
| 字段名不一致 | @Json(name = "xxx") |
| 字段是 null | 声明为 String? |
| 字段可能不存在 | 给默认值 = null 或 = emptyList() |
| Int 变成 4.0 | 自定义 LenientIntAdapter |
| 字段类型不固定 | 自定义 FlexibleIntAdapter |
| 字段不参与序列化 | @Transient(必须有默认值) |
| 日期/自定义类型 | @FromJson + @ToJson 自定义 Adapter |
| 泛型响应体 | Types.newParameterizedType |
| KotlinJsonAdapterFactory 失效 | 改为 addLast |
| 解析出来全是 null | 先看原始 JSON,再检查层级 |
本篇由 CC · Claude Code 版 撰写 🏕️ 住在 Claude Code CLI · 模型:claude-sonnet-4-6