1. 简介

Retrofit 是一个类型安全的 HTTP 客户端,广泛用于 Java 和 Kotlin 开发中,其核心思想是将 REST API 抽象为接口。在实际开发中,我们常常需要将枚举(Enum)序列化为对应的字符串值,以确保应用与后端 API 之间能够正确地通信。

在本教程中,我们将探讨如何在 Kotlin 中使用 Retrofit 实现枚举的序列化和反序列化。

2. 创建枚举类

首先,我们定义一个表示订单状态的简单枚举:

enum class OrderStatus { 
    PENDING,
    COMPLETED,
    CANCELLED
}

接着,我们创建一个依赖于 OrderStatus 的数据类 Order

data class Order(val id: String, val status: OrderStatus)

最后,定义一个用于处理请求的接口 OrderService

interface OrderService {
    @GET("orders/{id}")
    suspend fun getOrder(@Path("id") orderId: String): Order

    @POST("orders")
    suspend fun createOrder(@Body order: Order): Order
}

3. Retrofit 的转换器机制

Retrofit 通过 Converter 接口实现数据的序列化和反序列化。它负责将原始的 JSON 或 XML 数据转换为 Kotlin 对象,反之亦然。Retrofit 支持多种转换器库,例如 Gson、Moshi 和 Jackson。

这意味着我们可以根据项目需求灵活选择 JSON 序列化库,只需在构建 Retrofit 实例时添加对应的 ConverterFactory 即可。

3.1. 使用 Gson 作为转换器

Gson 是 Google 提供的流行 JSON 序列化库,Retrofit 提供了 GsonConverterFactory 用于集成:

val gson: Gson = GsonBuilder().create()

val gsonRetrofit: Retrofit = Retrofit.Builder()
  .baseUrl("http://localhost:8080")
  .addConverterFactory(GsonConverterFactory.create(gson))
  .build()

3.2. 使用 Moshi 作为转换器

Moshi 是由 Square 开发的轻量级 JSON 解析库,Retrofit 也提供了官方支持:

val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()

val moshiRetrofit: Retrofit = Retrofit.Builder()
  .baseUrl("http://localhost:8080")
  .addConverterFactory(MoshiConverterFactory.create(moshi))
  .build()

3.3. 使用 Jackson 作为转换器

Jackson 是功能强大的 JSON 处理库,适合需要复杂序列化配置的场景:

val objectMapper: ObjectMapper = jacksonObjectMapper()

val jacksonRetrofit: Retrofit = Retrofit.Builder()
  .baseUrl("http://localhost:8080")
  .addConverterFactory(JacksonConverterFactory.create(objectMapper))
  .build()

4. 枚举的序列化处理

上述三种转换器都对枚举有默认支持。默认情况下,它们会使用枚举的 name() 方法将其转换为字符串形式,例如 OrderStatus.PENDING 会被序列化为 "PENDING"

我们可以使用 WireMock 来模拟 HTTP 请求,验证 Retrofit 是否能正确地序列化和反序列化包含枚举的数据对象。

4.1. WireMock 初始化

在 JUnit 测试中使用 WireMock 模拟服务器行为:

class WireMockTest {    
    @Rule
    @JvmField
    val wireMockRule = WireMockRule(8080)

    @BeforeEach
    fun setup() {
        if(wireMockRule.isRunning) return
        wireMockRule.start()
    }

    @AfterEach
    fun tearDown() {
        wireMockRule.stop()
    }
}

4.2. Gson 序列化测试

测试 Gson 默认的枚举序列化行为:

@Test
fun `test Gson default serialization`() = runBlocking {
    val service = gsonRetrofit.create(OrderService::class.java)
    val order = Order("1", OrderStatus.PENDING)
    wireMockRule.stubFor(
        WireMock.post(WireMock.urlEqualTo("/orders"))
          .willReturn(WireMock.aResponse()
            .withBody("""{"id":"1","status":"PENDING"}""")
            .withStatus(200)
          )
    )

    val response = service.createOrder(order)

    assertEquals(order, response)
}

✅ 该测试验证了 Retrofit 使用 Gson 时能正确地将 OrderStatus.PENDING 转换为 "PENDING"

4.3. Moshi 序列化测试

测试 Moshi 的默认序列化行为:

@Test
fun `test Moshi default serialization`() = runBlocking {
    val service = moshiRetrofit.create(OrderService::class.java)
    val order = Order("1", OrderStatus.PENDING)
    wireMockRule.stubFor(
        WireMock.post(WireMock.urlEqualTo("/orders"))
          .willReturn(WireMock.aResponse()
            .withBody("""{"id":"1","status":"PENDING"}""")
            .withStatus(200)
          )
    )

    val response = service.createOrder(order)

    assertEquals(order, response)
}

✅ Moshi 同样使用 name() 方法进行枚举序列化,行为与 Gson 一致。

4.4. Jackson 序列化测试

测试 Jackson 的默认行为:

@Test
fun `test Jackson default serialization`() = runBlocking {
    val service = jacksonRetrofit.create(OrderService::class.java)
    val order = Order("1", OrderStatus.PENDING)
    wireMockRule.stubFor(
        WireMock.post(WireMock.urlEqualTo("/orders"))
          .willReturn(WireMock.aResponse()
            .withBody("""{"id":"1","status":"PENDING"}""")
            .withStatus(200)
          )
    )

    val response = service.createOrder(order)

    assertEquals(order, response)
}

✅ Jackson 也默认使用 name() 方法,所有测试均通过。

⚠️ 结论: Retrofit 中的 Gson、Moshi 和 Jackson 默认都会使用枚举的 name() 方法进行序列化和反序列化。

5. 总结

本文介绍了如何在 Kotlin 中使用 Retrofit 序列化枚举类型,并通过 WireMock 模拟测试验证了 Gson、Moshi 和 Jackson 的默认行为。它们都使用枚举的 name() 方法进行转换,这在大多数情况下已经足够使用。

如果你有自定义的序列化需求,例如希望将枚举序列化为特定的字符串(如数据库字段名、别名等),则需要通过自定义适配器或注解实现,这部分内容我们将在后续文章中展开。

完整代码示例可在 GitHub 上查看。


原始标题:Serialize Enum in Retrofit With Kotlin