Skip to content

Commit

Permalink
Merge pull request #689 from kkurczewski/duration-support
Browse files Browse the repository at this point in the history
Duration support
  • Loading branch information
k163377 authored Aug 6, 2023
2 parents 423fc83 + 73bf682 commit b98c76b
Show file tree
Hide file tree
Showing 7 changed files with 337 additions and 25 deletions.
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@
<version>${version.kotlin}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${version.kotlin}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
Expand All @@ -121,6 +127,12 @@
<artifactId>jackson-dataformat-xml</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<!-- needed for kotlin.time.Duration converter test -->
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
28 changes: 28 additions & 0 deletions src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package com.fasterxml.jackson.module.kotlin

import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.deser.std.StdDelegatingDeserializer
import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer
import com.fasterxml.jackson.databind.type.TypeFactory
import com.fasterxml.jackson.databind.util.StdConverter
import kotlin.reflect.KClass
import kotlin.time.toJavaDuration
import kotlin.time.toKotlinDuration
import java.time.Duration as JavaDuration
import kotlin.time.Duration as KotlinDuration

internal class SequenceToIteratorConverter(private val input: JavaType) : StdConverter<Sequence<*>, Iterator<*>>() {
override fun convert(value: Sequence<*>): Iterator<*> = value.iterator()
Expand All @@ -16,6 +21,29 @@ internal class SequenceToIteratorConverter(private val input: JavaType) : StdCon
?: typeFactory.constructType(Iterator::class.java)
}

internal object KotlinDurationValueToJavaDurationConverter : StdConverter<Long, JavaDuration>() {
private val boxConverter by lazy { ValueClassBoxConverter(Long::class.java, KotlinDuration::class) }

override fun convert(value: Long): JavaDuration = KotlinToJavaDurationConverter.convert(boxConverter.convert(value))
}

internal object KotlinToJavaDurationConverter : StdConverter<KotlinDuration, JavaDuration>() {
override fun convert(value: KotlinDuration) = value.toJavaDuration()
}

/**
* 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]
*/
internal object JavaToKotlinDurationConverter : StdConverter<JavaDuration, KotlinDuration>() {
override fun convert(value: JavaDuration) = value.toKotlinDuration()

val delegatingDeserializer: StdDelegatingDeserializer<KotlinDuration> by lazy {
StdDelegatingDeserializer(this)
}
}

// S is nullable because value corresponds to a nullable value class
// @see KotlinNamesAnnotationIntrospector.findNullSerializer
internal class ValueClassBoxConverter<S : Any?, D : Any>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import com.fasterxml.jackson.databind.Module
import com.fasterxml.jackson.databind.cfg.MapperConfig
import com.fasterxml.jackson.databind.introspect.*
import com.fasterxml.jackson.databind.jsontype.NamedType
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.fasterxml.jackson.databind.util.Converter
import java.lang.reflect.AccessibleObject
import java.lang.reflect.Constructor
Expand All @@ -22,14 +21,19 @@ import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.valueParameters
import kotlin.reflect.jvm.*
import kotlin.time.Duration


