Skip to content
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
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies {
testImplementation("io.mockk:mockk:1.13.3")

testImplementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
}

kotlin {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import java.lang.reflect.Constructor
import java.lang.reflect.Field
import java.lang.reflect.Method

internal typealias JavaDuration = java.time.Duration
internal typealias KotlinDuration = kotlin.time.Duration

internal fun Class<*>.isUnboxableValueClass() = this.getAnnotation(JvmInline::class.java) != null

internal fun Class<*>.toKmClass(): KmClass? = this.getAnnotation(Metadata::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,16 @@ public enum class KotlinFeature(internal val enabledByDefault: Boolean) {
*
* @see KotlinClassIntrospector
*/
CopySyntheticConstructorParameterAnnotations(enabledByDefault = false);
CopySyntheticConstructorParameterAnnotations(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 io.github.projectmapk.jackson.module.kogera.KotlinFeature.NullToEmptyColl
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.NullToEmptyMap
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.SingletonSupport
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.StrictNullChecks
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.UseJavaDurationConversion
import io.github.projectmapk.jackson.module.kogera.annotation_introspector.KotlinFallbackAnnotationIntrospector
import io.github.projectmapk.jackson.module.kogera.annotation_introspector.KotlinPrimaryAnnotationIntrospector
import io.github.projectmapk.jackson.module.kogera.deser.deserializers.KotlinDeserializers
Expand All @@ -32,6 +33,8 @@ import java.util.*
* 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].
*/
// Do not delete default arguments,
// as this will cause an error during initialization by Spring's Jackson2ObjectMapperBuilder.
Expand All @@ -43,7 +46,8 @@ public class KotlinModule private constructor(
public val singletonSupport: Boolean = SingletonSupport.enabledByDefault,
public val strictNullChecks: Boolean = StrictNullChecks.enabledByDefault,
public val copySyntheticConstructorParameterAnnotations: Boolean =
CopySyntheticConstructorParameterAnnotations.enabledByDefault
CopySyntheticConstructorParameterAnnotations.enabledByDefault,
public val useJavaDurationConversion: Boolean = UseJavaDurationConversion.enabledByDefault
) : SimpleModule(KotlinModule::class.java.name, kogeraVersion) { // kogeraVersion is generated by building.
private constructor(builder: Builder) : this(
builder.reflectionCacheSize,
Expand All @@ -52,7 +56,8 @@ public class KotlinModule private constructor(
builder.isEnabled(NullIsSameAsDefault),
builder.isEnabled(SingletonSupport),
builder.isEnabled(StrictNullChecks),
builder.isEnabled(CopySyntheticConstructorParameterAnnotations)
builder.isEnabled(CopySyntheticConstructorParameterAnnotations),
builder.isEnabled(UseJavaDurationConversion)
)

@Deprecated(
Expand Down Expand Up @@ -87,13 +92,15 @@ public class KotlinModule private constructor(
context.insertAnnotationIntrospector(
KotlinPrimaryAnnotationIntrospector(nullToEmptyCollection, nullToEmptyMap, cache)
)
context.appendAnnotationIntrospector(KotlinFallbackAnnotationIntrospector(strictNullChecks, cache))
context.appendAnnotationIntrospector(
KotlinFallbackAnnotationIntrospector(strictNullChecks, useJavaDurationConversion, cache)
)

if (copySyntheticConstructorParameterAnnotations) {
context.setClassIntrospector(KotlinClassIntrospector)
}

context.addDeserializers(KotlinDeserializers(cache))
context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion))
context.addKeyDeserializers(KotlinKeyDeserializers)
context.addSerializers(KotlinSerializers())
context.addKeySerializers(KotlinKeySerializers())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import com.fasterxml.jackson.databind.introspect.AnnotatedParameter
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector
import com.fasterxml.jackson.databind.type.TypeFactory
import com.fasterxml.jackson.databind.util.Converter
import io.github.projectmapk.jackson.module.kogera.KotlinDuration
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
import io.github.projectmapk.jackson.module.kogera.deser.CollectionValueStrictNullChecksConverter
import io.github.projectmapk.jackson.module.kogera.deser.MapValueStrictNullChecksConverter
import io.github.projectmapk.jackson.module.kogera.deser.ValueClassUnboxConverter
import io.github.projectmapk.jackson.module.kogera.isNullable
import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass
import io.github.projectmapk.jackson.module.kogera.reconstructClassOrNull
import io.github.projectmapk.jackson.module.kogera.ser.KotlinDurationValueToJavaDurationConverter
import io.github.projectmapk.jackson.module.kogera.ser.KotlinToJavaDurationConverter
import io.github.projectmapk.jackson.module.kogera.ser.SequenceToIteratorConverter
import kotlinx.metadata.KmTypeProjection
import kotlinx.metadata.KmValueParameter
Expand All @@ -31,6 +34,7 @@ import java.lang.reflect.Modifier
// Original name: KotlinNamesAnnotationIntrospector
internal class KotlinFallbackAnnotationIntrospector(
private val strictNullChecks: Boolean,
private val useJavaDurationConversion: Boolean,
private val cache: ReflectionCache
) : NopAnnotationIntrospector() {
private fun findKotlinParameter(param: AnnotatedParameter): KmValueParameter? =
Expand Down Expand Up @@ -73,12 +77,24 @@ internal class KotlinFallbackAnnotationIntrospector(

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 -> cache.findValueClassReturnType(a)?.let {
if (useJavaDurationConversion && it == KotlinDuration::class.java) {
if (a.rawReturnType == KotlinDuration::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)
KotlinDuration::class.java == a.rawType -> KotlinToJavaDurationConverter.takeIf { useJavaDurationConversion }
else -> null
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package io.github.projectmapk.jackson.module.kogera.deser

import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.deser.std.StdDelegatingDeserializer
import com.fasterxml.jackson.databind.exc.MismatchedInputException
import com.fasterxml.jackson.databind.type.TypeFactory
import com.fasterxml.jackson.databind.util.Converter
import com.fasterxml.jackson.databind.util.StdConverter
import io.github.projectmapk.jackson.module.kogera.JavaDuration
import io.github.projectmapk.jackson.module.kogera.KotlinDuration
import kotlin.time.toKotlinDuration

internal class ValueClassUnboxConverter<T : Any>(private val valueClass: Class<T>) : StdConverter<T, Any?>() {
private val unboxMethod = valueClass.getDeclaredMethod("unbox-impl").apply {
Expand Down Expand Up @@ -73,3 +77,16 @@ internal class MapValueStrictNullChecksConverter(
override fun getInputType(typeFactory: TypeFactory): JavaType = type
override fun getOutputType(typeFactory: TypeFactory): JavaType = type
}

/**
* 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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import com.fasterxml.jackson.databind.deser.Deserializers
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException
import io.github.projectmapk.jackson.module.kogera.JmClass
import io.github.projectmapk.jackson.module.kogera.KotlinDuration
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
import io.github.projectmapk.jackson.module.kogera.deser.JavaToKotlinDurationConverter
import io.github.projectmapk.jackson.module.kogera.hasCreatorAnnotation
import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass
import io.github.projectmapk.jackson.module.kogera.toSignature
Expand Down Expand Up @@ -127,7 +129,10 @@ private fun findValueCreator(type: JavaType, clazz: Class<*>, jmClass: JmClass):
return primaryConstructor
}

internal class KotlinDeserializers(private val cache: ReflectionCache) : Deserializers.Base() {
internal class KotlinDeserializers(
private val cache: ReflectionCache,
private val useJavaDurationConversion: Boolean
) : Deserializers.Base() {
override fun findBeanDeserializer(
type: JavaType,
config: DeserializationConfig?,
Expand All @@ -142,6 +147,8 @@ internal class KotlinDeserializers(private val cache: ReflectionCache) : Deseria
rawClass == UShort::class.java -> UShortDeserializer
rawClass == UInt::class.java -> UIntDeserializer
rawClass == ULong::class.java -> ULongDeserializer
rawClass == KotlinDuration::class.java ->
JavaToKotlinDurationConverter.takeIf { useJavaDurationConversion }?.delegatingDeserializer
rawClass.isUnboxableValueClass() -> findValueCreator(type, rawClass, cache.getJmClass(rawClass)!!)
?.let { ValueClassBoxDeserializer(it, rawClass) }
else -> null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer
import com.fasterxml.jackson.databind.type.TypeFactory
import com.fasterxml.jackson.databind.util.StdConverter
import io.github.projectmapk.jackson.module.kogera.JavaDuration
import io.github.projectmapk.jackson.module.kogera.KotlinDuration
import kotlin.time.toJavaDuration

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

val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
}

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

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

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