Skip to content

Commit 5286933

Browse files
authored
Merge pull request #937 from k163377/399
Added type match check to read functions
2 parents 13ff7d6 + 0eb0cfc commit 5286933

File tree

8 files changed

+307
-15
lines changed

8 files changed

+307
-15
lines changed

release-notes/CREDITS-2.x

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ Contributors:
1717

1818
# 2.19.0 (not yet released)
1919

20+
WrongWrong (@k163377)
21+
* #937: Added type match check to read functions
22+
2023
Tatu Saloranta (@cowtowncoder)
2124
* #889: Upgrade kotlin dep to 1.9.25 (from 1.9.24)
2225

release-notes/VERSION-2.x

+8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ Co-maintainers:
1616
=== Releases ===
1717
------------------------------------------------------------------------
1818

19+
2.19.0 (not yet released)
20+
21+
#937: For `readValue` and other shorthands for `ObjectMapper` deserialization methods,
22+
type consistency checks have been added.
23+
A `RuntimeJsonMappingException` will be thrown in case of inconsistency.
24+
This fixes a problem that broke `Kotlin` null safety by reading null as a value even if the type parameter was specified as non-null.
25+
It also checks for custom errors in ObjectMapper that cause a different value to be read than the specified type parameter.
26+
1927
2.19.0-rc2 (07-Apr-2025)
2028

2129
#929: Added consideration of `JsonProperty.isRequired` added in `2.19` in `hasRequiredMarker` processing.

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

