Skip to content

Commit

Permalink
Merge pull request #211 from ProjectMapK/fixes
Browse files Browse the repository at this point in the history
Tweaks related to `value class` support in deserialization
  • Loading branch information
k163377 authored Jan 21, 2024
2 parents 1490e6a + 9327444 commit a2e9042
Show file tree
Hide file tree
Showing 9 changed files with 45 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
package io.github.projectmapk.jackson.module.kogera.deser;

import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import io.github.projectmapk.jackson.module.kogera.deser.deserializers.ValueClassBoxDeserializer;
import io.github.projectmapk.jackson.module.kogera.deser.deserializers.WrapsNullableValueClassBoxDeserializer;
import kotlin.jvm.JvmClassMappingKt;
import kotlin.reflect.KClass;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;

/**
* An interface to be inherited by JsonDeserializer that handles value classes that may wrap nullable.
* @see ValueClassBoxDeserializer for implementation.
* @see WrapsNullableValueClassBoxDeserializer for implementation.
*/
// To ensure maximum compatibility with StdDeserializer, this class is defined in Java.
public abstract class ValueClassDeserializer<D> extends StdDeserializer<D> {
protected ValueClassDeserializer(@NotNull KClass<?> vc) {
// To ensure maximum compatibility with StdDeserializer, this class is written in Java.
public abstract class WrapsNullableValueClassDeserializer<D> extends StdDeserializer<D> {
protected WrapsNullableValueClassDeserializer(@NotNull KClass<?> vc) {
super(JvmClassMappingKt.getJavaClass(vc));
}

protected ValueClassDeserializer(@NotNull Class<?> vc) {
protected WrapsNullableValueClassDeserializer(@NotNull Class<?> vc) {
super(vc);
}

protected ValueClassDeserializer(@NotNull JavaType valueType) {
protected WrapsNullableValueClassDeserializer(@NotNull JavaType valueType) {
super(valueType);
}

protected ValueClassDeserializer(@NotNull StdDeserializer<D> src) {
protected WrapsNullableValueClassDeserializer(@NotNull StdDeserializer<D> src) {
super(src);
}

Expand All @@ -44,4 +49,8 @@ public final Class<D> handledType() {
// It is defined so that null can also be returned so that Nulls.SKIP can be applied.
@Nullable
public abstract D getBoxedNullValue();

@Override
public abstract D deserialize(@NotNull JsonParser p, @NotNull DeserializationContext ctxt)
throws IOException, JacksonException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ internal class ValueClassUnboxConverter<T : Any>(val valueClass: Class<T>) : Std
private val unboxMethod = valueClass.getDeclaredMethod("unbox-impl").apply {
if (!this.isAccessible) this.isAccessible = true
}
val unboxedClass: Class<*> = unboxMethod.returnType

override fun convert(value: T): Any? = unboxMethod.invoke(value)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import io.github.projectmapk.jackson.module.kogera.KotlinDuration
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
import io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter
import io.github.projectmapk.jackson.module.kogera.deser.JavaToKotlinDurationConverter
import io.github.projectmapk.jackson.module.kogera.deser.ValueClassDeserializer
import io.github.projectmapk.jackson.module.kogera.deser.WrapsNullableValueClassDeserializer
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 @@ -89,10 +89,10 @@ internal object ULongDeserializer : StdDeserializer<ULong>(ULong::class.java) {
ULongChecker.readWithRangeCheck(p, p.bigIntegerValue)
}

internal class ValueClassBoxDeserializer<S, D : Any>(
internal class WrapsNullableValueClassBoxDeserializer<S, D : Any>(
private val creator: Method,
private val converter: ValueClassBoxConverter<S, D>
) : ValueClassDeserializer<D>(converter.boxedClass) {
) : WrapsNullableValueClassDeserializer<D>(converter.boxedClass) {
private val inputType: Class<*> = creator.parameterTypes[0]

init {
Expand Down Expand Up @@ -169,9 +169,9 @@ internal class KotlinDeserializers(
rawClass == KotlinDuration::class.java ->
JavaToKotlinDurationConverter.takeIf { useJavaDurationConversion }?.delegatingDeserializer
rawClass.isUnboxableValueClass() -> findValueCreator(type, rawClass, cache.getJmClass(rawClass)!!)?.let {
val unboxedClass = cache.getValueClassUnboxConverter(rawClass).unboxedClass
val unboxedClass = it.returnType
val converter = cache.getValueClassBoxConverter(unboxedClass, rawClass)
ValueClassBoxDeserializer(it, converter)
WrapsNullableValueClassBoxDeserializer(it, converter)
}
else -> null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator
import com.fasterxml.jackson.databind.exc.InvalidNullException
import com.fasterxml.jackson.databind.module.SimpleValueInstantiators
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
import io.github.projectmapk.jackson.module.kogera.deser.ValueClassDeserializer
import io.github.projectmapk.jackson.module.kogera.deser.WrapsNullableValueClassDeserializer
import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator.ConstructorValueCreator
import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator.MethodValueCreator
import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator.ValueCreator
Expand Down Expand Up @@ -47,7 +47,7 @@ internal class KotlinValueInstantiator(
isNullableParam: Boolean,
valueDeserializer: JsonDeserializer<*>?
): Boolean = !isNullableParam &&
valueDeserializer is ValueClassDeserializer<*> &&
valueDeserializer is WrapsNullableValueClassDeserializer<*> &&
cache.getJmClass(valueDeserializer.handledType())!!.wrapsNullValueClass()

private val valueCreator: ValueCreator<*>? by ReflectProperties.lazySoft {
Expand Down Expand Up @@ -83,7 +83,7 @@ internal class KotlinValueInstantiator(
// Deserializer.getNullValue could not be used because there is no way to get and parse parameters
// from the BeanDescription and using AnnotationIntrospector would override user customization.
if (requireValueClassSpecialNullValue(paramDef.isNullable, valueDeserializer)) {
(valueDeserializer as ValueClassDeserializer<*>).boxedNullValue?.let { return@run it }
(valueDeserializer as WrapsNullableValueClassDeserializer<*>).boxedNullValue?.let { return@run it }
}

if (jsonProp.skipNulls() && paramDef.isOptional) return@forEachIndexed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.exc.ValueInstantiationException
import io.github.projectmapk.jackson.module.kogera.deser.ValueClassDeserializer
import io.github.projectmapk.jackson.module.kogera.deser.WrapsNullableValueClassDeserializer
import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper
import io.github.projectmapk.jackson.module.kogera.readValue
import org.junit.jupiter.api.Assertions.assertEquals
Expand Down Expand Up @@ -42,7 +42,7 @@ class NullableObjectEdgeCases {
assertEquals(NullValue(NullValueDeserializer.nv, NullValueDeserializer.nv), result)
}

class NullsSkipDeserializer : ValueClassDeserializer<VC>(VC::class) {
class NullsSkipDeserializerWrapsNullable : WrapsNullableValueClassDeserializer<VC>(VC::class) {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): VC {
TODO("Not yet implemented")
}
Expand All @@ -52,7 +52,7 @@ class NullableObjectEdgeCases {

data class NullsSkip(
@field:JsonSetter(nulls = Nulls.SKIP)
@field:JsonDeserialize(using = NullsSkipDeserializer::class)
@field:JsonDeserialize(using = NullsSkipDeserializerWrapsNullable::class)
val nn: VC = VC("skip"),
@field:JsonSetter(nulls = Nulls.SKIP)
val n: VC? = VC("skip")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClas
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import io.github.projectmapk.jackson.module.kogera.deser.ValueClassDeserializer
import io.github.projectmapk.jackson.module.kogera.deser.WrapsNullableValueClassDeserializer

@JvmInline
value class Primitive(val v: Int) {
Expand All @@ -22,7 +22,7 @@ value class NonNullObject(val v: String) {

@JvmInline
value class NullableObject(val v: String?) {
class Deserializer : ValueClassDeserializer<NullableObject>(NullableObject::class) {
class DeserializerWrapsNullable : WrapsNullableValueClassDeserializer<NullableObject>(NullableObject::class) {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): NullableObject =
NullableObject(p.valueAsString + "-deser")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class SpecifiedForObjectMapperTest {
val module = SimpleModule().apply {
this.addDeserializer(Primitive::class.java, Primitive.Deserializer())
this.addDeserializer(NonNullObject::class.java, NonNullObject.Deserializer())
this.addDeserializer(NullableObject::class.java, NullableObject.Deserializer())
this.addDeserializer(NullableObject::class.java, NullableObject.DeserializerWrapsNullable())
}
this.registerModule(module)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ class NullableObjectTest {
}

data class NonNull(
@get:JsonDeserialize(using = NullableObject.Deserializer::class)
@get:JsonDeserialize(using = NullableObject.DeserializerWrapsNullable::class)
val getterAnn: NullableObject,
@field:JsonDeserialize(using = NullableObject.Deserializer::class)
@field:JsonDeserialize(using = NullableObject.DeserializerWrapsNullable::class)
val fieldAnn: NullableObject
)

Expand All @@ -34,9 +34,9 @@ class NullableObjectTest {
}

data class Nullable(
@get:JsonDeserialize(using = NullableObject.Deserializer::class)
@get:JsonDeserialize(using = NullableObject.DeserializerWrapsNullable::class)
val getterAnn: NullableObject?,
@field:JsonDeserialize(using = NullableObject.Deserializer::class)
@field:JsonDeserialize(using = NullableObject.DeserializerWrapsNullable::class)
val fieldAnn: NullableObject?
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,38 @@ import org.junit.jupiter.api.Test
// Test for Creator that can be handled by the Jackson mechanism.
class HandledByJacksonTest {
@JvmInline
value class PrimitiveNullableCreator(val value: Int) {
value class PrimitiveMultiParamCreator(val value: Int) {
companion object {
// Avoiding unboxing by making the return value of Creator nullable
@JvmStatic
@JsonCreator
fun creator(first: Int, second: Int): PrimitiveNullableCreator? =
PrimitiveNullableCreator(first + second)
fun creator(first: Int, second: Int): PrimitiveMultiParamCreator? =
PrimitiveMultiParamCreator(first + second)
}
}

@Test
fun primitiveNullableCreatorTest() {
val mapper = jacksonObjectMapper()
val r: PrimitiveNullableCreator = mapper.readValue("""{"first":1,"second":2}""")
assertEquals(PrimitiveNullableCreator(3), r)
val r: PrimitiveMultiParamCreator = mapper.readValue("""{"first":1,"second":2}""")
assertEquals(PrimitiveMultiParamCreator(3), r)
}

@JvmInline
value class NullableObjectNullableCreator(val value: Int?) {
value class NullableObjectMiltiParamCreator(val value: Int?) {
companion object {
// Avoiding unboxing by making the return value of Creator nullable
@JvmStatic
@JsonCreator
fun creator(first: Int, second: Int): NullableObjectNullableCreator? =
NullableObjectNullableCreator(first + second)
fun creator(first: Int, second: Int): NullableObjectMiltiParamCreator? =
NullableObjectMiltiParamCreator(first + second)
}
}

@Test
fun nullableObjectNullableCreatorTest() {
val mapper = jacksonObjectMapper()
val r: NullableObjectNullableCreator = mapper.readValue("""{"first":1,"second":2}""")
assertEquals(NullableObjectNullableCreator(3), r)
val r: NullableObjectMiltiParamCreator = mapper.readValue("""{"first":1,"second":2}""")
assertEquals(NullableObjectMiltiParamCreator(3), r)
}
}

0 comments on commit a2e9042

Please sign in to comment.