Skip to content

Commit 1ab35ce

Browse files
committed
Serialize Kotlin Duration in same way as Java
1 parent a1f1619 commit 1ab35ce

File tree

3 files changed

+50
-12
lines changed

3 files changed

+50
-12
lines changed

src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ internal class SequenceToIteratorConverter(private val input: JavaType) : StdCon
2121

2222
internal object KotlinToJavaDurationConverter : StdConverter<KotlinDuration, JavaDuration>() {
2323
override fun convert(value: KotlinDuration) = value.toJavaDuration()
24+
25+
val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
2426
}
2527

2628
// this class is needed as workaround for deserialization

src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinAnnotationIntrospector.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ internal class KotlinAnnotationIntrospector(
6868

6969
override fun findSerializationConverter(a: Annotated): Converter<*, *>? = when (a) {
7070
// Find a converter to handle the case where the getter returns an unboxed value from the value class.
71-
is AnnotatedMethod -> cache.findValueClassReturnType(a)
72-
?.let { cache.getValueClassBoxConverter(a.rawReturnType, it) }
71+
is AnnotatedMethod -> a.ktClass()?.let { cache.getValueClassBoxConverter(a.rawReturnType, it) }
7372
is AnnotatedClass -> lookupKotlinTypeConverter(a)
7473
else -> null
7574
}
@@ -87,10 +86,14 @@ internal class KotlinAnnotationIntrospector(
8786

8887
// Perform proper serialization even if the value wrapped by the value class is null.
8988
// If value is a non-null object type, it must not be reboxing.
90-
override fun findNullSerializer(am: Annotated): JsonSerializer<*>? = (am as? AnnotatedMethod)?.let { _ ->
91-
cache.findValueClassReturnType(am)
92-
?.takeIf { it.requireRebox() }
93-
?.let { cache.getValueClassBoxConverter(am.rawReturnType, it).delegatingSerializer }
89+
override fun findNullSerializer(am: Annotated): JsonSerializer<*>? = (am as? AnnotatedMethod)
90+
?.ktClass()
91+
?.takeIf { it.requireRebox() }
92+
?.let { cache.getValueClassBoxConverter(am.rawReturnType, it).delegatingSerializer }
93+
94+
override fun findSerializer(am: Annotated): Any? = when ((am as? AnnotatedMethod)?.ktClass()) {
95+
Duration::class -> KotlinToJavaDurationConverter.delegatingSerializer
96+
else -> super.findSerializer(am)
9497
}
9598

9699
/**
@@ -177,6 +180,8 @@ internal class KotlinAnnotationIntrospector(
177180
return requiredAnnotationOrNullability(byAnnotation, byNullability)
178181
}
179182

183+
private fun AnnotatedMethod.ktClass() = cache.findValueClassReturnType(this)
184+
180185
private fun KFunction<*>.isConstructorParameterRequired(index: Int): Boolean {
181186
return isParameterRequired(index)
182187
}

src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/DurationTests.kt

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.fasterxml.jackson.module.kotlin.test
22

33
import com.fasterxml.jackson.annotation.JsonCreator
4+
import com.fasterxml.jackson.annotation.JsonFormat
5+
import com.fasterxml.jackson.annotation.JsonFormat.Shape.STRING
46
import com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
57
import com.fasterxml.jackson.databind.SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS
68
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
@@ -12,7 +14,8 @@ import org.junit.Test
1214
import java.time.Instant
1315
import kotlin.test.assertContentEquals
1416
import kotlin.test.assertEquals
15-
import kotlin.time.Duration
17+
import kotlin.time.Duration as KotlinDuration
18+
import java.time.Duration as JavaDuration
1619
import kotlin.time.Duration.Companion.hours
1720

1821
class DurationTests {
@@ -31,7 +34,7 @@ class DurationTests {
3134
fun `should deserialize Kotlin duration`() {
3235
val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())
3336

34-
val result = mapper.readValue<Duration>("\"PT1H\"")
37+
val result = mapper.readValue<KotlinDuration>("\"PT1H\"")
3538

3639
assertEquals(1.hours, result)
3740
}
@@ -51,7 +54,7 @@ class DurationTests {
5154
fun `should deserialize Kotlin duration inside list`() {
5255
val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())
5356

54-
val result = mapper.readValue<List<Duration>>("""["PT1H","PT2H","PT3H"]""")
57+
val result = mapper.readValue<List<KotlinDuration>>("""["PT1H","PT2H","PT3H"]""")
5558

5659
assertContentEquals(listOf(1.hours, 2.hours, 3.hours), result)
5760
}
@@ -75,7 +78,7 @@ class DurationTests {
7578
fun `should deserialize Kotlin duration inside map`() {
7679
val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())
7780

78-
val result = mapper.readValue<Map<String, Duration>>("""{"a":"PT1H","b":"PT2H","c":"PT3H"}""")
81+
val result = mapper.readValue<Map<String, KotlinDuration>>("""{"a":"PT1H","b":"PT2H","c":"PT3H"}""")
7982

8083
assertEquals(result["a"], 1.hours)
8184
assertEquals(result["b"], 2.hours)
@@ -85,12 +88,12 @@ class DurationTests {
8588
data class Meeting(
8689
val start: Instant,
8790
@get:JsonDeserialize(converter = JavaToKotlinDurationConverter::class)
88-
val duration: Duration,
91+
val duration: KotlinDuration,
8992
) {
9093
companion object {
9194
@JvmStatic
9295
@JsonCreator
93-
fun create(start: Instant, duration: Duration) = Meeting(start, duration)
96+
fun create(start: Instant, duration: KotlinDuration) = Meeting(start, duration)
9497
}
9598
}
9699

@@ -115,4 +118,32 @@ class DurationTests {
115118
assertEquals(result.start, Instant.parse("2023-06-20T14:00:00Z"))
116119
assertEquals(result.duration, 1.5.hours)
117120
}
121+
122+
data class JDTO(
123+
val plain: JavaDuration = JavaDuration.ofHours(1),
124+
val optPlain: JavaDuration? = JavaDuration.ofHours(1),
125+
@field:JsonFormat(shape = STRING)
126+
val shapeAnnotation: JavaDuration = JavaDuration.ofHours(1),
127+
@field:JsonFormat(shape = STRING)
128+
val optShapeAnnotation: JavaDuration? = JavaDuration.ofHours(1),
129+
)
130+
131+
data class KDTO(
132+
val plain: KotlinDuration = 1.hours,
133+
val optPlain: KotlinDuration? = 1.hours,
134+
@field:JsonFormat(shape = STRING)
135+
val shapeAnnotation: KotlinDuration = 1.hours,
136+
@field:JsonFormat(shape = STRING)
137+
val optShapeAnnotation: KotlinDuration? = 1.hours,
138+
)
139+
140+
@Test
141+
fun `should serialize Kotlin duration exactly as Java duration`() {
142+
val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())
143+
144+
val jdto = JDTO()
145+
val kdto = KDTO()
146+
147+
assertEquals(mapper.writeValueAsString(jdto), mapper.writeValueAsString(kdto))
148+
}
118149
}

0 commit comments

Comments
 (0)