+119-7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.JsonSerializer
99
import com.fasterxml.jackson.databind.MappingIterator
1010
import com.fasterxml.jackson.databind.ObjectMapper
1111
import com.fasterxml.jackson.databind.ObjectReader
12+
import com.fasterxml.jackson.databind.RuntimeJsonMappingException
1213
import com.fasterxml.jackson.databind.json.JsonMapper
1314
import com.fasterxml.jackson.databind.module.SimpleModule
1415
import com.fasterxml.jackson.databind.node.ArrayNode
@@ -50,21 +51,132 @@ fun ObjectMapper.registerKotlinModule(initializer: KotlinModule.Builder.() -> Un
5051

5152
inline fun <reified T> jacksonTypeRef(): TypeReference<T> = object: TypeReference<T>() {}
5253

54+
/**
55+
* It is public due to Kotlin restrictions, but should not be used externally.
56+
*/
57+
inline fun <reified T> Any?.checkTypeMismatch(): T {
58+
// Basically, this check assumes that T is non-null and the value is null.
59+
// Since this can be caused by both input or ObjectMapper implementation errors,
60+
// a more abstract RuntimeJsonMappingException is thrown.
61+
if (this !is T) {
62+
val nullability = if (null is T) "?" else "(non-null)"
63+
64+
// Since the databind implementation of MappingIterator throws RuntimeJsonMappingException,
65+
// JsonMappingException was not used to unify the behavior.
66+
throw RuntimeJsonMappingException(
67+
"Deserialized value did not match the specified type; " +
68+
"specified ${T::class.qualifiedName}${nullability} but was ${this?.let { it::class.qualifiedName }}"
69+
)
70+
}
71+
return this
72+
}
73+
74+
/**
75+
* Shorthand for [ObjectMapper.readValue].
76+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
77+
* Other cases where the read value is of a different type than [T]
78+
* due to an incorrect customization to [ObjectMapper].
79+
*/
5380
inline fun <reified T> ObjectMapper.readValue(jp: JsonParser): T = readValue(jp, jacksonTypeRef<T>())
54-
inline fun <reified T> ObjectMapper.readValues(jp: JsonParser): MappingIterator<T> = readValues(jp, jacksonTypeRef<T>())
81+
.checkTypeMismatch()
82+
/**
83+
* Shorthand for [ObjectMapper.readValues].
84+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
85+
* Other cases where the read value is of a different type than [T]
86+
* due to an incorrect customization to [ObjectMapper].
87+
*/
88+
inline fun <reified T> ObjectMapper.readValues(jp: JsonParser): MappingIterator<T> {
89+
val values = readValues(jp, jacksonTypeRef<T>())
90+
91+
return object : MappingIterator<T>(values) {
92+
override fun nextValue(): T = super.nextValue().checkTypeMismatch()
93+
}
94+
}
5595

56-
inline fun <reified T> ObjectMapper.readValue(src: File): T = readValue(src, jacksonTypeRef<T>())
57-
inline fun <reified T> ObjectMapper.readValue(src: URL): T = readValue(src, jacksonTypeRef<T>())
96+
/**
97+
* Shorthand for [ObjectMapper.readValue].
98+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
99+
* Other cases where the read value is of a different type than [T]
100+
* due to an incorrect customization to [ObjectMapper].
101+
*/
102+
inline fun <reified T> ObjectMapper.readValue(src: File): T = readValue(src, jacksonTypeRef<T>()).checkTypeMismatch()
103+
/**
104+
* Shorthand for [ObjectMapper.readValue].
105+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
106+
* Other cases where the read value is of a different type than [T]
107+
* due to an incorrect customization to [ObjectMapper].
108+
*/
109+
inline fun <reified T> ObjectMapper.readValue(src: URL): T = readValue(src, jacksonTypeRef<T>()).checkTypeMismatch()
110+
/**
111+
* Shorthand for [ObjectMapper.readValue].
112+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
113+
* Other cases where the read value is of a different type than [T]
114+
* due to an incorrect customization to [ObjectMapper].
115+
*/
58116
inline fun <reified T> ObjectMapper.readValue(content: String): T = readValue(content, jacksonTypeRef<T>())
59-
inline fun <reified T> ObjectMapper.readValue(src: Reader): T = readValue(src, jacksonTypeRef<T>())
117+
.checkTypeMismatch()
118+
/**
119+
* Shorthand for [ObjectMapper.readValue].
120+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
121+
* Other cases where the read value is of a different type than [T]
122+
* due to an incorrect customization to [ObjectMapper].
123+
*/
124+
inline fun <reified T> ObjectMapper.readValue(src: Reader): T = readValue(src, jacksonTypeRef<T>()).checkTypeMismatch()
125+
/**
126+
* Shorthand for [ObjectMapper.readValue].
127+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
128+
* Other cases where the read value is of a different type than [T]
129+
* due to an incorrect customization to [ObjectMapper].
130+
*/
60131
inline fun <reified T> ObjectMapper.readValue(src: InputStream): T = readValue(src, jacksonTypeRef<T>())
132+
.checkTypeMismatch()
133+
/**
134+
* Shorthand for [ObjectMapper.readValue].
135+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
136+
* Other cases where the read value is of a different type than [T]
137+
* due to an incorrect customization to [ObjectMapper].
138+
*/
61139
inline fun <reified T> ObjectMapper.readValue(src: ByteArray): T = readValue(src, jacksonTypeRef<T>())
62-
140+
.checkTypeMismatch()
141+
142+
/**
143+
* Shorthand for [ObjectMapper.readValue].
144+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
145+
* Other cases where the read value is of a different type than [T]
146+
* due to an incorrect customization to [ObjectMapper].
147+
*/
63148
inline fun <reified T> ObjectMapper.treeToValue(n: TreeNode): T = readValue(this.treeAsTokens(n), jacksonTypeRef<T>())
149+
.checkTypeMismatch()
150+
/**
151+
* Shorthand for [ObjectMapper.convertValue].
152+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
153+
* Other cases where the read value is of a different type than [T]
154+
* due to an incorrect customization to [ObjectMapper].
155+
*/
64156
inline fun <reified T> ObjectMapper.convertValue(from: Any?): T = convertValue(from, jacksonTypeRef<T>())
65-
157+
.checkTypeMismatch()
158+
159+
/**
160+
* Shorthand for [ObjectMapper.readValue].
161+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
162+
* Other cases where the read value is of a different type than [T]
163+
* due to an incorrect customization to [ObjectMapper].
164+
*/
66165
inline fun <reified T> ObjectReader.readValueTyped(jp: JsonParser): T = readValue(jp, jacksonTypeRef<T>())
67-
inline fun <reified T> ObjectReader.readValuesTyped(jp: JsonParser): Iterator<T> = readValues(jp, jacksonTypeRef<T>())
166+
.checkTypeMismatch()
167+
/**
168+
* Shorthand for [ObjectMapper.readValues].
169+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
170+
* Other cases where the read value is of a different type than [T]
171+
* due to an incorrect customization to [ObjectMapper].
172+
*/
173+
inline fun <reified T> ObjectReader.readValuesTyped(jp: JsonParser): Iterator<T> {
174+
val values = readValues(jp, jacksonTypeRef<T>())
175+
176+
return object : Iterator<T> by values {
177+
override fun next(): T = values.next().checkTypeMismatch<T>()
178+
}
179+
}
68180
inline fun <reified T> ObjectReader.treeToValue(n: TreeNode): T? = readValue(this.treeAsTokens(n), jacksonTypeRef<T>())
69181

70182
inline fun <reified T, reified U> ObjectMapper.addMixIn(): ObjectMapper = this.addMixIn(T::class.java, U::class.java)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.fasterxml.jackson.module.kotlin
2+
3+
import com.fasterxml.jackson.databind.RuntimeJsonMappingException
4+
import com.fasterxml.jackson.databind.node.NullNode
5+
import org.junit.jupiter.api.Nested
6+
import org.junit.jupiter.api.Test
7+
import org.junit.jupiter.api.assertThrows
8+
import java.io.StringReader
9+
10+
class ReadValueTest {
11+
@Nested
12+
inner class CheckTypeMismatchTest {
13+
@Test
14+
fun jsonParser() {
15+
val src = defaultMapper.createParser("null")
16+
assertThrows<RuntimeJsonMappingException> {
17+
defaultMapper.readValue<String>(src)
18+
}.printStackTrace()
19+
}
20+
21+
@Test
22+
fun file() {
23+
val src = createTempJson("null")
24+
assertThrows<RuntimeJsonMappingException> {
25+
defaultMapper.readValue<String>(src)
26+
}
27+
}
28+
29+
// Not implemented because a way to test without mocks was not found
30+
// @Test
31+
// fun url() {
32+
// }
33+
34+
@Test
35+
fun string() {
36+
val src = "null"
37+
assertThrows<RuntimeJsonMappingException> {
38+
defaultMapper.readValue<String>(src)
39+
}
40+
}
41+
42+
@Test
43+
fun reader() {
44+
val src = StringReader("null")
45+
assertThrows<RuntimeJsonMappingException> {
46+
defaultMapper.readValue<String>(src)
47+
}
48+
}
49+
50+
@Test
51+
fun inputStream() {
52+
val src = "null".byteInputStream()
53+
assertThrows<RuntimeJsonMappingException> {
54+
defaultMapper.readValue<String>(src)
55+
}
56+
}
57+
58+
@Test
59+
fun byteArray() {
60+
val src = "null".toByteArray()
61+
assertThrows<RuntimeJsonMappingException> {
62+
defaultMapper.readValue<String>(src)
63+
}
64+
}
65+
66+
@Test
67+
fun treeToValueTreeNode() {
68+
assertThrows<RuntimeJsonMappingException> {
69+
defaultMapper.treeToValue<String>(NullNode.instance)
70+
}
71+
}
72+
73+
@Test
74+
fun convertValueAny() {
75+
assertThrows<RuntimeJsonMappingException> {
76+
defaultMapper.convertValue<String>(null)
77+
}
78+
}
79+
80+
@Test
81+
fun readValueTypedJsonParser() {
82+
val reader = defaultMapper.reader()
83+
val src = reader.createParser("null")
84+
assertThrows<RuntimeJsonMappingException> {
85+
reader.readValueTyped<String>(src)
86+
}
87+
}
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.fasterxml.jackson.module.kotlin
2+
3+
import com.fasterxml.jackson.core.JsonParser
4+
import com.fasterxml.jackson.databind.DeserializationContext
5+
import com.fasterxml.jackson.databind.RuntimeJsonMappingException
6+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
7+
import com.fasterxml.jackson.databind.module.SimpleModule
8+
import org.junit.jupiter.api.Nested
9+
import org.junit.jupiter.api.Test
10+
import org.junit.jupiter.api.assertThrows
11+
import kotlin.test.assertEquals
12+
13+
class ReadValuesTest {
14+
class MyStrDeser : StdDeserializer<String>(String::class.java) {
15+
override fun deserialize(
16+
p: JsonParser,
17+
ctxt: DeserializationContext
18+
): String? = p.valueAsString.takeIf { it != "bar" }
19+
}
20+
21+
@Nested
22+
inner class CheckTypeMismatchTest {
23+
val mapper = jacksonObjectMapper().registerModule(
24+
object : SimpleModule() {
25+
init {
26+
addDeserializer(String::class.java, MyStrDeser())
27+
}
28+
}
29+
)!!
30+
31+
@Test
32+
fun readValuesJsonParserNext() {
33+
val src = mapper.createParser(""""foo"${"\n"}"bar"""")
34+
val itr = mapper.readValues<String>(src)
35+
36+
assertEquals("foo", itr.next())
37+
assertThrows<RuntimeJsonMappingException> {
38+
itr.next()
39+
}
40+
}
41+
42+
@Test
43+
fun readValuesJsonParserNextValue() {
44+
val src = mapper.createParser(""""foo"${"\n"}"bar"""")
45+
val itr = mapper.readValues<String>(src)
46+
47+
assertEquals("foo", itr.nextValue())
48+
assertThrows<RuntimeJsonMappingException> {
49+
itr.nextValue()
50+
}
51+
}
52+
53+
@Test
54+
fun readValuesTypedJsonParser() {
55+
val reader = mapper.reader()
56+
val src = reader.createParser(""""foo"${"\n"}"bar"""")
57+
val itr = reader.readValuesTyped<String>(src)
58+
59+
assertEquals("foo", itr.next())
60+
assertThrows<RuntimeJsonMappingException> {
61+
itr.next()
62+
}
63+
}
64+
}
65+
}

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

+17
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import com.fasterxml.jackson.core.util.DefaultIndenter
55
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
66
import com.fasterxml.jackson.databind.ObjectMapper
77
import com.fasterxml.jackson.databind.ObjectWriter
8+
import java.io.File
9+
import java.io.FileOutputStream
10+
import java.io.OutputStreamWriter
11+
import java.nio.charset.StandardCharsets
812
import kotlin.reflect.KParameter
913
import kotlin.reflect.full.memberProperties
1014
import kotlin.reflect.full.primaryConstructor
@@ -30,3 +34,16 @@ internal inline fun <reified T : Any> assertReflectEquals(expected: T, actual: T
3034
assertEquals(it.get(expected), it.get(actual))
3135
}
3236
}
37+
38+
internal fun createTempJson(json: String): File {
39+
val file = File.createTempFile("temp", ".json")
40+
file.deleteOnExit()
41+
OutputStreamWriter(
42+
FileOutputStream(file),
43+
StandardCharsets.UTF_8
44+
).use { writer ->
45+
writer.write(json)
46+
writer.flush()
47+
}
48+
return file
49+
}

src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt

+3-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import org.junit.jupiter.api.Nested
99
import org.junit.jupiter.api.assertThrows
1010
import org.junit.jupiter.api.Test
1111
import java.lang.reflect.InvocationTargetException
12+
import kotlin.test.assertNotEquals
1213

1314
class WithoutCustomDeserializeMethodTest {
1415
companion object {
@@ -42,10 +43,8 @@ class WithoutCustomDeserializeMethodTest {
4243
// failing
4344
@Test
4445
fun nullString() {
45-
org.junit.jupiter.api.assertThrows<NullPointerException>("#209 has been fixed.") {
46-
val result = defaultMapper.readValue<NullableObject>("null")
47-
assertEquals(NullableObject(null), result)
48-
}
46+
val result = defaultMapper.readValue<NullableObject?>("null")
47+
assertNotEquals(NullableObject(null), result, "kogera #209 has been fixed.")
4948
}
5049
}
5150
}

src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/deserializer/SpecifiedForObjectMapperTest.kt

+3-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser.valueClass.Pr
99
import org.junit.jupiter.api.Assertions.assertEquals
1010
import org.junit.jupiter.api.Nested
1111
import org.junit.jupiter.api.Test
12+
import kotlin.test.assertNotEquals
1213

1314
class SpecifiedForObjectMapperTest {
1415
companion object {
@@ -48,10 +49,8 @@ class SpecifiedForObjectMapperTest {
4849
// failing
4950
@Test
5051
fun nullString() {
51-
org.junit.jupiter.api.assertThrows<NullPointerException>("#209 has been fixed.") {
52-
val result = mapper.readValue<NullableObject>("null")
53-
assertEquals(NullableObject("null-value-deser"), result)
54-
}
52+
val result = mapper.readValue<NullableObject?>("null")
53+
assertNotEquals(NullableObject("null-value-deser"), result, "kogera #209 has been fixed.")
5554
}
5655
}
5756
}

0 commit comments

Comments
 (0)