Skip to content

Commit 1e4a09e

Browse files
committed
Add feature toggle for Duration parsing
1 parent 24ead11 commit 1e4a09e

File tree

6 files changed

+61
-32
lines changed

6 files changed

+61
-32
lines changed

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ internal object KotlinToJavaDurationConverter : StdConverter<KotlinDuration, Jav
2525
val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
2626
}
2727

28-
// this class is needed as workaround for deserialization
29-
// data classes with kotlin.time.Duration field which is a value class
30-
//
31-
// @see DurationTests.`should deserialize Kotlin duration inside data class`
28+
/**
29+
* Currently it is not possible to deduce type of [kotlin.time.Duration] fields therefore explicit annotation is needed on fields in order to properly deserialize POJO.
30+
*
31+
* @see [com.fasterxml.jackson.module.kotlin.test.DurationTests]
32+
*/
3233
object JavaToKotlinDurationConverter : StdConverter<JavaDuration, KotlinDuration>() {
3334
override fun convert(value: JavaDuration) = KotlinDuration.parseIsoString(value.toString())
3435
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ internal class KotlinAnnotationIntrospector(
3131
private val nullToEmptyCollection: Boolean,
3232
private val nullToEmptyMap: Boolean,
3333
private val nullIsSameAsDefault: Boolean,
34+
private val useJavaDurationConversion: Boolean,
3435
) : NopAnnotationIntrospector() {
3536

3637
// TODO: implement nullIsSameAsDefault flag, which represents when TRUE that if something has a default value, it can be passed a null to default it
@@ -75,7 +76,7 @@ internal class KotlinAnnotationIntrospector(
7576

7677
private fun lookupKotlinTypeConverter(a: AnnotatedClass) = when {
7778
Sequence::class.java.isAssignableFrom(a.rawType) -> SequenceToIteratorConverter(a.type)
78-
Duration::class.java.isAssignableFrom(a.rawType) -> KotlinToJavaDurationConverter
79+
Duration::class.java.isAssignableFrom(a.rawType) -> KotlinToJavaDurationConverter.takeIf { useJavaDurationConversion }
7980
else -> null
8081
}
8182

@@ -92,9 +93,9 @@ internal class KotlinAnnotationIntrospector(
9293
?.let { cache.getValueClassBoxConverter(am.rawReturnType, it).delegatingSerializer }
9394

9495
override fun findSerializer(am: Annotated): Any? = when ((am as? AnnotatedMethod)?.ktClass()) {
95-
Duration::class -> KotlinToJavaDurationConverter.delegatingSerializer
96-
else -> super.findSerializer(am)
97-
}
96+
Duration::class -> KotlinToJavaDurationConverter.delegatingSerializer.takeIf { useJavaDurationConversion }
97+
else -> null
98+
} ?: super.findSerializer(am)
9899

99100
/**
100101
* Subclasses can be detected automatically for sealed classes, since all possible subclasses are known

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,13 @@ object ULongDeserializer : StdDeserializer<ULong>(ULong::class.java) {
8989
)
9090
}
9191

92-
internal class KotlinDeserializers : Deserializers.Base() {
92+
internal class KotlinDeserializers(
93+
private val useJavaDurationConversion: Boolean,
94+
) : Deserializers.Base() {
9395
override fun findBeanDeserializer(
9496
type: JavaType,
9597
config: DeserializationConfig?,
96-
beanDesc: BeanDescription?
98+
beanDesc: BeanDescription?,
9799
): JsonDeserializer<*>? {
98100
return when {
99101
type.isInterface && type.rawClass == Sequence::class.java -> SequenceDeserializer
@@ -102,7 +104,7 @@ internal class KotlinDeserializers : Deserializers.Base() {
102104
type.rawClass == UShort::class.java -> UShortDeserializer
103105
type.rawClass == UInt::class.java -> UIntDeserializer
104106
type.rawClass == ULong::class.java -> ULongDeserializer
105-
type.rawClass == KotlinDuration::class.java -> DurationDeserializer
107+
type.rawClass == KotlinDuration::class.java -> DurationDeserializer.takeIf { useJavaDurationConversion }
106108
else -> null
107109
}
108110
}

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

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

33
import java.util.BitSet
4-
import kotlin.math.pow
54

65
/**
76
* @see KotlinModule.Builder
@@ -42,7 +41,14 @@ enum class KotlinFeature(private val enabledByDefault: Boolean) {
4241
* may contain null values after deserialization.
4342
* Enabling it protects against this but has significant performance impact.
4443
*/
45-
StrictNullChecks(enabledByDefault = false);
44+
StrictNullChecks(enabledByDefault = false),
45+
46+
/**
47+
* This feature represents whether to handle [kotlin.time.Duration] using [java.time.Duration] as conversion bridge.
48+
*
49+
* This allows use Kotlin Duration type with [com.fasterxml.jackson.datatype.jsr310.JavaTimeModule].
50+
*/
51+
UseJavaDurationConversion(enabledByDefault = false);
4652

4753
internal val bitSet: BitSet = (1 shl ordinal).toBitSet()
4854

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullIsSameAsDefault
77
import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyCollection
88
import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyMap
99
import com.fasterxml.jackson.module.kotlin.KotlinFeature.StrictNullChecks
10+
import com.fasterxml.jackson.module.kotlin.KotlinFeature.UseJavaDurationConversion
1011
import com.fasterxml.jackson.module.kotlin.SingletonSupport.CANONICALIZE
1112
import com.fasterxml.jackson.module.kotlin.SingletonSupport.DISABLED
1213
import java.util.*
@@ -32,6 +33,8 @@ fun Class<*>.isKotlinClass(): Boolean {
3233
* the default, collections which are typed to disallow null members
3334
* (e.g. List<String>) may contain null values after deserialization. Enabling it
3435
* protects against this but has significant performance impact.
36+
* @param useJavaDurationConversion Default: false. Whether to use [java.time.Duration] as a bridge for [kotlin.time.Duration].
37+
* This allows use Kotlin Duration type with [com.fasterxml.jackson.datatype.jsr310.JavaTimeModule].
3538
*/
3639
class KotlinModule @Deprecated(
3740
level = DeprecationLevel.WARNING,
@@ -53,7 +56,8 @@ class KotlinModule @Deprecated(
5356
val nullToEmptyMap: Boolean = false,
5457
val nullIsSameAsDefault: Boolean = false,
5558
val singletonSupport: SingletonSupport = DISABLED,
56-
val strictNullChecks: Boolean = false
59+
val strictNullChecks: Boolean = false,
60+
private val useJavaDurationConversion: Boolean = false,
5761
) : SimpleModule(KotlinModule::class.java.name, PackageVersion.VERSION) {
5862
init {
5963
if (!KotlinVersion.CURRENT.isAtLeast(1, 5)) {
@@ -102,7 +106,8 @@ class KotlinModule @Deprecated(
102106
builder.isEnabled(KotlinFeature.SingletonSupport) -> CANONICALIZE
103107
else -> DISABLED
104108
},
105-
builder.isEnabled(StrictNullChecks)
109+
builder.isEnabled(StrictNullChecks),
110+
builder.isEnabled(UseJavaDurationConversion)
106111
)
107112

108113
companion object {
@@ -129,10 +134,17 @@ class KotlinModule @Deprecated(
129134
}
130135
}
131136

132-
context.insertAnnotationIntrospector(KotlinAnnotationIntrospector(context, cache, nullToEmptyCollection, nullToEmptyMap, nullIsSameAsDefault))
137+
context.insertAnnotationIntrospector(KotlinAnnotationIntrospector(
138+
context,
139+
cache,
140+
nullToEmptyCollection,
141+
nullToEmptyMap,
142+
nullIsSameAsDefault,
143+
useJavaDurationConversion
144+
))
133145
context.appendAnnotationIntrospector(KotlinNamesAnnotationIntrospector(this, cache, ignoredClassesForImplyingJsonCreator))
134146

135-
context.addDeserializers(KotlinDeserializers())
147+
context.addDeserializers(KotlinDeserializers(useJavaDurationConversion))
136148
context.addKeyDeserializers(KotlinKeyDeserializers)
137149
context.addSerializers(KotlinSerializers())
138150
context.addKeySerializers(KotlinKeySerializers())

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

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,30 @@ package com.fasterxml.jackson.module.kotlin.test
33
import com.fasterxml.jackson.annotation.JsonCreator
44
import com.fasterxml.jackson.annotation.JsonFormat
55
import com.fasterxml.jackson.annotation.JsonFormat.Shape.STRING
6+
import com.fasterxml.jackson.databind.ObjectMapper
67
import com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
78
import com.fasterxml.jackson.databind.SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS
89
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
910
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
1011
import com.fasterxml.jackson.module.kotlin.JavaToKotlinDurationConverter
11-
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
12+
import com.fasterxml.jackson.module.kotlin.KotlinFeature.UseJavaDurationConversion
13+
import com.fasterxml.jackson.module.kotlin.KotlinModule
14+
import com.fasterxml.jackson.module.kotlin.kotlinModule
1215
import com.fasterxml.jackson.module.kotlin.readValue
1316
import org.junit.Test
1417
import java.time.Instant
1518
import kotlin.test.assertContentEquals
1619
import kotlin.test.assertEquals
17-
import kotlin.time.Duration as KotlinDuration
18-
import java.time.Duration as JavaDuration
1920
import kotlin.time.Duration.Companion.hours
21+
import java.time.Duration as JavaDuration
22+
import kotlin.time.Duration as KotlinDuration
2023

2124
class DurationTests {
25+
private val objectMapper = jacksonObjectMapper { enable(UseJavaDurationConversion) }
26+
2227
@Test
2328
fun `should serialize Kotlin duration using Java time module`() {
24-
val mapper = jacksonObjectMapper()
25-
.registerModule(JavaTimeModule())
26-
.disable(WRITE_DURATIONS_AS_TIMESTAMPS)
29+
val mapper = objectMapper.registerModule(JavaTimeModule()).disable(WRITE_DURATIONS_AS_TIMESTAMPS)
2730

2831
val result = mapper.writeValueAsString(1.hours)
2932

@@ -32,7 +35,7 @@ class DurationTests {
3235

3336
@Test
3437
fun `should deserialize Kotlin duration`() {
35-
val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())
38+
val mapper = objectMapper.registerModule(JavaTimeModule())
3639

3740
val result = mapper.readValue<KotlinDuration>("\"PT1H\"")
3841

@@ -41,7 +44,7 @@ class DurationTests {
4144

4245
@Test
4346
fun `should serialize Kotlin duration inside list using Java time module`() {
44-
val mapper = jacksonObjectMapper()
47+
val mapper = objectMapper
4548
.registerModule(JavaTimeModule())
4649
.disable(WRITE_DURATIONS_AS_TIMESTAMPS)
4750

@@ -52,7 +55,7 @@ class DurationTests {
5255

5356
@Test
5457
fun `should deserialize Kotlin duration inside list`() {
55-
val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())
58+
val mapper = objectMapper.registerModule(JavaTimeModule())
5659

5760
val result = mapper.readValue<List<KotlinDuration>>("""["PT1H","PT2H","PT3H"]""")
5861

@@ -61,7 +64,7 @@ class DurationTests {
6164

6265
@Test
6366
fun `should serialize Kotlin duration inside map using Java time module`() {
64-
val mapper = jacksonObjectMapper()
67+
val mapper = objectMapper
6568
.registerModule(JavaTimeModule())
6669
.disable(WRITE_DURATIONS_AS_TIMESTAMPS)
6770

@@ -76,7 +79,7 @@ class DurationTests {
7679

7780
@Test
7881
fun `should deserialize Kotlin duration inside map`() {
79-
val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())
82+
val mapper = objectMapper.registerModule(JavaTimeModule())
8083

8184
val result = mapper.readValue<Map<String, KotlinDuration>>("""{"a":"PT1H","b":"PT2H","c":"PT3H"}""")
8285

@@ -99,7 +102,7 @@ class DurationTests {
99102

100103
@Test
101104
fun `should serialize Kotlin duration inside data class using Java time module`() {
102-
val mapper = jacksonObjectMapper()
105+
val mapper = objectMapper
103106
.registerModule(JavaTimeModule())
104107
.disable(WRITE_DATES_AS_TIMESTAMPS)
105108
.disable(WRITE_DURATIONS_AS_TIMESTAMPS)
@@ -111,7 +114,7 @@ class DurationTests {
111114

112115
@Test
113116
fun `should deserialize Kotlin duration inside data class`() {
114-
val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())
117+
val mapper = objectMapper.registerModule(JavaTimeModule())
115118

116119
val result = mapper.readValue<Meeting>("""{"start":"2023-06-20T14:00:00Z","duration":"PT1H30M"}""")
117120

@@ -139,11 +142,15 @@ class DurationTests {
139142

140143
@Test
141144
fun `should serialize Kotlin duration exactly as Java duration`() {
142-
val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())
145+
val mapper = objectMapper.registerModule(JavaTimeModule())
143146

144147
val jdto = JDTO()
145148
val kdto = KDTO()
146149

147150
assertEquals(mapper.writeValueAsString(jdto), mapper.writeValueAsString(kdto))
148151
}
149-
}
152+
153+
private fun jacksonObjectMapper(
154+
configuration: KotlinModule.Builder.() -> Unit,
155+
) = ObjectMapper().registerModule(kotlinModule(configuration))
156+
}

0 commit comments

Comments
 (0)