Skip to content

Commit b98c76b

Browse files
authored
Merge pull request #689 from kkurczewski/duration-support
Duration support
2 parents 423fc83 + 73bf682 commit b98c76b

File tree

7 files changed

+337
-25
lines changed

7 files changed

+337
-25
lines changed

pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@
9696
<version>${version.kotlin}</version>
9797
<scope>provided</scope>
9898
</dependency>
99+
<dependency>
100+
<groupId>org.jetbrains.kotlin</groupId>
101+
<artifactId>kotlin-stdlib-jdk8</artifactId>
102+
<version>${version.kotlin}</version>
103+
<scope>provided</scope>
104+
</dependency>
99105
<dependency>
100106
<groupId>org.jetbrains.kotlin</groupId>
101107
<artifactId>kotlin-reflect</artifactId>
@@ -121,6 +127,12 @@
121127
<artifactId>jackson-dataformat-xml</artifactId>
122128
<scope>test</scope>
123129
</dependency>
130+
<dependency>
131+
<!-- needed for kotlin.time.Duration converter test -->
132+
<groupId>com.fasterxml.jackson.datatype</groupId>
133+
<artifactId>jackson-datatype-jsr310</artifactId>
134+
<scope>test</scope>
135+
</dependency>
124136
</dependencies>
125137

126138
<build>

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package com.fasterxml.jackson.module.kotlin
22

33
import com.fasterxml.jackson.databind.JavaType
4+
import com.fasterxml.jackson.databind.deser.std.StdDelegatingDeserializer
45
import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer
56
import com.fasterxml.jackson.databind.type.TypeFactory
67
import com.fasterxml.jackson.databind.util.StdConverter
78
import kotlin.reflect.KClass
9+
import kotlin.time.toJavaDuration
10+
import kotlin.time.toKotlinDuration
11+
import java.time.Duration as JavaDuration
12+
import kotlin.time.Duration as KotlinDuration
813

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

