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 类型

因此,我们需要为 UUIDBigDecimal 自定义 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 的底层依赖)。这意味着不能直接传入 InputStreamFile,必须先包装成 BufferedSourceJsonReader

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:

  1. 定义注解并标记为 @JsonQualifier
@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class Hexadecimal
  1. 创建对应 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)
}
  1. 在模型中使用注解:
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


原始标题:Moshi JSON Library for Kotlin Applications