公司动态
Moshi:Kotlin 原生 JSON 库的序列化与反序列化实战指南
1. 从 Gson 到 Moshi为什么我们需要一个“对 Kotlin 友好的” JSON 库如果你是一个 Android 开发者或者正在用 Kotlin 写后端服务处理 JSON 数据绝对是日常操作。过去几年甚至十几年Gson 和 Jackson 几乎是 Java 生态里处理 JSON 的默认选择。它们很强大也很成熟但当你把项目语言切换到 Kotlin 后事情就开始变得有点“别扭”了。我最早用 Gson 时觉得它真方便一个fromJson就能把字符串变成对象。但后来在 Kotlin 项目里我遇到了几个典型问题我的数据类Data Class明明定义了非空non-null属性但 Gson 反序列化时如果 JSON 里这个字段缺失了它不会报错而是默默地把这个属性设置成null。这直接破坏了 Kotlin 的空安全契约导致后续代码里可能出现意料之外的NullPointerException让 Kotlin 最大的卖点之一形同虚设。另一个问题是默认值Kotlin 允许你在主构造函数参数里直接给默认值比如val name: String “Unknown”但 Gson 在反序列化时如果字段缺失它并不会使用这个默认值而是会调用无参构造函数如果存在或者用其他方式初始化结果往往不是你想的那样。这就是“对 Kotlin 不友好”的典型表现。这些库在设计时核心假设是围绕 Java 的反射和空指针语义它们不理解、也不尊重 Kotlin 的语言特性比如空安全、默认参数、不可变数据类等。于是Moshi 出现了。它由 Square 公司出品对就是做 OkHttp 和 Retrofit 的那家公司。Moshi 的核心理念之一就是“一等公民式的 Kotlin 支持”。它从底层就为 Kotlin 设计能够理解 Kotlin 的类型系统。例如它会严格检查空安全如果 JSON 中缺失了一个在 Kotlin 中声明为非空的字段Moshi 会直接抛出JsonDataException而不是给你一个null。对于有默认值的参数如果 JSON 缺失它会聪明地使用你定义的默认值。这种设计哲学让 Moshi 和 Kotlin 的协作变得无比丝滑。所以当你的项目以 Kotlin 为主尤其是大量使用数据类和非空类型时Moshi 几乎是一个必然的选择。它解决的不仅仅是“能用”的问题更是“用得安心”、“符合语言习惯”的问题。接下来我们就深入看看怎么把这个好用的工具真正用起来。2. 环境搭建与基础依赖从引入到第一个序列化万事开头难但 Moshi 的开头相当简单。我们以 Android 项目为例如果你用的是 Gradle Kotlin DSL.kts文件在app模块的build.gradle.kts文件中添加依赖dependencies { // Moshi 核心库 implementation(com.squareup.moshi:moshi:1.15.0) // Moshi 的 Kotlin 代码生成支持强烈推荐 kapt(com.squareup.moshi:moshi-kotlin-codegen:1.15.0) // 或者如果你不想用 kapt也可以用反射方式性能稍差 // implementation(com.squareup.moshi:moshi-kotlin:1.15.0) }这里有个关键选择moshi-kotlin-codegen还是moshi-kotlin我强烈推荐使用 Codegen代码生成的方式。它的原理是在编译期kapt阶段为你的每个需要序列化的 Kotlin 类生成一个对应的JsonAdapter。这样做的好处是零运行时反射性能最好因为所有适配逻辑都是编译时生成的普通代码。Proguard/R8 友好不需要额外配置混淆规则来保留反射需要的类、方法名。编译期错误检查如果你的类无法被 Moshi 适配比如有不支持的属性类型会在编译时就报错而不是等到运行时才崩溃。而moshi-kotlin是纯反射实现它使用 Kotlin 反射在运行时去探查类的结构。虽然用起来更“自动”连JsonClass注解都可以省了但会有性能损耗并且需要配置混淆规则。对于新项目无脑选 Codegen 就对了。添加完依赖同步项目。现在我们来定义一个简单的数据类并用 Moshi 处理它。import com.squareup.moshi.JsonClass JsonClass(generateAdapter true) data class User( val id: Long, val name: String, val email: String? null, // 可空类型且有默认值 null val isActive: Boolean true // 非空类型有默认值 true )注意JsonClass(generateAdapter true)这个注解。这就是告诉 Moshi 的注解处理器“请为这个类生成一个JsonAdapter”。这是使用 Codegen 方式必须的一步。现在让我们写一段最简单的序列化对象转JSON字符串和反序列化JSON字符串转对象的代码import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory fun main() { // 1. 构建 Moshi 实例 val moshi Moshi.Builder() .addLast(KotlinJsonAdapterFactory()) // 添加 Kotlin 支持处理泛型、密封类等 .build() // 2. 获取 User 类的 JsonAdapter // 如果你用了 Codegen这里会使用生成的适配器而不是反射。 // 但为了通用性我们通过 moshi.adapterT() 来获取它会自动找到最优的适配器。 val userAdapter moshi.adapterUser() // 3. 创建一个 User 对象 val user User(id 1L, name “张三”) // email 使用默认值 null, isActive 使用默认值 true // 4. 序列化toJson val jsonString userAdapter.toJson(user) println(“序列化结果$jsonString”) // 输出类似{“id”:1,“name”:“张三”,“isActive”:true} // 注意email 字段因为值为 null默认被省略了。这是 Moshi 的默认行为。 // 5. 反序列化fromJson val inputJson “”“{“id”: 2, “name”: “李四”}”“” val parsedUser userAdapter.fromJson(inputJson) println(“反序列化结果$parsedUser”) // 输出User(id2, name李四, emailnull, isActivetrue) // 看到了吗JSON 里没有 email 和 isActive但反序列化后的对象使用了我们定义的默认值 }这段代码虽然简单但已经揭示了 Moshi 的几个核心优点空安全User.name是String非空如果 JSON 中name字段是null或者缺失fromJson会直接抛出异常而不是给你一个null的name。默认值生效email和isActive都成功使用了数据类中定义的默认值。简洁的 APIadapterT()、toJson()、fromJson()非常直观。注意KotlinJsonAdapterFactory的addLast方法很重要。Moshi 的适配器查找是链式的addLast意味着这个工厂会最后被查询。这样对于有自定义适配器或 Codegen 生成适配器的类会优先使用它们对于没有的比如ListUser这样的泛型类型才会 fallback 到这个 Kotlin 反射工厂来处理。这是推荐的配置方式。3. 核心功能实战处理字段映射、泛型与复杂结构基础用法会了但真实业务中的数据可没这么规整。API返回的JSON字段名可能不符合Kotlin的命名规范比如用蛇形命名user_name或者我们需要处理一些特殊类型如日期、枚举以及嵌套对象和集合。别担心Moshi 都有优雅的解决方案。3.1 自定义字段名与Json注解后端接口经常返回蛇形命名snake_case的字段而 Kotlin 属性通常用驼峰命名camelCase。Moshi 提供了Json注解来轻松映射。JsonClass(generateAdapter true) data class ApiResponse( Json(name “user_id”) // 将 JSON 中的 “user_id” 映射到 Kotlin 属性 userId val userId: Long, Json(name “created_at”) val createdAt: String, // 日期先按字符串处理后面我们再处理日期类型 val data: UserData // 嵌套对象 ) JsonClass(generateAdapter true) data class UserData( val items: ListString ) fun main() { val moshi Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() val adapter moshi.adapterApiResponse() val json “”“ { “user_id”: 12345, “created_at”: “2023-10-27T10:30:00Z”, “data”: { “items”: [“apple”, “banana”] } } ”“”.trimIndent() val response adapter.fromJson(json) println(response?.userId) // 输出12345 // 即使 JSON 是 user_id我们在代码里也能用 userId 访问非常干净。 }3.2 处理日期时间JSON 标准没有日期类型通常用 ISO 8601 格式的字符串表示。Moshi 本身不提供日期适配器但我们可以轻松集成java.timeAndroid API 26 或通过脱糖支持或kotlinx.datetime库。这里以集成java.time.Instant为例我们需要一个自定义的JsonAdapter。首先添加一个对moshi-adapters的依赖这个模块包含了一些常用类型的适配器但未必有你要的implementation(“com.squareup.moshi:moshi-adapters:1.15.0”)但为了展示自定义适配器我们手动为Instant写一个import com.squareup.moshi.* import java.time.Instant import java.time.format.DateTimeFormatter class InstantAdapter { ToJson fun toJson(instant: Instant): String { return DateTimeFormatter.ISO_INSTANT.format(instant) } FromJson fun fromJson(string: String): Instant { return Instant.from(DateTimeFormatter.ISO_INSTANT.parse(string)) } } // 使用它 JsonClass(generateAdapter true) data class Event( val name: String, Json(name “happened_at”) // 仍然可以用 Json 注解映射字段名 val happenedAt: Instant ) fun main() { val moshi Moshi.Builder() .add(InstantAdapter()) // 注册我们的自定义适配器 .addLast(KotlinJsonAdapterFactory()) .build() val eventAdapter moshi.adapterEvent() val event Event(“Product Launch”, Instant.now()) val json eventAdapter.toJson(event) println(json) // {“name”:“Product Launch”,“happened_at”:“2023-10-27T10:30:00Z”} val parsedEvent eventAdapter.fromJson(json) println(parsedEvent?.happenedAt) // 输出一个 Instant 对象 }ToJson和FromJson注解让创建自定义适配器变得非常简单。你只需要在一个类里定义这两个方法然后将这个类实例添加到Moshi.Builder中。Moshi 会自动识别方法的参数和返回类型将其绑定到对应的 Kotlin/Java 类型上。3.3 处理泛型集合与多态类型密封类这是 JSON 解析中两个稍微高级但非常常见的场景。泛型集合得益于KotlinJsonAdapterFactory或 CodegenMoshi 能很好地处理ListT、MapString, T这样的泛型集合。你只需要获取对应泛型类型的 Adapter 即可。val moshi Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() // 获取 ListUser 的适配器 val listAdapter: JsonAdapterListUser moshi.adapter() val usersJson “”“[{“id”:1,“name”:“A”}, {“id”:2,“name”:“B”}]”“” val userList listAdapter.fromJson(usersJson)多态类型密封类当 JSON 中某个字段可能是几种不同类型之一时比如一个消息通知的content字段可能是文本、图片或链接Kotlin 的密封类Sealed Class是完美的建模工具。Moshi 通过JsonAdapter工厂和JsonClass注解来支持。假设我们有一个消息内容JsonClass(generateAdapter true) sealed class MessageContent { JsonClass(generateAdapter true) data class Text(val text: String) : MessageContent() JsonClass(generateAdapter true) data class Image(val url: String, val caption: String?) : MessageContent() JsonClass(generateAdapter true) data class Link(val url: String, val title: String) : MessageContent() } JsonClass(generateAdapter true) data class Message( val id: Long, val type: String, // 我们可以用这个字段来判断类型 val content: MessageContent )现在问题来了Moshi 怎么知道一段 JSON 该对应Text、Image还是Link呢我们需要一个自定义的JsonAdapter根据某个判别字段这里是type来决策。class MessageContentAdapter { FromJson fun fromJson(reader: JsonReader, delegate: JsonAdapterMessageContent): MessageContent? { // 这里需要一些技巧。一种常见模式是先将 JSON 读成一个 Map 或 JsonElement判断后再解析。 // 更优雅的方式是使用 Moshi 的 PolymorphicJsonAdapterFactory在 moshi-adapters 中。 // 但为了理解原理我们手动实现一下。 // 注意手动实现较复杂实际项目建议用 PolymorphicJsonAdapterFactory。 // 这里仅展示概念。 return delegate.fromJson(reader) // 简单起见假设 delegate 能处理。实际需要更复杂的逻辑。 } ToJson fun toJson(writer: JsonWriter, value: MessageContent?, delegate: JsonAdapterMessageContent) { delegate.toJson(writer, value) } }实际上对于这种多态情况更推荐使用moshi-adapters模块中的PolymorphicJsonAdapterFactory它能极大简化这项工作import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory val messageContentAdapterFactory PolymorphicJsonAdapterFactory.of(MessageContent::class.java, “type”) .withSubtype(MessageContent.Text::class.java, “text”) .withSubtype(MessageContent.Image::class.java, “image”) .withSubtype(MessageContent.Link::class.java, “link”) val moshi Moshi.Builder() .add(messageContentAdapterFactory) .addLast(KotlinJsonAdapterFactory()) .build() // 现在 Moshi 就能自动根据 JSON 中的 “type” 字段的值“text”, “image”, “link”来实例化正确的子类了。4. 与 Retrofit 集成打造类型安全的网络层Moshi 和 Retrofit 同出自 Square它们的集成是天作之合能让你用最 Kotlin 的方式处理网络请求和响应。假设我们有一个简单的用户 APIimport retrofit2.http.GET import retrofit2.http.Path interface UserApiService { GET(“users/{id}”) suspend fun getUser(Path(“id”) id: Long): ApiResponseUser // 假设 ApiResponse 是一个包装类 GET(“users”) suspend fun getUsers(): ApiResponseListUser }在创建 Retrofit 实例时将 Moshi 的转换工厂提供给它import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory val moshi Moshi.Builder() .addLast(KotlinJsonAdapterFactory()) .build() val retrofit Retrofit.Builder() .baseUrl(“https://api.example.com/”) .addConverterFactory(MoshiConverterFactory.create(moshi)) // 关键在这里 .build() val userApi retrofit.create(UserApiService::class.java)就这样Retrofit 会自动使用 Moshi 来解析接口返回的 JSON 数据转换成你定义的 Kotlin 数据类。整个过程是类型安全的并且完美支持 Kotlin 的空安全和默认值。一个实战中的坑有时候 API 返回的 JSON 最外层是一个固定的包装结构比如{“code”: 0, “message”: “success”, “data”: {...}}。我们不想在每个 API 接口的返回类型里都写ApiResponseX更希望直接拿到data里的内容并且能统一处理code非 0 的错误情况。这需要用到 Retrofit 的CallAdapter。我们可以自定义一个或者使用一些现成的库如retrofit2.adapter.result的Result类型。核心思想是在 Moshi 解析完 JSON 到ApiResponseT后由CallAdapter检查code如果成功则提取data并返回如果失败则抛出包含message的异常。这属于网络层架构设计篇幅所限不展开但知道 Moshi 能无缝融入这个流程很重要。5. 性能调优与问题排查让 Moshi 飞得更稳选择 Codegen 已经是最大的性能优化。但还有一些细节值得注意。1. 重用 Moshi 实例和 AdapterMoshi 实例和 JsonAdapter 实例都是线程安全的且构建成本较高。绝对不要在每次序列化/反序列化时都新建一个。应该在应用生命周期内如通过依赖注入、单例或全局变量创建并重用它们。// 不好的做法 fun parseUserBad(json: String): User? { val moshi Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() // 每次新建 return moshi.adapterUser().fromJson(json) } // 好的做法 object MoshiHelper { val moshi: Moshi by lazy { Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() } inline fun reified T adapter(): JsonAdapterT moshi.adapter() } fun parseUserGood(json: String): User? { return MoshiHelper.adapterUser().fromJson(json) // 重用 Adapter }2. 注意 Proguard/R8 混淆如果你使用了 Codegenmoshi-kotlin-codegen通常不需要额外的混淆规则因为生成的代码是普通的类和方法。但如果你使用了moshi-kotlin反射方式或者集成了某些使用反射的自定义适配器则需要在proguard-rules.pro中添加规则来保留相关的类名、方法名。对于 Codegen基本可以忽略这一步。3. 常见异常与排查JsonDataException: Required value ‘name’ missing at $这通常是因为 JSON 中缺失了 Kotlin 类中声明为非空且无默认值的字段。检查你的数据模型是否和 API 契约完全匹配。有时后端可能在某些情况下不返回某个字段这时你需要将其在 Kotlin 中改为可空类型String?或提供合理的默认值。JsonDataException: Expected a string but was NUMBER at path $.id类型不匹配。例如JSON 中id是数字但你的 Kotlin 属性是String。仔细核对字段类型。使用 Codegen 时编译报错Moshi doesn‘t know how to handle...这可能是因为你的类中有 Moshi 无法自动处理的自定义类型。你需要为这个类型提供一个自定义的JsonAdapter并通过JsonClass(generateAdapter false)告诉 Moshi 不要为这个类生成适配器或者将你的自定义适配器添加到 Moshi 实例中。反序列化后属性全是默认值null 或 false这很可能是因为字段名映射错误。检查Json(name “...” )注解是否正确或者属性名是否与 JSON 键名完全匹配注意大小写。可以用moshi.adapterYourClass().indent(“ “).toJson(yourObj)打印出 Moshi 期望的 JSON 格式与实际的 API 响应对比。4. 使用JsonReader和JsonWriter进行低级控制绝大多数情况下你不需要直接操作JsonReader和JsonWriter。但如果你需要处理非常不规则、动态的 JSON或者要实现极其定制化的解析逻辑比如流式解析巨大的 JSON 文件它们就派上用场了。你可以通过自定义JsonAdapter并重写fromJson(reader: JsonReader)和toJson(writer: JsonWriter, value: T?)方法来获得完全的控制权。这属于高级用法在需要性能极限优化或处理特殊格式时再考虑。6. 进阶技巧适配器组合、懒加载与 Kotlin 特性深挖当你对 Moshi 的基础用法驾轻就熟后下面这些技巧能让你的代码更加健壮和优雅。1. 适配器组合Adapter Composition这是 Moshi 一个非常强大的特性。假设你有一个Settings类里面包含一个MapString, Any用来存储动态配置。Any类型对 Moshi 来说太模糊了。你可以创建一个组合适配器根据 Map 里值的实际类型来动态处理。class SettingsMapAdapter { private val stringAdapter Moshi.Builder().build().adapterString() private val intAdapter Moshi.Builder().build().adapterInt() private val boolAdapter Moshi.Builder().build().adapterBoolean() FromJson fun fromJson(reader: JsonReader): MapString, Any { // 手动读取 JSON Object根据值的类型调用不同的 adapter val map mutableMapOfString, Any() reader.beginObject() while (reader.hasNext()) { val key reader.nextName() when (reader.peek()) { JsonReader.Token.STRING - map[key] stringAdapter.fromJson(reader)!! JsonReader.Token.NUMBER - map[key] intAdapter.fromJson(reader)!! JsonReader.Token.BOOLEAN - map[key] boolAdapter.fromJson(reader)!! else - reader.skipValue() // 忽略不支持的类型 } } reader.endObject() return map } ToJson fun toJson(writer: JsonWriter, value: MapString, Any?) { // 类似地根据值的类型调用不同的 adapter 写入 writer.beginObject() value?.forEach { (key, anyValue) - writer.name(key) when (anyValue) { is String - stringAdapter.toJson(writer, anyValue) is Int - intAdapter.toJson(writer, anyValue) is Boolean - boolAdapter.toJson(writer, anyValue) else - writer.nullValue() // 或抛出异常 } } writer.endObject() } }然后把这个适配器注册给MapString, Any类型。这展示了如何将简单的适配器组合起来处理复杂场景。2. 懒加载Lazy属性的处理Kotlin 的by lazy是一种常见的属性委托用于延迟初始化。但 Moshi 在反序列化时会直接调用主构造函数来创建对象by lazy的初始化块不会被执行因为属性可能已经被构造函数参数赋值了。如果你有一个属性其值需要从其他属性计算得来并且希望它被序列化/反序列化更好的模式是不序列化计算属性将其声明为普通函数fun calculatedValue(): String或不在构造函数中的val这样它就不会是数据类的一部分默认不会被 Moshi 序列化。需要序列化将其包含在主构造函数中即使它可以从其他字段推导。或者使用自定义适配器在序列化时计算它反序列化时忽略或从 JSON 中读取。通常计算属性不应该参与序列化/反序列化因为它不是“状态”而是状态的衍生品。3. 使用Transient注解忽略字段如果你有一个属性不需要参与序列化和反序列化比如内存缓存、临时状态可以用Transient注解标记它。Moshi 会完全忽略这个字段。JsonClass(generateAdapter true) data class User( val id: Long, val name: String, Transient // 这个字段不会被序列化或反序列化 var cachedAvatar: Bitmap? null )4. 处理默认值策略默认情况下Moshi 在序列化时会跳过值为null的字段toJson时。如果你希望显式输出null可以在构建JsonAdapter时配置val adapter moshi.adapterUser().serializeNulls() val jsonWithNull adapter.toJson(User(1, “Bob”, null)) // 输出包含 “email”: null同样反序列化时如果 JSON 中有null对于非空类型会抛异常对于可空类型会赋值为null。这个行为是符合 Kotlin 空安全预期的。7. 测试策略确保你的 JSON 解析坚如磐石对于核心的数据模型和 JSON 适配逻辑编写测试至关重要。Moshi 的纯函数特性输入 JSON输出对象或反之让它非常易于测试。1. 单元测试数据类解析使用 JUnit 和 Moshi你可以轻松测试各种边界情况。import com.google.common.truth.Truth.assertThat import org.junit.Test class UserParsingTest { private val moshi Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() private val adapter moshi.adapterUser() Test fun parse complete user json() { val json “”“{“id”: 1, “name”: “Alice”, “email”: “aliceexample.com”}”“” val user adapter.fromJson(json) assertThat(user).isEqualTo(User(id 1L, name “Alice”, email “aliceexample.com”)) } Test fun parse user json without email uses default null() { val json “”“{“id”: 1, “name”: “Alice”}”“” val user adapter.fromJson(json) assertThat(user).isEqualTo(User(id 1L, name “Alice”, email null)) // email 是默认的 null } Test(expected JsonDataException::class) // 期望抛出异常 fun parse user json without name should fail() { val json “”“{“id”: 1}”“” // 缺少非空字段 name adapter.fromJson(json) } Test fun serialize user with null email omits field() { val user User(id 1L, name “Alice”, email null) val json adapter.toJson(user) // 检查 json 不包含 email 字段 assertThat(json).doesNotContain(“email”) } }2. 测试自定义适配器为你写的每一个自定义JsonAdapter比如上面的InstantAdapter编写测试覆盖正常的序列化/反序列化以及异常输入如格式错误的日期字符串。3. 集成测试与 Retrofit使用 MockWebServerOkHttp 的测试组件可以模拟网络响应并测试从 HTTP 请求到最终 Kotlin 对象的完整链条。这能确保你的 API 接口定义、Moshi 转换器和数据模型协同工作正常。import okhttp3.mockwebserver.MockWebServer import org.junit.After import org.junit.Before import org.junit.Test import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory class UserApiTest { private lateinit var server: MockWebServer private lateinit var api: UserApiService Before fun setup() { server MockWebServer() server.start() val moshi Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() val retrofit Retrofit.Builder() .baseUrl(server.url(“/”)) .addConverterFactory(MoshiConverterFactory.create(moshi)) .build() api retrofit.create(UserApiService::class.java) } After fun teardown() { server.shutdown() } Test fun getUser returns parsed object() runBlocking { // 1. 准备模拟响应 val mockResponse “”“ { “code”: 0, “message”: “success”, “data”: { “id”: 100, “name”: “Test User” } } ”“”.trimIndent() server.enqueue(MockResponse().setBody(mockResponse).setResponseCode(200)) // 2. 执行调用 val response api.getUser(100) // 3. 验证结果 assertThat(response.code).isEqualTo(0) assertThat(response.data?.id).isEqualTo(100) assertThat(response.data?.name).isEqualTo(“Test User”) } }通过分层测试你可以自信地重构数据类或适配器逻辑而不用担心破坏现有的 JSON 解析功能。从最初的依赖引入到基础的对象映射再到处理复杂的多态类型、与网络库集成最后到性能优化和完备的测试Moshi 提供了一套完整、高效且“Kotlin 原生”的 JSON 处理方案。它强迫你思考数据模型的可空性利用 Kotlin 的语言特性写出更安全的代码。迁移到 Moshi 可能需要一些前期投入尤其是修改旧的数据类但带来的类型安全性和开发体验的提升绝对是值得的。下次启动新的 Kotlin 项目时不妨直接选择 Moshi 作为你的 JSON 库体验一下这种“友好”的感觉。