24+
internal object KotlinDurationValueToJavaDurationConverter : StdConverter<Long, JavaDuration>() {
25+
private val boxConverter by lazy { ValueClassBoxConverter(Long::class.java, KotlinDuration::class) }
26+
27+
override fun convert(value: Long): JavaDuration = KotlinToJavaDurationConverter.convert(boxConverter.convert(value))
28+
}
29+
30+
internal object KotlinToJavaDurationConverter : StdConverter<KotlinDuration, JavaDuration>() {
31+
override fun convert(value: KotlinDuration) = value.toJavaDuration()
32+
}
33+
34+
/**
35+
* 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.
36+
*
37+
* @see [com.fasterxml.jackson.module.kotlin.test.DurationTests]
38+
*/
39+
internal object JavaToKotlinDurationConverter : StdConverter<JavaDuration, KotlinDuration>() {
40+
override fun convert(value: JavaDuration) = value.toKotlinDuration()
41+
42+
val delegatingDeserializer: StdDelegatingDeserializer<KotlinDuration> by lazy {
43+
StdDelegatingDeserializer(this)
44+
}
45+
}
46+
1947
// S is nullable because value corresponds to a nullable value class
2048
// @see KotlinNamesAnnotationIntrospector.findNullSerializer
2149
internal class ValueClassBoxConverter<S : Any?, D : Any>(

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

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import com.fasterxml.jackson.databind.Module
88
import com.fasterxml.jackson.databind.cfg.MapperConfig
99
import com.fasterxml.jackson.databind.introspect.*
1010
import com.fasterxml.jackson.databind.jsontype.NamedType
11-
import com.fasterxml.jackson.databind.ser.std.StdSerializer
1211
import com.fasterxml.jackson.databind.util.Converter
1312
import java.lang.reflect.AccessibleObject
1413
import java.lang.reflect.Constructor
@@ -22,14 +21,19 @@ import kotlin.reflect.KType
2221
import kotlin.reflect.full.createType
2322
import kotlin.reflect.full.declaredMemberProperties
2423
import kotlin.reflect.full.memberProperties
24+
import kotlin.reflect.full.valueParameters
2525
import kotlin.reflect.jvm.*
26+
import kotlin.time.Duration
2627

2728

28-
internal class KotlinAnnotationIntrospector(private val context: Module.SetupContext,
29-
private val cache: ReflectionCache,
30-
private val nullToEmptyCollection: Boolean,
31-
private val nullToEmptyMap: Boolean,
32-
private val nullIsSameAsDefault: Boolean) : NopAnnotationIntrospector() {
29+
internal class KotlinAnnotationIntrospector(
30+
private val context: Module.SetupContext,
31+
private val cache: ReflectionCache,
32+
private val nullToEmptyCollection: Boolean,
33+
private val nullToEmptyMap: Boolean,
34+
private val nullIsSameAsDefault: Boolean,
35+
private val useJavaDurationConversion: Boolean,
36+
) : NopAnnotationIntrospector() {
3337

3438
// TODO: implement nullIsSameAsDefault flag, which represents when TRUE that if something has a default value, it can be passed a null to default it
3539
// this likely impacts this class to be accurate about what COULD be considered required
@@ -66,11 +70,23 @@ internal class KotlinAnnotationIntrospector(private val context: Module.SetupCon
6670

6771
override fun findSerializationConverter(a: Annotated): Converter<*, *>? = when (a) {
6872
// Find a converter to handle the case where the getter returns an unboxed value from the value class.
69-
is AnnotatedMethod -> cache.findValueClassReturnType(a)
70-
?.let { cache.getValueClassBoxConverter(a.rawReturnType, it) }
71-
is AnnotatedClass -> a
72-
.takeIf { Sequence::class.java.isAssignableFrom(it.rawType) }
73-
?.let { SequenceToIteratorConverter(it.type) }
73+
is AnnotatedMethod -> a.findValueClassReturnType()?.let {
74+
if (useJavaDurationConversion && it == Duration::class) {
75+
if (a.rawReturnType == Duration::class.java)
76+
KotlinToJavaDurationConverter
77+
else
78+
KotlinDurationValueToJavaDurationConverter
79+
} else {
80+
cache.getValueClassBoxConverter(a.rawReturnType, it)
81+
}
82+
}
83+
is AnnotatedClass -> lookupKotlinTypeConverter(a)
84+
else -> null
85+
}
86+
87+
private fun lookupKotlinTypeConverter(a: AnnotatedClass) = when {
88+
Sequence::class.java.isAssignableFrom(a.rawType) -> SequenceToIteratorConverter(a.type)
89+
Duration::class.java == a.rawType -> KotlinToJavaDurationConverter.takeIf { useJavaDurationConversion }
7490
else -> null
7591
}
7692

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

8298
// Perform proper serialization even if the value wrapped by the value class is null.
8399
// If value is a non-null object type, it must not be reboxing.
84-
override fun findNullSerializer(am: Annotated): JsonSerializer<*>? = (am as? AnnotatedMethod)?.let { _ ->
85-
cache.findValueClassReturnType(am)
86-
?.takeIf { it.requireRebox() }
87-
?.let { cache.getValueClassBoxConverter(am.rawReturnType, it).delegatingSerializer }
100+
override fun findNullSerializer(am: Annotated): JsonSerializer<*>? = (am as? AnnotatedMethod)
101+
?.findValueClassReturnType()
102+
?.takeIf { it.requireRebox() }
103+
?.let { cache.getValueClassBoxConverter(am.rawReturnType, it).delegatingSerializer }
104+
105+
override fun findDeserializationConverter(a: Annotated): Any? {
106+
if (!useJavaDurationConversion) return null
107+
108+
return (a as? AnnotatedParameter)?.let { param ->
109+
@Suppress("UNCHECKED_CAST")
110+
val function: KFunction<*> = when (val owner = param.owner.member) {
111+
is Constructor<*> -> cache.kotlinFromJava(owner as Constructor<Any>)
112+
is Method -> cache.kotlinFromJava(owner)
113+
else -> null
114+
} ?: return@let null
115+
val valueParameter = function.valueParameters[a.index]
116+
117+
if (valueParameter.type.classifier == Duration::class) {
118+
JavaToKotlinDurationConverter
119+
} else {
120+
null
121+
}
122+
}
88123
}
89124

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

103138
private fun AnnotatedField.hasRequiredMarker(): Boolean? {
104139
val byAnnotation = (member as Field).isRequiredByAnnotation()
105-
val byNullability = (member as Field).kotlinProperty?.returnType?.isRequired()
140+
val byNullability = (member as Field).kotlinProperty?.returnType?.isRequired()
106141

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

124159
private fun Method.isRequiredByAnnotation(): Boolean? {
125-
return (this.annotations.firstOrNull { it.annotationClass.java == JsonProperty::class.java } as? JsonProperty)?.required
160+
return (this.annotations.firstOrNull { it.annotationClass.java == JsonProperty::class.java } as? JsonProperty)?.required
126161
}
127162

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

209+
private fun AnnotatedMethod.findValueClassReturnType() = cache.findValueClassReturnType(this)
210+
174211
private fun KFunction<*>.isConstructorParameterRequired(index: Int): Boolean {
175212
return isParameterRequired(index)
176213
}
177214

178215
private fun KFunction<*>.isMethodParameterRequired(index: Int): Boolean {
179-
return isParameterRequired(index+1)
216+
return isParameterRequired(index + 1)
180217
}
181218

182219
private fun KFunction<*>.isParameterRequired(index: Int): Boolean {

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.JavaType
1010
import com.fasterxml.jackson.databind.JsonDeserializer
1111
import com.fasterxml.jackson.databind.deser.Deserializers
1212
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
13+
import kotlin.time.Duration as KotlinDuration
1314

1415
object SequenceDeserializer : StdDeserializer<Sequence<*>>(Sequence::class.java) {
1516
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Sequence<*> {
@@ -81,11 +82,13 @@ object ULongDeserializer : StdDeserializer<ULong>(ULong::class.java) {
8182
)
8283
}
8384

84-
internal class KotlinDeserializers : Deserializers.Base() {
85+
internal class KotlinDeserializers(
86+
private val useJavaDurationConversion: Boolean,
87+
) : Deserializers.Base() {
8588
override fun findBeanDeserializer(
8689
type: JavaType,
8790
config: DeserializationConfig?,
88-
beanDesc: BeanDescription?
91+
beanDesc: BeanDescription?,
8992
): JsonDeserializer<*>? {
9093
return when {
9194
type.isInterface && type.rawClass == Sequence::class.java -> SequenceDeserializer
@@ -94,6 +97,8 @@ internal class KotlinDeserializers : Deserializers.Base() {
9497
type.rawClass == UShort::class.java -> UShortDeserializer
9598
type.rawClass == UInt::class.java -> UIntDeserializer
9699
type.rawClass == ULong::class.java -> ULongDeserializer
100+
type.rawClass == KotlinDuration::class.java ->
101+
JavaToKotlinDurationConverter.takeIf { useJavaDurationConversion }?.delegatingDeserializer
97102
else -> null
98103
}
99104
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,16 @@ enum class KotlinFeature(private val enabledByDefault: Boolean) {
5858
* In addition, the adjustment of behavior using get:JvmName is disabled.
5959
* Note also that this feature does not apply to setters.
6060
*/
61-
KotlinPropertyNameAsImplicitName(enabledByDefault = false);
61+
KotlinPropertyNameAsImplicitName(enabledByDefault = false),
62+
63+
/**
64+
* This feature represents whether to handle [kotlin.time.Duration] using [java.time.Duration] as conversion bridge.
65+
*
66+
* This allows use Kotlin Duration type with [com.fasterxml.jackson.datatype.jsr310.JavaTimeModule].
67+
* `@JsonFormat` annotations need to be declared either on getter using `@get:JsonFormat` or field using `@field:JsonFormat`.
68+
* See [jackson-module-kotlin#651] for details.
69+
*/
70+
UseJavaDurationConversion(enabledByDefault = false);
6271

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

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ 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
1010
import com.fasterxml.jackson.module.kotlin.KotlinFeature.KotlinPropertyNameAsImplicitName
11+
import com.fasterxml.jackson.module.kotlin.KotlinFeature.UseJavaDurationConversion
1112
import com.fasterxml.jackson.module.kotlin.SingletonSupport.CANONICALIZE
1213
import com.fasterxml.jackson.module.kotlin.SingletonSupport.DISABLED
1314
import java.util.*
@@ -33,6 +34,8 @@ fun Class<*>.isKotlinClass(): Boolean {
3334
* the default, collections which are typed to disallow null members
3435
* (e.g. List<String>) may contain null values after deserialization. Enabling it
3536
* protects against this but has significant performance impact.
37+
* @param useJavaDurationConversion Default: false. Whether to use [java.time.Duration] as a bridge for [kotlin.time.Duration].
38+
* This allows use Kotlin Duration type with [com.fasterxml.jackson.datatype.jsr310.JavaTimeModule].
3639
*/
3740
class KotlinModule @Deprecated(
3841
level = DeprecationLevel.WARNING,
@@ -55,7 +58,8 @@ class KotlinModule @Deprecated(
5558
val nullIsSameAsDefault: Boolean = false,
5659
val singletonSupport: SingletonSupport = DISABLED,
5760
val strictNullChecks: Boolean = false,
58-
val useKotlinPropertyNameForGetter: Boolean = false
61+
val useKotlinPropertyNameForGetter: Boolean = false,
62+
val useJavaDurationConversion: Boolean = false,
5963
) : SimpleModule(KotlinModule::class.java.name, PackageVersion.VERSION) {
6064
init {
6165
if (!KotlinVersion.CURRENT.isAtLeast(1, 5)) {
@@ -105,7 +109,8 @@ class KotlinModule @Deprecated(
105109
else -> DISABLED
106110
},
107111
builder.isEnabled(StrictNullChecks),
108-
builder.isEnabled(KotlinPropertyNameAsImplicitName)
112+
builder.isEnabled(KotlinPropertyNameAsImplicitName),
113+
builder.isEnabled(UseJavaDurationConversion),
109114
)
110115

111116
companion object {
@@ -132,7 +137,14 @@ class KotlinModule @Deprecated(
132137
}
133138
}
134139

135-
context.insertAnnotationIntrospector(KotlinAnnotationIntrospector(context, cache, nullToEmptyCollection, nullToEmptyMap, nullIsSameAsDefault))
140+
context.insertAnnotationIntrospector(KotlinAnnotationIntrospector(
141+
context,
142+
cache,
143+
nullToEmptyCollection,
144+
nullToEmptyMap,
145+
nullIsSameAsDefault,
146+
useJavaDurationConversion
147+
))
136148
context.appendAnnotationIntrospector(
137149
KotlinNamesAnnotationIntrospector(
138150
this,
@@ -141,7 +153,7 @@ class KotlinModule @Deprecated(
141153
useKotlinPropertyNameForGetter)
142154
)
143155

144-
context.addDeserializers(KotlinDeserializers())
156+
context.addDeserializers(KotlinDeserializers(useJavaDurationConversion))
145157
context.addKeyDeserializers(KotlinKeyDeserializers)
146158
context.addSerializers(KotlinSerializers())
147159
context.addKeySerializers(KotlinKeySerializers())

0 commit comments

Comments
 (0)