square/moshi

Kotlin Duration Adapter fails to be generated with codegen

andretmarques opened this issue · 0 comments

When integrating custom adapters and annotations with kotlin.time.Duration, the expected behavior is for the adapter to accurately convert JSON's long values into Kotlin's Duration type. However, I've observed that the deserialization process doesn't work as expected. Instead of obtaining a Duration object, the output remains as a Long type. This issue seems to occur during the deserialization process, indicating a potential problem in the handling of Kotlin's Duration type.

Here is the adapter and annotations created:

@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class DurationMillis

@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class DurationSeconds

@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class DurationMinutes

object DurationJsonAdapter {
  @ToJson
  fun millisToJson(@DurationMillis duration: Duration): Long = duration.inWholeMilliseconds

  @FromJson
  @DurationMillis
  fun millisFromJson(value: Long): Duration = value.milliseconds

  @ToJson
  fun secondsToJson(@DurationSeconds duration: Duration): Long = duration.inWholeSeconds

  @FromJson
  @DurationSeconds
  fun secondsFromJson(value: Long): Duration = value.seconds

  @ToJson
  fun minutesToJson(@DurationMinutes duration: Duration): Long = duration.inWholeMinutes

  @FromJson
  @DurationMinutes
  fun minutesFromJson(value: Long): Duration = value.minutes
}

with some tests included:

class DurationJsonAdapterTest {
  @JsonClass(generateAdapter = true)
  data class DurationTypes(
    @Json(name = "durationMillis") @DurationMillis val durationMillis: Duration,
    @Json(name = "durationSeconds") @DurationSeconds val durationSeconds: Duration,
    @Json(name = "durationMinutes") @DurationMinutes val durationMinutes: Duration,
  )

  private val moshi = Moshi.Builder()
    .add(DurationJsonAdapter)
    .build()


  @OptIn(ExperimentalStdlibApi::class)
  @Test
  fun fromJson() {
    val adapter = moshi.adapter<DurationTypes>()
    val json = """{"durationMillis": 120000, "durationSeconds": 120, "durationMinutes": 2}"""
    val javanDuration = adapter.fromJson(json)
    assertEquals(2.minutes, javanDuration?.durationMillis)
    assertEquals(2.minutes, javanDuration?.durationSeconds)
    assertEquals(2.minutes, javanDuration?.durationMinutes)
  }

  @OptIn(ExperimentalStdlibApi::class)
  @Test
  fun toJson() {
    val adapter: JsonAdapter<DurationTypes> = moshi.adapter<DurationTypes>()
    val javanDuration = DurationTypes(2.minutes, 2.minutes, 2.minutes)
    val json = adapter.toJson(javanDuration)
    assertEquals("""{"durationMillis":120000, "durationSeconds":120, "durationMinutes":2}""", json)
  }
}

The output:
image

Inspecting the factories:
image

With Java Duration (java.time.Duration) works perfectly well
image
image

class JavaDurationJsonAdapterTest {
  @JsonClass(generateAdapter = true)
  data class DurationTypes(
    @Json(name = "durationMillis") @JavaDurationMillis val durationMillis: JavaDuration,
    @Json(name = "durationSeconds") @JavaDurationSeconds val durationSeconds: JavaDuration,
    @Json(name = "durationMinutes") @JavaDurationMinutes val durationMinutes: JavaDuration,
  )

  private val moshi = Moshi.Builder()
    .add(JavaDurationJsonAdapter)
    .build()


  @OptIn(ExperimentalStdlibApi::class)
  @Test
  fun fromJson() {
    val adapter = moshi.adapter<DurationTypes>()
    val json = """{"durationMillis": 120000, "durationSeconds": 120, "durationMinutes": 2}"""
    val javanDuration = adapter.fromJson(json)
    assertEquals(JavaDuration.ofMinutes(2), javanDuration?.durationMillis)
    assertEquals(JavaDuration.ofMinutes(2), javanDuration?.durationSeconds)
    assertEquals(JavaDuration.ofMinutes(2), javanDuration?.durationMinutes)
  }

  @OptIn(ExperimentalStdlibApi::class)
  @Test
  fun toJson() {
    val adapter: JsonAdapter<DurationTypes> = moshi.adapter<DurationTypes>()
    val javanDuration = DurationTypes(JavaDuration.ofMinutes(2), JavaDuration.ofMinutes(2), JavaDuration.ofMinutes(2))
    val json = adapter.toJson(javanDuration)
    assertEquals("""{"durationMillis":120000,"durationSeconds":120,"durationMinutes":2}""", json)
  }
}

@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class JavaDurationMillis

@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class JavaDurationSeconds

@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class JavaDurationMinutes

object JavaDurationJsonAdapter {
  @ToJson
  fun millisToJson(@JavaDurationMillis duration: JavaDuration): Long = duration.toMillis()

  @FromJson
  @JavaDurationMillis
  fun millisFromJson(value: Long): JavaDuration = JavaDuration.ofMillis(value)

  @ToJson
  fun secondsToJson(@JavaDurationSeconds duration: JavaDuration): Long = duration.toSeconds()

  @FromJson
  @JavaDurationSeconds
  fun secondsFromJson(value: Long): JavaDuration = JavaDuration.ofSeconds(value)

  @ToJson
  fun minutesToJson(@JavaDurationMinutes duration: JavaDuration): Long = duration.toMinutes()

  @FromJson
  @JavaDurationMinutes
  fun minutesFromJson(value: Long): JavaDuration = JavaDuration.ofMinutes(value)
}