1. 简介
Moshi 是由 Square 开发的一款轻量级 JSON 解析库,底层基于 Okio,设计理念上继承自 Gson。相比 Gson,它性能更优;相比 Jackson,它的依赖体积更小,这对 Android 或嵌入式应用尤为重要。
我们之前已经讨论过 Moshi 的通用用法。但考虑到它是专为 Kotlin 开发者打造、主要使用 Kotlin 编写的库,本文将聚焦其在 Kotlin 环境下的独特优势和最佳实践。
2. Moshi 基础配置
要在 Kotlin 项目中使用 Moshi,需引入以下 Maven 依赖:
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>1.14.0</version>
</dependency>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-adapters</artifactId>
<version>1.14.0</version>
</dependency>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-kotlin</artifactId>
<version>1.14.0</version>
<!-- 若仅使用 codegen 可省略 -->
</dependency>
⚠️ 如果你计划使用 Moshi Codegen 自动生成 Type Adapter(提升性能),还需要在 kotlin-maven-plugin
中配置 kapt 注解处理器,在编译期生成代码:
<!-- 在 kotlin-maven-plugin 的 executions 配置块中 -->
<execution>
<id>kapt</id>
<goals>
<goal>kapt</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/main/kotlin</sourceDir>
</sourceDirs>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-kotlin-codegen</artifactId>
<version>1.14.0</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</execution>
2.1 基本序列化与反序列化
假设我们需要处理一个典型的业务场景:接收 JSON 输入,进行转换后返回新的 JSON 结构。定义如下数据类:
data class Department(
val name: String,
val code: UUID,
val employees: List<Employee>
)
data class Employee(
val firstName: String,
val lastName: String,
val title: String,
val age: Int,
val salary: BigDecimal
)
data class SalaryRecord(
val employeeFirstName: String,
val employeeLastName: String,
val departmentCode: UUID,
val departmentName: String,
val sum: BigDecimal,
val taxPercentage: BigDecimal
)
Moshi 的工作方式类似 Gson,不同于 Jackson。它通过反射机制处理 Kotlin 数据类(Data Class)的序列化/反序列化。但关键点是:对于 UUID、BigDecimal 等平台类型,必须手动提供 Type Adapter。
Moshi 默认内置适配器较少,这是为了:
- ✅ 避免绑定特定 JDK 版本
- ✅ 控制库体积小巧
默认支持的类型包括:
- 基本类型及其包装类
- 标准集合(List、Map、Array 等)
- String 类型
因此,我们需要为 UUID
和 BigDecimal
自定义 Adapter:
class UuidAdapter : JsonAdapter<UUID>() {
@FromJson
override fun fromJson(reader: JsonReader): UUID? = UUID.fromString(reader.readJsonValue().toString())
@ToJson
override fun toJson(writer: JsonWriter, value: UUID?) {
writer.jsonValue(value.toString())
}
}
然后注册到 Moshi 实例:
val moshi = Moshi.Builder()
.add(UuidAdapter())
.add(BigDecimalAdapter()) // 其他自定义 Adapter
.addLast(KotlinJsonAdapterFactory()) // 必须放在最后,兜底处理 Kotlin 类型
.build()
准备工作完成,即可进行序列化操作:
val adapter = moshi.adapter<Department>()
val department = adapter.fromJson(resource("sales_department.json")!!.source().buffer())
val salaryRecordJsonAdapter = moshi.adapter<SalaryRecord>()
val serialized: String = salaryRecordJsonAdapter.toJson(record)
📌 注意:当前版本中,部分 Kotlin 特性可能需要添加 @ExperimentalStdlibApi
注解。
2.2 运行时反射 vs 编译期生成 Adapter
Moshi 支持两种 Adapter 生成方式:
方式 | 优点 | 缺点 |
---|---|---|
反射 (moshi-kotlin) | ✅ 支持 private /protected 字段✅ 支持默认值和非空类型推断 |
❌ 依赖 ~2.5MB ❌ 性能稍慢 |
编译期生成 (moshi-kotlin-codegen) | ✅ 更小体积 ✅ 更快运行速度 |
❌ 仅支持 public /internal 字段 |
若选择 codegen 方式,需配合 KAPT 使用,并为需要生成的类添加注解:
@JsonClass(generateAdapter = true)
data class Department( /* 属性声明 */ )
编译后会在 target/generated-sources
目录下生成 DepartmentJsonAdapter.class
文件。
✅ 推荐策略:
- Android 项目 → 优先使用 codegen,减小 APK 体积
- 后端服务 → 可根据需求混合使用,灵活控制
2.3 泛型类型的解析
处理 JSON 数组是常见需求。Java 中需通过 TypeToken
构造泛型类型,较为繁琐。而 Kotlin 利用 reified generics 可轻松解决:
val employeeListAdapter = moshi.adapter<List<Employee>>()
后续使用方式一致:
val list = employeeListAdapter.fromJson(inputStream.source().buffer())
简洁明了,无需额外样板代码。
2.4 与 Okio 协同工作
Moshi 依赖 Okio 处理 I/O 操作(也是 OkHttp 的底层依赖)。这意味着不能直接传入 InputStream
或 File
,必须先包装成 BufferedSource
或 JsonReader
:
val bufferedSource = inputStream.source().buffer()
val reader = JsonReader.of(bufferedSource)
这种设计虽然多了一步,但带来了更好的内存控制能力,尤其适合处理大文件流。
3. 使用注解定制 Moshi 行为
Moshi 设计目标是高效与低内存占用,因此不像 Jackson 那样提供大量配置选项(如全局命名策略)。但它仍支持通过注解灵活控制序列化行为。
3.1 JSON 字段名映射
当 Kotlin 属性名(camelCase)与 JSON 字段名(snake_case)不一致时,可通过 @Json
注解指定别名:
data class SnakeProject(
@Json(name = "project_name")
val snakeProjectName: String,
@Json(name = "responsible_person_name")
val snakeResponsiblePersonName: String,
@Json(name = "project_budget")
val snakeProjectBudget: BigDecimal
)
序列化结果将使用注解中定义的名称:
{
"project_name": "Mayhem",
"responsible_person_name": "Tailor Burden",
"project_budget": "100000000"
}
✅ 小技巧:如果整个项目都使用 snake_case,也可以考虑结合 Moshi + Kotlin 扩展函数实现统一转换逻辑,避免重复注解。
3.2 忽略字段
某些字段无需参与序列化(如敏感信息、临时状态),可用 @Json(ignore = true)
标记:
data class SnakeProject(
// ... 其他字段
@Json(ignore = true)
val snakeProjectSecret: String = "No secret"
)
⚠️ 踩坑提醒:若使用 codegen 模式,被忽略的字段必须提供默认值,否则反序列化时无法构造对象实例。
3.3 基于自定义注解选择 Adapter
同一类型在不同上下文中可能需要不同的序列化格式。典型例子是颜色值:JVM 中是 Int
,但 JSON 中希望表示为 #RRGGBB
格式的字符串。
Moshi 支持通过自定义注解绑定特定 Adapter:
- 定义注解并标记为
@JsonQualifier
:
@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class Hexadecimal
- 创建对应 Adapter 并注册:
class HexadecimalAdapter {
@ToJson
fun toJson(@Hexadecimal color: Int): String = "#%06x".format(color)
@FromJson
@Hexadecimal
fun fromJson(color: String): Int = color.substring(1).toInt(16)
}
- 在模型中使用注解:
data class Palette(
@Hexadecimal val mainColor: Int,
@Hexadecimal val backgroundColor: Int,
val frameFrequency: Int
)
注册 Adapter 后即可正确序列化:
val moshi = Moshi.Builder()
.add(HexadecimalAdapter())
.add(KotlinJsonAdapterFactory())
.build()
val paletteAdapter = moshi.adapter<Palette>()
val result = paletteAdapter.toJson(palette)
// 输出: {"mainColor":"#532b12","backgroundColor":"#00a012","frameFrequency":25}
✅ 这种模式非常适合处理时间戳(Long ↔ ISO8601)、枚举(String ↔ Int)等场景。
4. 使用 Moshi 流式解析 JSON 数组
当面对大型 JSON 文件(如日志、批量数据导入)时,一次性加载到内存可能导致 OOM。此时应采用 流式解析(Streaming Parsing)。
得益于底层 Okio 的支持,Moshi 可以将输入源包装为 JsonReader
,逐个读取 token,极大降低内存占用。
我们可以封装一个辅助函数,按需读取数组元素:
inline fun JsonReader.readArray(body: JsonReader.() -> Unit) {
beginArray()
while (hasNext()) {
body()
}
endArray()
}
结合 Kotlin 协程与 Flow
,实现非阻塞、背压友好的流式处理:
suspend inline fun <reified T> readToFlow(input: InputStream, adapter: JsonAdapter<T>): Flow<T> = flow {
JsonReader.of(input.source().buffer())
.readArray {
emit(adapter.fromJson(this)!!)
}
}
这样就可以安全地处理海量数据,例如计算员工总薪资:
val totalSalary = runBlocking {
readToFlow(inputStream, employeeAdapter)
.fold(BigDecimal.ZERO) { acc, value -> acc + value.salary }
}
✅ 优势:
- 内存恒定,不随文件大小增长
- 支持早期中断(如找到目标记录即停止)
- 与协程天然集成,适合异步处理流水线
5. 总结
Moshi 是一款极简却强大的 JSON 库,特别适合 Kotlin 项目。虽然初期需要为平台类型编写 Adapter 略显繁琐,但换来的是:
- ⚡ 比 Gson 更快的性能
- 📦 比 Jackson 更小的体积
- 🔧 对 Kotlin 特性的原生支持(非空、默认值、data class)
- 🌊 流式处理能力应对大数据场景
本文涵盖了:
- Maven 环境下的完整配置(Gradle 更简单)
- 反射与 codegen 两种模式的选择建议
- 注解驱动的高级定制能力
- 大文件流式解析实战方案
所有示例代码均可在 GitHub 获取:https://github.com/Baeldung/kotlin-tutorials/tree/master/kotlin-json