internal class KotlinAnnotationIntrospector(private val context: Module.SetupContext,
private val cache: ReflectionCache,
private val nullToEmptyCollection: Boolean,
private val nullToEmptyMap: Boolean,
private val nullIsSameAsDefault: Boolean) : NopAnnotationIntrospector() {
internal class KotlinAnnotationIntrospector(
private val context: Module.SetupContext,
private val cache: ReflectionCache,
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
// this likely impacts this class to be accurate about what COULD be considered required
Expand Down Expand Up @@ -66,11 +70,23 @@ internal class KotlinAnnotationIntrospector(private val context: Module.SetupCon

override fun findSerializationConverter(a: Annotated): Converter<*, *>? = when (a) {
// Find a converter to handle the case where the getter returns an unboxed value from the value class.
is AnnotatedMethod -> cache.findValueClassReturnType(a)
?.let { cache.getValueClassBoxConverter(a.rawReturnType, it) }
is AnnotatedClass -> a
.takeIf { Sequence::class.java.isAssignableFrom(it.rawType) }
?.let { SequenceToIteratorConverter(it.type) }
is AnnotatedMethod -> a.findValueClassReturnType()?.let {
if (useJavaDurationConversion && it == Duration::class) {
if (a.rawReturnType == Duration::class.java)
KotlinToJavaDurationConverter
else
KotlinDurationValueToJavaDurationConverter
} else {
cache.getValueClassBoxConverter(a.rawReturnType, it)
}
}
is AnnotatedClass -> lookupKotlinTypeConverter(a)
else -> null
}

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

Expand All @@ -81,10 +97,29 @@ internal class KotlinAnnotationIntrospector(private val context: Module.SetupCon

// Perform proper serialization even if the value wrapped by the value class is null.
// If value is a non-null object type, it must not be reboxing.
override fun findNullSerializer(am: Annotated): JsonSerializer<*>? = (am as? AnnotatedMethod)?.let { _ ->
cache.findValueClassReturnType(am)
?.takeIf { it.requireRebox() }
?.let { cache.getValueClassBoxConverter(am.rawReturnType, it).delegatingSerializer }
override fun findNullSerializer(am: Annotated): JsonSerializer<*>? = (am as? AnnotatedMethod)
?.findValueClassReturnType()
?.takeIf { it.requireRebox() }
?.let { cache.getValueClassBoxConverter(am.rawReturnType, it).delegatingSerializer }

override fun findDeserializationConverter(a: Annotated): Any? {
if (!useJavaDurationConversion) return null

return (a as? AnnotatedParameter)?.let { param ->
@Suppress("UNCHECKED_CAST")
val function: KFunction<*> = when (val owner = param.owner.member) {
is Constructor<*> -> cache.kotlinFromJava(owner as Constructor<Any>)
is Method -> cache.kotlinFromJava(owner)
else -> null
} ?: return@let null
val valueParameter = function.valueParameters[a.index]

if (valueParameter.type.classifier == Duration::class) {
JavaToKotlinDurationConverter
} else {
null
}
}
}

/**
Expand All @@ -102,7 +137,7 @@ internal class KotlinAnnotationIntrospector(private val context: Module.SetupCon

private fun AnnotatedField.hasRequiredMarker(): Boolean? {
val byAnnotation = (member as Field).isRequiredByAnnotation()
val byNullability = (member as Field).kotlinProperty?.returnType?.isRequired()
val byNullability = (member as Field).kotlinProperty?.returnType?.isRequired()

return requiredAnnotationOrNullability(byAnnotation, byNullability)
}
Expand All @@ -122,7 +157,7 @@ internal class KotlinAnnotationIntrospector(private val context: Module.SetupCon
}

private fun Method.isRequiredByAnnotation(): Boolean? {
return (this.annotations.firstOrNull { it.annotationClass.java == JsonProperty::class.java } as? JsonProperty)?.required
return (this.annotations.firstOrNull { it.annotationClass.java == JsonProperty::class.java } as? JsonProperty)?.required
}

// Since Kotlin's property has the same Type for each field, getter, and setter,
Expand Down Expand Up @@ -171,12 +206,14 @@ internal class KotlinAnnotationIntrospector(private val context: Module.SetupCon
return requiredAnnotationOrNullability(byAnnotation, byNullability)
}

private fun AnnotatedMethod.findValueClassReturnType() = cache.findValueClassReturnType(this)

private fun KFunction<*>.isConstructorParameterRequired(index: Int): Boolean {
return isParameterRequired(index)
}

private fun KFunction<*>.isMethodParameterRequired(index: Int): Boolean {
return isParameterRequired(index+1)
return isParameterRequired(index + 1)
}

private fun KFunction<*>.isParameterRequired(index: Int): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.deser.Deserializers
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import kotlin.time.Duration as KotlinDuration

object SequenceDeserializer : StdDeserializer<Sequence<*>>(Sequence::class.java) {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Sequence<*> {
Expand Down Expand Up @@ -81,11 +82,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 @@ -94,6 +97,8 @@ 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 ->
JavaToKotlinDurationConverter.takeIf { useJavaDurationConversion }?.delegatingDeserializer
else -> null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,16 @@ enum class KotlinFeature(private val enabledByDefault: Boolean) {
* In addition, the adjustment of behavior using get:JvmName is disabled.
* Note also that this feature does not apply to setters.
*/
KotlinPropertyNameAsImplicitName(enabledByDefault = false);
KotlinPropertyNameAsImplicitName(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].
* `@JsonFormat` annotations need to be declared either on getter using `@get:JsonFormat` or field using `@field:JsonFormat`.
* See [jackson-module-kotlin#651] for details.
*/
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 @@ -8,6 +8,7 @@ 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.KotlinPropertyNameAsImplicitName
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 @@ -33,6 +34,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 @@ -55,7 +58,8 @@ class KotlinModule @Deprecated(
val nullIsSameAsDefault: Boolean = false,
val singletonSupport: SingletonSupport = DISABLED,
val strictNullChecks: Boolean = false,
val useKotlinPropertyNameForGetter: Boolean = false
val useKotlinPropertyNameForGetter: Boolean = false,
val useJavaDurationConversion: Boolean = false,
) : SimpleModule(KotlinModule::class.java.name, PackageVersion.VERSION) {
init {
if (!KotlinVersion.CURRENT.isAtLeast(1, 5)) {
Expand Down Expand Up @@ -105,7 +109,8 @@ class KotlinModule @Deprecated(
else -> DISABLED
},
builder.isEnabled(StrictNullChecks),
builder.isEnabled(KotlinPropertyNameAsImplicitName)
builder.isEnabled(KotlinPropertyNameAsImplicitName),
builder.isEnabled(UseJavaDurationConversion),
)

companion object {
Expand All @@ -132,7 +137,14 @@ class KotlinModule @Deprecated(
}
}

context.insertAnnotationIntrospector(KotlinAnnotationIntrospector(context, cache, nullToEmptyCollection, nullToEmptyMap, nullIsSameAsDefault))
context.insertAnnotationIntrospector(KotlinAnnotationIntrospector(
context,
cache,
nullToEmptyCollection,
nullToEmptyMap,
nullIsSameAsDefault,
useJavaDurationConversion
))
context.appendAnnotationIntrospector(
KotlinNamesAnnotationIntrospector(
this,
Expand All @@ -141,7 +153,7 @@ class KotlinModule @Deprecated(
useKotlinPropertyNameForGetter)
)

context.addDeserializers(KotlinDeserializers())
context.addDeserializers(KotlinDeserializers(useJavaDurationConversion))
context.addKeyDeserializers(KotlinKeyDeserializers)
context.addSerializers(KotlinSerializers())
context.addKeySerializers(KotlinKeySerializers())
Expand Down
Loading

0 comments on commit b98c76b

Please sign in to comment.