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

General ClosedRange deserialization support #97

Merged
merged 3 commits into from
Apr 21, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/FixedIssues.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ A list of issues that have not been resolved in `jackson-module-kotlin`, but hav
- [`JsonSerializer` is enabled when the value is an Object type with non\-null value and the property definition is nullable. · Issue \#618](https://github.com/FasterXML/jackson-module-kotlin/issues/618)
- [About the problem that property names in \`Jackson\` and definitions in \`Kotlin\` are sometimes different\. · Issue \#630](https://github.com/FasterXML/jackson-module-kotlin/issues/630)
- [Annotation given to constructor parameters containing \`value class\` as argument does not work · Issue \#651](https://github.com/FasterXML/jackson-module-kotlin/issues/651)
- [How to deserialize a kotlin\.ranges\.ClosedRange<T> with Jackson · Issue \#663](https://github.com/FasterXML/jackson-module-kotlin/issues/663)

## Maybe fixed(verification required)
- [@JsonProperty is ignored on data class properties with names starting with "is" · Issue \#237](https://github.com/FasterXML/jackson-module-kotlin/issues/237)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ package com.fasterxml.jackson.module.kotlin.annotation_introspector
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.cfg.MapperConfig
import com.fasterxml.jackson.databind.introspect.Annotated
import com.fasterxml.jackson.databind.introspect.AnnotatedClass
import com.fasterxml.jackson.databind.introspect.AnnotatedMember
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod
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 com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.ReflectionCache
Expand Down Expand Up @@ -116,6 +119,54 @@ internal class KotlinFallbackAnnotationIntrospector(
?.takeIf { it.requireRebox() }
?.let { cache.getValueClassBoxConverter(am.rawReturnType, it).delegatingSerializer }
}

/*
* ClosedRange, which is not a concrete type like IntRange, does not have a type to deserialize to,
* so deserialization by ClosedRangeMixin does not work.
* Therefore, this process provides a concrete type.
*
* The target of processing is ClosedRange and interfaces or abstract classes that inherit from it.
* As of Kotlin 1.5.32, ClosedRange and ClosedFloatingPointRange are processed.
*/
override fun refineDeserializationType(config: MapperConfig<*>, a: Annotated, baseType: JavaType): JavaType {
return (a as? AnnotatedClass)
?.let { _ ->
a.rawType.apply {
if (this != ClosedRange::class.java && this != ClosedFloatingPointRange::class.java) return@let null
}

baseType.bindings.typeParameters.firstOrNull()
?.let { ClosedRangeHelpers.findClosedFloatingPointRangeRef(it.rawClass) }
?: ClosedRangeHelpers.comparableRangeClass?.let {
val factory = config.typeFactory
factory.constructParametricType(it, a.type.bindings)
}
} ?: baseType
}
}

// At present, it depends on the private class, but if it is made public, it must be switched to a direct reference.
// see https://youtrack.jetbrains.com/issue/KT-55376
internal object ClosedRangeHelpers {
val closedDoubleRangeRef: JavaType? by lazy {
runCatching { Class.forName("kotlin.ranges.ClosedDoubleRange") }.getOrNull()
?.let { TypeFactory.defaultInstance().constructType(it) }
}

val closedFloatRangeRef: JavaType? by lazy {
runCatching { Class.forName("kotlin.ranges.ClosedFloatRange") }.getOrNull()
?.let { TypeFactory.defaultInstance().constructType(it) }
}

fun findClosedFloatingPointRangeRef(contentType: Class<*>): JavaType? = when (contentType) {
Double::class.javaPrimitiveType, Double::class.javaObjectType -> closedDoubleRangeRef
Float::class.javaPrimitiveType, Float::class.javaObjectType -> closedFloatRangeRef
else -> null
}

val comparableRangeClass: Class<*>? by lazy {
runCatching { Class.forName("kotlin.ranges.ComparableRange") }.getOrNull()
}
}

private fun ValueParameter.createValueClassUnboxConverterOrNull(rawType: Class<*>): ValueClassUnboxConverter<*>? {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.fasterxml.jackson.module.kotlin._integration

import com.fasterxml.jackson.module.kotlin.annotation_introspector.ClosedRangeHelpers
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Test

class ClosedRangesTest {
companion object {
val mapper = jacksonObjectMapper()
}

@Test
fun intLikeRange() {
val src = IntRange(0, 1)
val json = mapper.writeValueAsString(src)
val result = mapper.readValue<IntRange>(json)

assertEquals(src, result)
}

@Test
fun closedDoubleRange() {
val src: ClosedFloatingPointRange<Double> = 0.0..1.0
val json = mapper.writeValueAsString(src)
val result = mapper.readValue<ClosedRange<Double>>(json)

assertEquals(src, result)
}

@Test
fun closedFloatRange() {
val src: ClosedFloatingPointRange<Float> = 0.0f..1.0f
val json = mapper.writeValueAsString(src)
val result = mapper.readValue<ClosedFloatingPointRange<Float>>(json)

assertEquals(src, result)
}

private data class Wrapper(val value: Int) : Comparable<Wrapper> {
override fun compareTo(other: Wrapper): Int = value.compareTo(other.value)
}

@Test
fun comparableRange() {
val src: ClosedRange<Wrapper> = Wrapper(0)..Wrapper(1)
val json = mapper.writeValueAsString(src)
val result = mapper.readValue<ClosedRange<Wrapper>>(json)

assertEquals(src, result)
}

@Test
fun loadClasses() {
assertNotNull(ClosedRangeHelpers.closedDoubleRangeRef)
assertNotNull(ClosedRangeHelpers.closedFloatRangeRef)
assertNotNull(ClosedRangeHelpers.comparableRangeClass)
}

@Test
fun findClosedFloatingPointRangeRefTest() {
assertEquals(
ClosedRangeHelpers.closedDoubleRangeRef,
ClosedRangeHelpers.findClosedFloatingPointRangeRef(Double::class.javaPrimitiveType!!)
)
assertEquals(
ClosedRangeHelpers.closedDoubleRangeRef,
ClosedRangeHelpers.findClosedFloatingPointRangeRef(Double::class.javaObjectType)
)

assertEquals(
ClosedRangeHelpers.closedFloatRangeRef,
ClosedRangeHelpers.findClosedFloatingPointRangeRef(Float::class.javaPrimitiveType!!)
)
assertEquals(
ClosedRangeHelpers.closedFloatRangeRef,
ClosedRangeHelpers.findClosedFloatingPointRangeRef(Float::class.javaObjectType)
)
}
}