Skip to content

Commit d61d5c7

Browse files
committed
fixes #26 and #29
1 parent 1770ef2 commit d61d5c7

File tree

5 files changed

+160
-1
lines changed

5 files changed

+160
-1
lines changed

src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class KotlinModule() : SimpleModule(PackageVersion.VERSION) {
3131
override fun setupModule(context: SetupContext) {
3232
super.setupModule(context)
3333

34+
context.addValueInstantiators(KotlinInstantiators());
35+
3436
fun addMixin(clazz: Class<*>, mixin: Class<*>) {
3537
impliedClasses.add(clazz)
3638
context.setMixInAnnotations(clazz, mixin)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.fasterxml.jackson.module.kotlin
2+
3+
import com.fasterxml.jackson.databind.BeanDescription
4+
import com.fasterxml.jackson.databind.DeserializationConfig
5+
import com.fasterxml.jackson.databind.DeserializationContext
6+
import com.fasterxml.jackson.databind.JsonMappingException
7+
import com.fasterxml.jackson.databind.deser.SettableBeanProperty
8+
import com.fasterxml.jackson.databind.deser.ValueInstantiator
9+
import com.fasterxml.jackson.databind.deser.ValueInstantiators
10+
import com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer
11+
import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator
12+
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor
13+
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod
14+
import java.lang.reflect.Constructor
15+
import java.lang.reflect.Method
16+
import kotlin.reflect.KParameter
17+
import kotlin.reflect.jvm.isAccessible
18+
import kotlin.reflect.jvm.kotlinFunction
19+
20+
class KotlinValueInstantiator(src: StdValueInstantiator) : StdValueInstantiator(src) {
21+
@Suppress("UNCHECKED_CAST")
22+
override fun createFromObjectWith(ctxt: DeserializationContext, props: Array<out SettableBeanProperty>, buffer: PropertyValueBuffer): Any? {
23+
val callable = when (_withArgsCreator) {
24+
is AnnotatedConstructor -> (_withArgsCreator.annotated as Constructor<Any>).kotlinFunction
25+
is AnnotatedMethod -> (_withArgsCreator.annotated as Method).kotlinFunction
26+
else -> throw IllegalStateException("Expected a construtor or method to create a Kotlin object, instead found ${_withArgsCreator.annotated.javaClass.name}")
27+
} ?: return super.createFromObjectWith(ctxt, props, buffer) // we cannot reflect this method so do the default Java-ish behavior
28+
29+
val jsonParmValueList = buffer.getParameters(props) // properties in order, null for missing or actual nulled parameters
30+
31+
// quick short circuit for special handling for no null checks needed and no optional parameters
32+
if (jsonParmValueList.none { it == null } && callable.parameters.none { it.isOptional }) {
33+
return super.createFromObjectWith(ctxt, props, buffer)
34+
}
35+
36+
val callableParametersByName = hashMapOf<KParameter, Any?>()
37+
38+
callable.parameters.forEachIndexed { idx, paramDef ->
39+
if (paramDef.kind == KParameter.Kind.INSTANCE || paramDef.kind == KParameter.Kind.EXTENSION_RECEIVER) {
40+
// we shouldn't have an instance or receiver parameter and if we do, just go with default Java-ish behavior
41+
return super.createFromObjectWith(ctxt, props, buffer)
42+
} else {
43+
val jsonProp = props.get(idx)
44+
val isMissing = !buffer.hasParameter(jsonProp)
45+
val paramVal = jsonParmValueList.get(idx)
46+
47+
if (isMissing) {
48+
if (paramDef.isOptional) {
49+
// this is ok, optional parameter not resolved will have default parameter value of method
50+
} else if (paramVal == null) {
51+
if (paramDef.type.isMarkedNullable) {
52+
// null value for nullable type, is ok
53+
callableParametersByName.put(paramDef, null)
54+
} else {
55+
// missing value coming in as null for non-nullable type
56+
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")
57+
}
58+
} else {
59+
// default value for datatype for non nullable type, is ok
60+
callableParametersByName.put(paramDef, paramVal)
61+
}
62+
} else {
63+
if (paramVal == null && !paramDef.type.isMarkedNullable) {
64+
// value coming in as null for non-nullable type
65+
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")
66+
} else {
67+
// value present, and can be set
68+
callableParametersByName.put(paramDef, paramVal)
69+
}
70+
}
71+
}
72+
}
73+
74+
75+
return if (callableParametersByName.size == jsonParmValueList.size) {
76+
// we didn't do anything special with default parameters, do a normal call
77+
super.createFromObjectWith(ctxt, props, buffer)
78+
} else {
79+
callable.isAccessible = true
80+
callable.callBy(callableParametersByName)
81+
}
82+
83+
}
84+
85+
override fun createFromObjectWith(ctxt: DeserializationContext, args: Array<out Any>): Any {
86+
return super.createFromObjectWith(ctxt, args)
87+
}
88+
89+
}
90+
91+
class KotlinInstantiators : ValueInstantiators {
92+
override fun findValueInstantiator(deserConfig: DeserializationConfig, beanDescriptor: BeanDescription, defaultInstantiator: ValueInstantiator): ValueInstantiator {
93+
return if (beanDescriptor.beanClass.isKotlinClass()) {
94+
if (defaultInstantiator is StdValueInstantiator) {
95+
KotlinValueInstantiator(defaultInstantiator)
96+
} else {
97+
// TODO: return defaultInstantiator and let default method parameters and nullability go unused? or die with exception:
98+
throw IllegalStateException("KotlinValueInstantiator requires that the default ValueInstantiator is StdValueInstantiator")
99+
}
100+
} else {
101+
defaultInstantiator
102+
}
103+
}
104+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.fasterxml.jackson.module.kotlin.test
2+
3+
import com.fasterxml.jackson.annotation.JsonValue
4+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
5+
import com.fasterxml.jackson.module.kotlin.readValue
6+
import org.junit.Test
7+
import kotlin.test.assertEquals
8+
9+
data class ClassWithPrimitivesWithDefaults(val i: Int = 5, val x: Int)
10+
11+
class TestGithub26 {
12+
@Test fun testConstructorWithPrimitiveTypesDefaultedExplicitlyAndImplicitly() {
13+
val check1: ClassWithPrimitivesWithDefaults = jacksonObjectMapper().readValue("""{"i":3,"x":2}""")
14+
assertEquals(3, check1.i)
15+
assertEquals(2, check1.x)
16+
17+
val check2: ClassWithPrimitivesWithDefaults = jacksonObjectMapper().readValue("""{}""")
18+
assertEquals(5, check2.i)
19+
assertEquals(0, check2.x)
20+
21+
val check3: ClassWithPrimitivesWithDefaults = jacksonObjectMapper().readValue("""{"i": 2}""")
22+
assertEquals(2, check3.i)
23+
assertEquals(0, check3.x)
24+
25+
}
26+
27+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.fasterxml.jackson.module.kotlin.test
2+
3+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
4+
import com.fasterxml.jackson.module.kotlin.readValue
5+
import org.junit.Test
6+
import kotlin.test.assertEquals
7+
8+
data class Github29TestObj(val name: String, val other: String = "test")
9+
10+
class TestGithub29 {
11+
@Test fun testDefaultValuesInDeser() {
12+
val check1: Github29TestObj = jacksonObjectMapper().readValue("""{"name": "bla"}""")
13+
assertEquals("bla", check1.name)
14+
assertEquals("test", check1.other)
15+
16+
val check2: Github29TestObj = jacksonObjectMapper().readValue("""{"name": "bla", "other": "fish"}""")
17+
assertEquals("bla", check2.name)
18+
assertEquals("fish", check2.other)
19+
}
20+
}

src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/ParameterNameTests.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,13 @@ class TestJacksonWithKotlin {
137137

138138
// ==================
139139

140-
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
140+
private class StateObjectAsDataClassConfusingConstructor constructor (@Suppress("UNUSED_PARAMETER") nonField: String?,
141+
override val name: String,
142+
@Suppress("UNUSED_PARAMETER") yearOfBirth: Int,
143+
override val age: Int,
144+
override val primaryAddress: String,
145+
@JsonProperty("renamed") override val wrongName: Boolean,
146+
override val createdDt: DateTime) : TestFields
141147

142148
@Test fun testDataClassWithNonFieldParametersInConstructor() {
143149
// data class with non fields appearing as parameters in constructor, this works but null values or defaults for primitive types are passed to

0 commit comments

Comments
 (0)