Skip to content

Commit

Permalink
fixes #26 and #29
Browse files Browse the repository at this point in the history
  • Loading branch information
apatrida committed Jun 4, 2016
1 parent 1770ef2 commit d61d5c7
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<out SettableBeanProperty>, buffer: PropertyValueBuffer): Any? {
val callable = when (_withArgsCreator) {
is AnnotatedConstructor -> (_withArgsCreator.annotated as Constructor<Any>).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<KParameter, Any?>()

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<out Any>): 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
}
}
}
Original file line number Diff line number Diff line change
@@ -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)

}

}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit d61d5c7

Please sign in to comment.