Skip to content

Commit

Permalink
Add feature toggle for Duration parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
kkurczewski committed Aug 3, 2023
1 parent 24ead11 commit 1e4a09e
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ internal object KotlinToJavaDurationConverter : StdConverter<KotlinDuration, Jav
val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
}

// this class is needed as workaround for deserialization
// data classes with kotlin.time.Duration field which is a value class
//
// @see DurationTests.`should deserialize Kotlin duration inside data class`
/**
* 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.
*
* @see [com.fasterxml.jackson.module.kotlin.test.DurationTests]
*/
object JavaToKotlinDurationConverter : StdConverter<JavaDuration, KotlinDuration>() {
override fun convert(value: JavaDuration) = KotlinDuration.parseIsoString(value.toString())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal class KotlinAnnotationIntrospector(
private val nullToEmptyCollection: Boolean,
private val nullToEmptyMap: Boolean,
private val nullIsSameAsDefault: Boolean,
private val useJavaDurationConversion: Boolean,
) : NopAnnotationIntrospector() {

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

private fun lookupKotlinTypeConverter(a: AnnotatedClass) = when {
Sequence::class.java.isAssignableFrom(a.rawType) -> SequenceToIteratorConverter(a.type)
Duration::class.java.isAssignableFrom(a.rawType) -> KotlinToJavaDurationConverter
Duration::class.java.isAssignableFrom(a.rawType) -> KotlinToJavaDurationConverter.takeIf { useJavaDurationConversion }
else -> null
}

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

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

/**
* Subclasses can be detected automatically for sealed classes, since all possible subclasses are known
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,13 @@ object ULongDeserializer : StdDeserializer<ULong>(ULong::class.java) {
)
}

internal class KotlinDeserializers : Deserializers.Base() {
internal class KotlinDeserializers(
private val useJavaDurationConversion: Boolean,
) : Deserializers.Base() {
override fun findBeanDeserializer(
type: JavaType,
config: DeserializationConfig?,
beanDesc: BeanDescription?
beanDesc: BeanDescription?,
): JsonDeserializer<*>? {
return when {
type.isInterface && type.rawClass == Sequence::class.java -> SequenceDeserializer
Expand All @@ -102,7 +104,7 @@ internal class KotlinDeserializers : Deserializers.Base() {
type.rawClass == UShort::class.java -> UShortDeserializer
type.rawClass == UInt::class.java -> UIntDeserializer
type.rawClass == ULong::class.java -> ULongDeserializer
type.rawClass == KotlinDuration::class.java -> DurationDeserializer
type.rawClass == KotlinDuration::class.java -> DurationDeserializer.takeIf { useJavaDurationConversion }
else -> null
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.fasterxml.jackson.module.kotlin

import java.util.BitSet
import kotlin.math.pow

/**
* @see KotlinModule.Builder
Expand Down Expand Up @@ -42,7 +41,14 @@ enum class KotlinFeature(private val enabledByDefault: Boolean) {
* may contain null values after deserialization.
* Enabling it protects against this but has significant performance impact.
*/
StrictNullChecks(enabledByDefault = false);
StrictNullChecks(enabledByDefault = false),

/**
* This feature represents whether to handle [kotlin.time.Duration] using [java.time.Duration] as conversion bridge.
*
* This allows use Kotlin Duration type with [com.fasterxml.jackson.datatype.jsr310.JavaTimeModule].
*/
UseJavaDurationConversion(enabledByDefault = false);

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullIsSameAsDefault
import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyCollection
import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyMap
import com.fasterxml.jackson.module.kotlin.KotlinFeature.StrictNullChecks
import com.fasterxml.jackson.module.kotlin.KotlinFeature.UseJavaDurationConversion
import com.fasterxml.jackson.module.kotlin.SingletonSupport.CANONICALIZE
import com.fasterxml.jackson.module.kotlin.SingletonSupport.DISABLED
import java.util.*
Expand All @@ -32,6 +33,8 @@ fun Class<*>.isKotlinClass(): Boolean {
* the default, collections which are typed to disallow null members
* (e.g. List<String>) may contain null values after deserialization. Enabling it
* protects against this but has significant performance impact.
* @param useJavaDurationConversion Default: false. Whether to use [java.time.Duration] as a bridge for [kotlin.time.Duration].
* This allows use Kotlin Duration type with [com.fasterxml.jackson.datatype.jsr310.JavaTimeModule].
*/
class KotlinModule @Deprecated(
level = DeprecationLevel.WARNING,
Expand All @@ -53,7 +56,8 @@ class KotlinModule @Deprecated(
val nullToEmptyMap: Boolean = false,
val nullIsSameAsDefault: Boolean = false,
val singletonSupport: SingletonSupport = DISABLED,
val strictNullChecks: Boolean = false
val strictNullChecks: Boolean = false,
private val useJavaDurationConversion: Boolean = false,
) : SimpleModule(KotlinModule::class.java.name, PackageVersion.VERSION) {
init {
if (!KotlinVersion.CURRENT.isAtLeast(1, 5)) {
Expand Down Expand Up @@ -102,7 +106,8 @@ class KotlinModule @Deprecated(
builder.isEnabled(KotlinFeature.SingletonSupport) -> CANONICALIZE
else -> DISABLED
},
builder.isEnabled(StrictNullChecks)
builder.isEnabled(StrictNullChecks),
builder.isEnabled(UseJavaDurationConversion)
)

companion object {
Expand All @@ -129,10 +134,17 @@ class KotlinModule @Deprecated(
}
}

context.insertAnnotationIntrospector(KotlinAnnotationIntrospector(context, cache, nullToEmptyCollection, nullToEmptyMap, nullIsSameAsDefault))
context.insertAnnotationIntrospector(KotlinAnnotationIntrospector(
context,
cache,
nullToEmptyCollection,
nullToEmptyMap,
nullIsSameAsDefault,
useJavaDurationConversion
))
context.appendAnnotationIntrospector(KotlinNamesAnnotationIntrospector(this, cache, ignoredClassesForImplyingJsonCreator))

context.addDeserializers(KotlinDeserializers())
context.addDeserializers(KotlinDeserializers(useJavaDurationConversion))
context.addKeyDeserializers(KotlinKeyDeserializers)
context.addSerializers(KotlinSerializers())
context.addKeySerializers(KotlinKeySerializers())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,30 @@ package com.fasterxml.jackson.module.kotlin.test
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.annotation.JsonFormat.Shape.STRING
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
import com.fasterxml.jackson.databind.SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.JavaToKotlinDurationConverter
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinFeature.UseJavaDurationConversion
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.kotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import org.junit.Test
import java.time.Instant
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.time.Duration as KotlinDuration
import java.time.Duration as JavaDuration
import kotlin.time.Duration.Companion.hours
import java.time.Duration as JavaDuration
import kotlin.time.Duration as KotlinDuration

class DurationTests {
private val objectMapper = jacksonObjectMapper { enable(UseJavaDurationConversion) }

@Test
fun `should serialize Kotlin duration using Java time module`() {
val mapper = jacksonObjectMapper()
.registerModule(JavaTimeModule())
.disable(WRITE_DURATIONS_AS_TIMESTAMPS)
val mapper = objectMapper.registerModule(JavaTimeModule()).disable(WRITE_DURATIONS_AS_TIMESTAMPS)

val result = mapper.writeValueAsString(1.hours)

Expand All @@ -32,7 +35,7 @@ class DurationTests {

@Test
fun `should deserialize Kotlin duration`() {
val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())
val mapper = objectMapper.registerModule(JavaTimeModule())

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

Expand All @@ -41,7 +44,7 @@ class DurationTests {

@Test
fun `should serialize Kotlin duration inside list using Java time module`() {
val mapper = jacksonObjectMapper()
val mapper = objectMapper
.registerModule(JavaTimeModule())
.disable(WRITE_DURATIONS_AS_TIMESTAMPS)

Expand All @@ -52,7 +55,7 @@ class DurationTests {

@Test
fun `should deserialize Kotlin duration inside list`() {
val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())
val mapper = objectMapper.registerModule(JavaTimeModule())

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

Expand All @@ -61,7 +64,7 @@ class DurationTests {

@Test
fun `should serialize Kotlin duration inside map using Java time module`() {
val mapper = jacksonObjectMapper()
val mapper = objectMapper
.registerModule(JavaTimeModule())
.disable(WRITE_DURATIONS_AS_TIMESTAMPS)

Expand All @@ -76,7 +79,7 @@ class DurationTests {

@Test
fun `should deserialize Kotlin duration inside map`() {
val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())
val mapper = objectMapper.registerModule(JavaTimeModule())

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

Expand All @@ -99,7 +102,7 @@ class DurationTests {

@Test
fun `should serialize Kotlin duration inside data class using Java time module`() {
val mapper = jacksonObjectMapper()
val mapper = objectMapper
.registerModule(JavaTimeModule())
.disable(WRITE_DATES_AS_TIMESTAMPS)
.disable(WRITE_DURATIONS_AS_TIMESTAMPS)
Expand All @@ -111,7 +114,7 @@ class DurationTests {

@Test
fun `should deserialize Kotlin duration inside data class`() {
val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())
val mapper = objectMapper.registerModule(JavaTimeModule())

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

Expand Down Expand Up @@ -139,11 +142,15 @@ class DurationTests {

@Test
fun `should serialize Kotlin duration exactly as Java duration`() {
val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())
val mapper = objectMapper.registerModule(JavaTimeModule())

val jdto = JDTO()
val kdto = KDTO()

assertEquals(mapper.writeValueAsString(jdto), mapper.writeValueAsString(kdto))
}
}

private fun jacksonObjectMapper(
configuration: KotlinModule.Builder.() -> Unit,
) = ObjectMapper().registerModule(kotlinModule(configuration))
}

0 comments on commit 1e4a09e

Please sign in to comment.