From d61d5c7edf4206b236d06a85e8c8a7d7e593c6d4 Mon Sep 17 00:00:00 2001 From: apatrida Date: Sat, 4 Jun 2016 00:31:54 -0300 Subject: [PATCH] fixes #26 and #29 --- .../jackson/module/kotlin/KotlinModule.kt | 2 + .../module/kotlin/KotlinValueInstantiator.kt | 104 ++++++++++++++++++ .../jackson/module/kotlin/test/Github26.kt | 27 +++++ .../jackson/module/kotlin/test/Github29.kt | 20 ++++ .../module/kotlin/test/ParameterNameTests.kt | 8 +- 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt create mode 100644 src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/Github26.kt create mode 100644 src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/Github29.kt diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt index 37d40384..b4a557c7 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt @@ -31,6 +31,8 @@ class KotlinModule() : SimpleModule(PackageVersion.VERSION) { override fun setupModule(context: SetupContext) { super.setupModule(context) + context.addValueInstantiators(KotlinInstantiators()); + fun addMixin(clazz: Class<*>, mixin: Class<*>) { impliedClasses.add(clazz) context.setMixInAnnotations(clazz, mixin) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt new file mode 100644 index 00000000..c5166ece --- /dev/null +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt @@ -0,0 +1,104 @@ +package com.fasterxml.jackson.module.kotlin + +import com.fasterxml.jackson.databind.BeanDescription +import com.fasterxml.jackson.databind.DeserializationConfig +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonMappingException +import com.fasterxml.jackson.databind.deser.SettableBeanProperty +import com.fasterxml.jackson.databind.deser.ValueInstantiator +import com.fasterxml.jackson.databind.deser.ValueInstantiators +import com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer +import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator +import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor +import com.fasterxml.jackson.databind.introspect.AnnotatedMethod +import java.lang.reflect.Constructor +import java.lang.reflect.Method +import kotlin.reflect.KParameter +import kotlin.reflect.jvm.isAccessible +import kotlin.reflect.jvm.kotlinFunction + +class KotlinValueInstantiator(src: StdValueInstantiator) : StdValueInstantiator(src) { + @Suppress("UNCHECKED_CAST") + override fun createFromObjectWith(ctxt: DeserializationContext, props: Array, buffer: PropertyValueBuffer): Any? { + val callable = when (_withArgsCreator) { + is AnnotatedConstructor -> (_withArgsCreator.annotated as Constructor).kotlinFunction + is AnnotatedMethod -> (_withArgsCreator.annotated as Method).kotlinFunction + else -> throw IllegalStateException("Expected a construtor or method to create a Kotlin object, instead found ${_withArgsCreator.annotated.javaClass.name}") + } ?: return super.createFromObjectWith(ctxt, props, buffer) // we cannot reflect this method so do the default Java-ish behavior + + val jsonParmValueList = buffer.getParameters(props) // properties in order, null for missing or actual nulled parameters + + // quick short circuit for special handling for no null checks needed and no optional parameters + if (jsonParmValueList.none { it == null } && callable.parameters.none { it.isOptional }) { + return super.createFromObjectWith(ctxt, props, buffer) + } + + val callableParametersByName = hashMapOf() + + callable.parameters.forEachIndexed { idx, paramDef -> + if (paramDef.kind == KParameter.Kind.INSTANCE || paramDef.kind == KParameter.Kind.EXTENSION_RECEIVER) { + // we shouldn't have an instance or receiver parameter and if we do, just go with default Java-ish behavior + return super.createFromObjectWith(ctxt, props, buffer) + } else { + val jsonProp = props.get(idx) + val isMissing = !buffer.hasParameter(jsonProp) + val paramVal = jsonParmValueList.get(idx) + + if (isMissing) { + if (paramDef.isOptional) { + // this is ok, optional parameter not resolved will have default parameter value of method + } else if (paramVal == null) { + if (paramDef.type.isMarkedNullable) { + // null value for nullable type, is ok + callableParametersByName.put(paramDef, null) + } else { + // missing value coming in as null for non-nullable type + throw JsonMappingException(null, "Instantiation of " + this.getValueTypeDesc() + " value failed for JSON property ${jsonProp.name} due to missing (therefore NULL) value for creator parameter ${paramDef.name} which is a non-nullable type") + } + } else { + // default value for datatype for non nullable type, is ok + callableParametersByName.put(paramDef, paramVal) + } + } else { + if (paramVal == null && !paramDef.type.isMarkedNullable) { + // value coming in as null for non-nullable type + throw JsonMappingException(null, "Instantiation of " + this.getValueTypeDesc() + " value failed for JSON property ${jsonProp.name} due to NULL value for creator parameter ${paramDef.name} which is a non-nullable type") + } else { + // value present, and can be set + callableParametersByName.put(paramDef, paramVal) + } + } + } + } + + + return if (callableParametersByName.size == jsonParmValueList.size) { + // we didn't do anything special with default parameters, do a normal call + super.createFromObjectWith(ctxt, props, buffer) + } else { + callable.isAccessible = true + callable.callBy(callableParametersByName) + } + + } + + override fun createFromObjectWith(ctxt: DeserializationContext, args: Array): Any { + return super.createFromObjectWith(ctxt, args) + } + +} + +class KotlinInstantiators : ValueInstantiators { + override fun findValueInstantiator(deserConfig: DeserializationConfig, beanDescriptor: BeanDescription, defaultInstantiator: ValueInstantiator): ValueInstantiator { + return if (beanDescriptor.beanClass.isKotlinClass()) { + if (defaultInstantiator is StdValueInstantiator) { + KotlinValueInstantiator(defaultInstantiator) + } else { + // TODO: return defaultInstantiator and let default method parameters and nullability go unused? or die with exception: + throw IllegalStateException("KotlinValueInstantiator requires that the default ValueInstantiator is StdValueInstantiator") + } + } else { + defaultInstantiator + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/Github26.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/Github26.kt new file mode 100644 index 00000000..406670d5 --- /dev/null +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/Github26.kt @@ -0,0 +1,27 @@ +package com.fasterxml.jackson.module.kotlin.test + +import com.fasterxml.jackson.annotation.JsonValue +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.junit.Test +import kotlin.test.assertEquals + +data class ClassWithPrimitivesWithDefaults(val i: Int = 5, val x: Int) + +class TestGithub26 { + @Test fun testConstructorWithPrimitiveTypesDefaultedExplicitlyAndImplicitly() { + val check1: ClassWithPrimitivesWithDefaults = jacksonObjectMapper().readValue("""{"i":3,"x":2}""") + assertEquals(3, check1.i) + assertEquals(2, check1.x) + + val check2: ClassWithPrimitivesWithDefaults = jacksonObjectMapper().readValue("""{}""") + assertEquals(5, check2.i) + assertEquals(0, check2.x) + + val check3: ClassWithPrimitivesWithDefaults = jacksonObjectMapper().readValue("""{"i": 2}""") + assertEquals(2, check3.i) + assertEquals(0, check3.x) + + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/Github29.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/Github29.kt new file mode 100644 index 00000000..bf00f582 --- /dev/null +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/Github29.kt @@ -0,0 +1,20 @@ +package com.fasterxml.jackson.module.kotlin.test + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.junit.Test +import kotlin.test.assertEquals + +data class Github29TestObj(val name: String, val other: String = "test") + +class TestGithub29 { + @Test fun testDefaultValuesInDeser() { + val check1: Github29TestObj = jacksonObjectMapper().readValue("""{"name": "bla"}""") + assertEquals("bla", check1.name) + assertEquals("test", check1.other) + + val check2: Github29TestObj = jacksonObjectMapper().readValue("""{"name": "bla", "other": "fish"}""") + assertEquals("bla", check2.name) + assertEquals("fish", check2.other) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/ParameterNameTests.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/ParameterNameTests.kt index c61043de..315b8e56 100644 --- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/ParameterNameTests.kt +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/ParameterNameTests.kt @@ -137,7 +137,13 @@ class TestJacksonWithKotlin { // ================== - private class StateObjectAsDataClassConfusingConstructor constructor (@Suppress("UNUSED_PARAMETER") nonField: String?, override val name: String, @Suppress("UNUSED_PARAMETER") yearOfBirth: Int, override val age: Int, override val primaryAddress: String, @JsonProperty("renamed") override val wrongName: Boolean, override val createdDt: DateTime) : TestFields + private class StateObjectAsDataClassConfusingConstructor constructor (@Suppress("UNUSED_PARAMETER") nonField: String?, + override val name: String, + @Suppress("UNUSED_PARAMETER") yearOfBirth: Int, + override val age: Int, + override val primaryAddress: String, + @JsonProperty("renamed") override val wrongName: Boolean, + override val createdDt: DateTime) : TestFields @Test fun testDataClassWithNonFieldParametersInConstructor() { // data class with non fields appearing as parameters in constructor, this works but null values or defaults for primitive types are passed to