Skip to content

Commit deb88d1

Browse files
authored
Merge pull request #131 from ProjectMapK/port-689
Porting Kotlin Duration Support
2 parents 7d9dbcd + 58a1276 commit deb88d1

File tree

9 files changed

+296
-12
lines changed

9 files changed

+296
-12
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies {
3333
testImplementation("io.mockk:mockk:1.13.3")
3434

3535
testImplementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
36+
testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
3637
}
3738

3839
kotlin {

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/InternalCommons.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import java.lang.reflect.Constructor
1414
import java.lang.reflect.Field
1515
import java.lang.reflect.Method
1616

17+
internal typealias JavaDuration = java.time.Duration
18+
internal typealias KotlinDuration = kotlin.time.Duration
19+
1720
internal fun Class<*>.isUnboxableValueClass() = this.getAnnotation(JvmInline::class.java) != null
1821

1922
internal fun Class<*>.toKmClass(): KmClass? = this.getAnnotation(Metadata::class.java)

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/KotlinFeature.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,16 @@ public enum class KotlinFeature(internal val enabledByDefault: Boolean) {
7373
*
7474
* @see KotlinClassIntrospector
7575
*/
76-
CopySyntheticConstructorParameterAnnotations(enabledByDefault = false);
76+
CopySyntheticConstructorParameterAnnotations(enabledByDefault = false),
77+
78+
/**
79+
* This feature represents whether to handle [kotlin.time.Duration] using [java.time.Duration] as conversion bridge.
80+
*
81+
* This allows use Kotlin Duration type with [com.fasterxml.jackson.datatype.jsr310.JavaTimeModule].
82+
* `@JsonFormat` annotations need to be declared either on getter using `@get:JsonFormat` or field using `@field:JsonFormat`.
83+
* See [jackson-module-kotlin#651] for details.
84+
*/
85+
UseJavaDurationConversion(enabledByDefault = false);
7786

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

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/KotlinModule.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import io.github.projectmapk.jackson.module.kogera.KotlinFeature.NullToEmptyColl
88
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.NullToEmptyMap
99
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.SingletonSupport
1010
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.StrictNullChecks
11+
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.UseJavaDurationConversion
1112
import io.github.projectmapk.jackson.module.kogera.annotation_introspector.KotlinFallbackAnnotationIntrospector
1213
import io.github.projectmapk.jackson.module.kogera.annotation_introspector.KotlinPrimaryAnnotationIntrospector
1314
import io.github.projectmapk.jackson.module.kogera.deser.deserializers.KotlinDeserializers
@@ -32,6 +33,8 @@ import java.util.*
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
// Do not delete default arguments,
3740
// as this will cause an error during initialization by Spring's Jackson2ObjectMapperBuilder.
@@ -43,7 +46,8 @@ public class KotlinModule private constructor(
4346
public val singletonSupport: Boolean = SingletonSupport.enabledByDefault,
4447
public val strictNullChecks: Boolean = StrictNullChecks.enabledByDefault,
4548
public val copySyntheticConstructorParameterAnnotations: Boolean =
46-
CopySyntheticConstructorParameterAnnotations.enabledByDefault
49+
CopySyntheticConstructorParameterAnnotations.enabledByDefault,
50+
public val useJavaDurationConversion: Boolean = UseJavaDurationConversion.enabledByDefault
4751
) : SimpleModule(KotlinModule::class.java.name, kogeraVersion) { // kogeraVersion is generated by building.
4852
private constructor(builder: Builder) : this(
4953
builder.reflectionCacheSize,
@@ -52,7 +56,8 @@ public class KotlinModule private constructor(
5256
builder.isEnabled(NullIsSameAsDefault),
5357
builder.isEnabled(SingletonSupport),
5458
builder.isEnabled(StrictNullChecks),
55-
builder.isEnabled(CopySyntheticConstructorParameterAnnotations)
59+
builder.isEnabled(CopySyntheticConstructorParameterAnnotations),
60+
builder.isEnabled(UseJavaDurationConversion)
5661
)
5762

5863
@Deprecated(
@@ -87,13 +92,15 @@ public class KotlinModule private constructor(
8792
context.insertAnnotationIntrospector(
8893
KotlinPrimaryAnnotationIntrospector(nullToEmptyCollection, nullToEmptyMap, cache)
8994
)
90-
context.appendAnnotationIntrospector(KotlinFallbackAnnotationIntrospector(strictNullChecks, cache))
95+
context.appendAnnotationIntrospector(
96+
KotlinFallbackAnnotationIntrospector(strictNullChecks, useJavaDurationConversion, cache)
97+
)
9198

9299
if (copySyntheticConstructorParameterAnnotations) {
93100
context.setClassIntrospector(KotlinClassIntrospector)
94101
}
95102

96-
context.addDeserializers(KotlinDeserializers(cache))
103+
context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion))
97104
context.addKeyDeserializers(KotlinKeyDeserializers)
98105
context.addSerializers(KotlinSerializers())
99106
context.addKeySerializers(KotlinKeySerializers())

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotation_introspector/KotlinFallbackAnnotationIntrospector.kt

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ import com.fasterxml.jackson.databind.introspect.AnnotatedParameter
1111
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector
1212
import com.fasterxml.jackson.databind.type.TypeFactory
1313
import com.fasterxml.jackson.databind.util.Converter
14+
import io.github.projectmapk.jackson.module.kogera.KotlinDuration
1415
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
1516
import io.github.projectmapk.jackson.module.kogera.deser.CollectionValueStrictNullChecksConverter
1617
import io.github.projectmapk.jackson.module.kogera.deser.MapValueStrictNullChecksConverter
1718
import io.github.projectmapk.jackson.module.kogera.deser.ValueClassUnboxConverter
1819
import io.github.projectmapk.jackson.module.kogera.isNullable
1920
import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass
2021
import io.github.projectmapk.jackson.module.kogera.reconstructClassOrNull
22+
import io.github.projectmapk.jackson.module.kogera.ser.KotlinDurationValueToJavaDurationConverter
23+
import io.github.projectmapk.jackson.module.kogera.ser.KotlinToJavaDurationConverter
2124
import io.github.projectmapk.jackson.module.kogera.ser.SequenceToIteratorConverter
2225
import kotlinx.metadata.KmTypeProjection
2326
import kotlinx.metadata.KmValueParameter
@@ -31,6 +34,7 @@ import java.lang.reflect.Modifier
3134
// Original name: KotlinNamesAnnotationIntrospector
3235
internal class KotlinFallbackAnnotationIntrospector(
3336
private val strictNullChecks: Boolean,
37+
private val useJavaDurationConversion: Boolean,
3438
private val cache: ReflectionCache
3539
) : NopAnnotationIntrospector() {
3640
private fun findKotlinParameter(param: AnnotatedParameter): KmValueParameter? =
@@ -73,12 +77,24 @@ internal class KotlinFallbackAnnotationIntrospector(
7377

7478
override fun findSerializationConverter(a: Annotated): Converter<*, *>? = when (a) {
7579
// Find a converter to handle the case where the getter returns an unboxed value from the value class.
76-
is AnnotatedMethod -> cache.findValueClassReturnType(a)
77-
?.let { cache.getValueClassBoxConverter(a.rawReturnType, it) }
78-
is AnnotatedClass ->
79-
a
80-
.takeIf { Sequence::class.java.isAssignableFrom(it.rawType) }
81-
?.let { SequenceToIteratorConverter(it.type) }
80+
is AnnotatedMethod -> cache.findValueClassReturnType(a)?.let {
81+
if (useJavaDurationConversion && it == KotlinDuration::class.java) {
82+
if (a.rawReturnType == KotlinDuration::class.java) {
83+
KotlinToJavaDurationConverter
84+
} else {
85+
KotlinDurationValueToJavaDurationConverter
86+
}
87+
} else {
88+
cache.getValueClassBoxConverter(a.rawReturnType, it)
89+
}
90+
}
91+
is AnnotatedClass -> lookupKotlinTypeConverter(a)
92+
else -> null
93+
}
94+
95+
private fun lookupKotlinTypeConverter(a: AnnotatedClass) = when {
96+
Sequence::class.java.isAssignableFrom(a.rawType) -> SequenceToIteratorConverter(a.type)
97+
KotlinDuration::class.java == a.rawType -> KotlinToJavaDurationConverter.takeIf { useJavaDurationConversion }
8298
else -> null
8399
}
84100

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/Converters.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package io.github.projectmapk.jackson.module.kogera.deser
22

33
import com.fasterxml.jackson.databind.JavaType
4+
import com.fasterxml.jackson.databind.deser.std.StdDelegatingDeserializer
45
import com.fasterxml.jackson.databind.exc.MismatchedInputException
56
import com.fasterxml.jackson.databind.type.TypeFactory
67
import com.fasterxml.jackson.databind.util.Converter
78
import com.fasterxml.jackson.databind.util.StdConverter
9+
import io.github.projectmapk.jackson.module.kogera.JavaDuration
10+
import io.github.projectmapk.jackson.module.kogera.KotlinDuration
11+
import kotlin.time.toKotlinDuration
812

913
internal class ValueClassUnboxConverter<T : Any>(private val valueClass: Class<T>) : StdConverter<T, Any?>() {
1014
private val unboxMethod = valueClass.getDeclaredMethod("unbox-impl").apply {
@@ -73,3 +77,16 @@ internal class MapValueStrictNullChecksConverter(
7377
override fun getInputType(typeFactory: TypeFactory): JavaType = type
7478
override fun getOutputType(typeFactory: TypeFactory): JavaType = type
7579
}
80+
81+
/**
82+
* 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.
83+
*
84+
* @see [com.fasterxml.jackson.module.kotlin.test.DurationTests]
85+
*/
86+
internal object JavaToKotlinDurationConverter : StdConverter<JavaDuration, KotlinDuration>() {
87+
override fun convert(value: JavaDuration) = value.toKotlinDuration()
88+
89+
val delegatingDeserializer: StdDelegatingDeserializer<KotlinDuration> by lazy {
90+
StdDelegatingDeserializer(this)
91+
}
92+
}

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import com.fasterxml.jackson.databind.deser.Deserializers
1010
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
1111
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException
1212
import io.github.projectmapk.jackson.module.kogera.JmClass
13+
import io.github.projectmapk.jackson.module.kogera.KotlinDuration
1314
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
15+
import io.github.projectmapk.jackson.module.kogera.deser.JavaToKotlinDurationConverter
1416
import io.github.projectmapk.jackson.module.kogera.hasCreatorAnnotation
1517
import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass
1618
import io.github.projectmapk.jackson.module.kogera.toSignature
@@ -127,7 +129,10 @@ private fun findValueCreator(type: JavaType, clazz: Class<*>, jmClass: JmClass):
127129
return primaryConstructor
128130
}
129131

130-
internal class KotlinDeserializers(private val cache: ReflectionCache) : Deserializers.Base() {
132+
internal class KotlinDeserializers(
133+
private val cache: ReflectionCache,
134+
private val useJavaDurationConversion: Boolean
135+
) : Deserializers.Base() {
131136
override fun findBeanDeserializer(
132137
type: JavaType,
133138
config: DeserializationConfig?,
@@ -142,6 +147,8 @@ internal class KotlinDeserializers(private val cache: ReflectionCache) : Deseria
142147
rawClass == UShort::class.java -> UShortDeserializer
143148
rawClass == UInt::class.java -> UIntDeserializer
144149
rawClass == ULong::class.java -> ULongDeserializer
150+
rawClass == KotlinDuration::class.java ->
151+
JavaToKotlinDurationConverter.takeIf { useJavaDurationConversion }?.delegatingDeserializer
145152
rawClass.isUnboxableValueClass() -> findValueCreator(type, rawClass, cache.getJmClass(rawClass)!!)
146153
?.let { ValueClassBoxDeserializer(it, rawClass) }
147154
else -> null

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/Converters.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import com.fasterxml.jackson.databind.JavaType
44
import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer
55
import com.fasterxml.jackson.databind.type.TypeFactory
66
import com.fasterxml.jackson.databind.util.StdConverter
7+
import io.github.projectmapk.jackson.module.kogera.JavaDuration
8+
import io.github.projectmapk.jackson.module.kogera.KotlinDuration
9+
import kotlin.time.toJavaDuration
710

811
internal class SequenceToIteratorConverter(private val input: JavaType) : StdConverter<Sequence<*>, Iterator<*>>() {
912
override fun convert(value: Sequence<*>): Iterator<*> = value.iterator()
@@ -31,3 +34,13 @@ internal class ValueClassBoxConverter<S : Any?, D : Any>(
3134

3235
val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
3336
}
37+
38+
internal object KotlinDurationValueToJavaDurationConverter : StdConverter<Long, JavaDuration>() {
39+
private val boxConverter by lazy { ValueClassBoxConverter(Long::class.java, KotlinDuration::class.java) }
40+
41+
override fun convert(value: Long): JavaDuration = KotlinToJavaDurationConverter.convert(boxConverter.convert(value))
42+
}
43+
44+
internal object KotlinToJavaDurationConverter : StdConverter<KotlinDuration, JavaDuration>() {
45+
override fun convert(value: KotlinDuration) = value.toJavaDuration()
46+
}

0 commit comments

Comments
 (0)