Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Duration support #689

Merged
merged 16 commits into from
Aug 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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].
k163377 marked this conversation as resolved.
Show resolved Hide resolved
* `@JsonFormat` annotations need to be declared either on getter using `@get:JsonFormat` or field using `@field:JsonFormat`.
kkurczewski marked this conversation as resolved.
Show resolved Hide resolved
* 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