Skip to content

Commit 6fed499

Browse files
committed
Added type match check to read functions
from FasterXML/jackson-module-kotlin#937
1 parent 3ed9a8f commit 6fed499

File tree

6 files changed

+307
-18
lines changed

6 files changed

+307
-18
lines changed

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Extensions.kt

Lines changed: 116 additions & 10 deletions
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
@@ -52,28 +53,133 @@ public fun ObjectMapper.registerKotlinModule(
5253

5354
public inline fun <reified T> jacksonTypeRef(): TypeReference<T> = object : TypeReference<T>() {}
5455

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

88+
/**
89+
* Shorthand for [ObjectMapper.readValue].
90+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
91+
* Other cases where the read value is of a different type than [T]
92+
* due to an incorrect customization to [ObjectMapper].
93+
*/
6094
public inline fun <reified T> ObjectMapper.readValue(src: File): T = readValue(src, jacksonTypeRef<T>())
95+
.checkTypeMismatch()
96+
97+
/**
98+
* Shorthand for [ObjectMapper.readValue].
99+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
100+
* Other cases where the read value is of a different type than [T]
101+
* due to an incorrect customization to [ObjectMapper].
102+
*/
61103
public inline fun <reified T> ObjectMapper.readValue(src: URL): T = readValue(src, jacksonTypeRef<T>())
104+
.checkTypeMismatch()
105+
106+
/**
107+
* Shorthand for [ObjectMapper.readValue].
108+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
109+
* Other cases where the read value is of a different type than [T]
110+
* due to an incorrect customization to [ObjectMapper].
111+
*/
62112
public inline fun <reified T> ObjectMapper.readValue(content: String): T = readValue(content, jacksonTypeRef<T>())
113+
.checkTypeMismatch()
114+
115+
/**
116+
* Shorthand for [ObjectMapper.readValue].
117+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
118+
* Other cases where the read value is of a different type than [T]
119+
* due to an incorrect customization to [ObjectMapper].
120+
*/
63121
public inline fun <reified T> ObjectMapper.readValue(src: Reader): T = readValue(src, jacksonTypeRef<T>())
122+
.checkTypeMismatch()
123+
124+
/**
125+
* Shorthand for [ObjectMapper.readValue].
126+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
127+
* Other cases where the read value is of a different type than [T]
128+
* due to an incorrect customization to [ObjectMapper].
129+
*/
64130
public inline fun <reified T> ObjectMapper.readValue(src: InputStream): T = readValue(src, jacksonTypeRef<T>())
131+
.checkTypeMismatch()
132+
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+
*/
65139
public inline fun <reified T> ObjectMapper.readValue(src: ByteArray): T = readValue(src, jacksonTypeRef<T>())
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+
*/
148+
public inline fun <reified T> ObjectMapper.treeToValue(n: TreeNode): T = readValue(this.treeAsTokens(n), jacksonTypeRef<T>())
149+
.checkTypeMismatch()
66150

67-
public inline fun <reified T> ObjectMapper.treeToValue(n: TreeNode): T = readValue(treeAsTokens(n), jacksonTypeRef<T>())
151+
/**
152+
* Shorthand for [ObjectMapper.convertValue].
153+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
154+
* Other cases where the read value is of a different type than [T]
155+
* due to an incorrect customization to [ObjectMapper].
156+
*/
68157
public inline fun <reified T> ObjectMapper.convertValue(from: Any?): T = convertValue(from, jacksonTypeRef<T>())
158+
.checkTypeMismatch()
69159

160+
/**
161+
* Shorthand for [ObjectReader.readValue].
162+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
163+
* Other cases where the read value is of a different type than [T]
164+
* due to an incorrect customization to [ObjectReader].
165+
*/
70166
public inline fun <reified T> ObjectReader.readValueTyped(jp: JsonParser): T = readValue(jp, jacksonTypeRef<T>())
71-
public inline fun <reified T> ObjectReader.readValuesTyped(
72-
jp: JsonParser
73-
): Iterator<T> = readValues(jp, jacksonTypeRef<T>())
74-
public inline fun <reified T> ObjectReader.treeToValue(
75-
n: TreeNode
76-
): T? = readValue(treeAsTokens(n), jacksonTypeRef<T>())
167+
.checkTypeMismatch()
168+
169+
/**
170+
* Shorthand for [ObjectReader.readValues].
171+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
172+
* Other cases where the read value is of a different type than [T]
173+
* due to an incorrect customization to [ObjectReader].
174+
*/
175+
public inline fun <reified T> ObjectReader.readValuesTyped(jp: JsonParser): Iterator<T> {
176+
val values = readValues(jp, jacksonTypeRef<T>())
177+
178+
return object : Iterator<T> by values {
179+
override fun next(): T = values.next().checkTypeMismatch<T>()
180+
}
181+
}
182+
public inline fun <reified T> ObjectReader.treeToValue(n: TreeNode): T? = readValue(this.treeAsTokens(n), jacksonTypeRef<T>())
77183

78184
public inline fun <reified T, reified U> ObjectMapper.addMixIn(): ObjectMapper = addMixIn(T::class.java, U::class.java)
79185
public inline fun <reified T, reified U> JsonMapper.Builder.addMixIn(): JsonMapper.Builder = addMixIn(

src/test/kotlin/io/github/projectmapk/jackson/module/kogera/TestCommons.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
55
import com.fasterxml.jackson.databind.ObjectMapper
66
import com.fasterxml.jackson.databind.ObjectWriter
77
import org.junit.jupiter.api.Assertions.assertEquals
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
@@ -33,3 +37,16 @@ internal inline fun <reified T : Any> assertReflectEquals(expected: T, actual: T
3337
assertEquals(it.get(expected), it.get(actual))
3438
}
3539
}
40+
41+
internal fun createTempJson(json: String): File {
42+
val file = File.createTempFile("temp", ".json")
43+
file.deleteOnExit()
44+
OutputStreamWriter(
45+
FileOutputStream(file),
46+
StandardCharsets.UTF_8
47+
).use { writer ->
48+
writer.write(json)
49+
writer.flush()
50+
}
51+
return file
52+
}

src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper
55
import io.github.projectmapk.jackson.module.kogera.readValue
66
import org.junit.jupiter.api.Assertions
77
import org.junit.jupiter.api.Assertions.assertEquals
8+
import org.junit.jupiter.api.Assertions.assertNotEquals
89
import org.junit.jupiter.api.Nested
910
import org.junit.jupiter.api.Test
1011
import org.junit.jupiter.api.assertThrows
@@ -42,10 +43,8 @@ class WithoutCustomDeserializeMethodTest {
4243
// failing
4344
@Test
4445
fun nullString() {
45-
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, "#209 has been fixed.")
4948
}
5049
}
5150
}

src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/deserializer/SpecifiedForObjectMapperTest.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass
77
import io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass.NullableObject
88
import io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass.Primitive
99
import org.junit.jupiter.api.Assertions.assertEquals
10+
import org.junit.jupiter.api.Assertions.assertNotEquals
1011
import org.junit.jupiter.api.Nested
1112
import org.junit.jupiter.api.Test
1213
import org.junit.jupiter.api.assertThrows
@@ -49,10 +50,8 @@ class SpecifiedForObjectMapperTest {
4950
// failing
5051
@Test
5152
fun nullString() {
52-
assertThrows<NullPointerException>("#209 has been fixed.") {
53-
val result = mapper.readValue<NullableObject>("null")
54-
assertEquals(NullableObject("null-value-deser"), result)
55-
}
53+
val result = mapper.readValue<NullableObject?>("null")
54+
assertNotEquals(NullableObject("null-value-deser"), result, "#209 has been fixed.")
5655
}
5756
}
5857
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package io.github.projectmapk.jackson.module.kogera.zPorted
2+
3+
import com.fasterxml.jackson.databind.RuntimeJsonMappingException
4+
import com.fasterxml.jackson.databind.node.NullNode
5+
import io.github.projectmapk.jackson.module.kogera.createTempJson
6+
import io.github.projectmapk.jackson.module.kogera.defaultMapper
7+
import io.github.projectmapk.jackson.module.kogera.readValue
8+
import io.github.projectmapk.jackson.module.kogera.readValueTyped
9+
import io.github.projectmapk.jackson.module.kogera.treeToValue
10+
import io.github.projectmapk.jackson.module.kogera.convertValue
11+
import org.junit.jupiter.api.Nested
12+
import org.junit.jupiter.api.Test
13+
import org.junit.jupiter.api.assertThrows
14+
import java.io.StringReader
15+
16+
class ReadValueTest {
17+
@Nested
18+
inner class CheckTypeMismatchTest {
19+
@Test
20+
fun jsonParser() {
21+
val src = defaultMapper.createParser("null")
22+
assertThrows<RuntimeJsonMappingException> {
23+
defaultMapper.readValue<String>(src)
24+
}.printStackTrace()
25+
}
26+
27+
@Test
28+
fun file() {
29+
val src = createTempJson("null")
30+
assertThrows<RuntimeJsonMappingException> {
31+
defaultMapper.readValue<String>(src)
32+
}
33+
}
34+
35+
// Not implemented because a way to test without mocks was not found
36+
// @Test
37+
// fun url() {
38+
// }
39+
40+
@Test
41+
fun string() {
42+
val src = "null"
43+
assertThrows<RuntimeJsonMappingException> {
44+
defaultMapper.readValue<String>(src)
45+
}
46+
}
47+
48+
@Test
49+
fun reader() {
50+
val src = StringReader("null")
51+
assertThrows<RuntimeJsonMappingException> {
52+
defaultMapper.readValue<String>(src)
53+
}
54+
}
55+
56+
@Test
57+
fun inputStream() {
58+
val src = "null".byteInputStream()
59+
assertThrows<RuntimeJsonMappingException> {
60+
defaultMapper.readValue<String>(src)
61+
}
62+
}
63+
64+
@Test
65+
fun byteArray() {
66+
val src = "null".toByteArray()
67+
assertThrows<RuntimeJsonMappingException> {
68+
defaultMapper.readValue<String>(src)
69+
}
70+
}
71+
72+
@Test
73+
fun treeToValueTreeNode() {
74+
assertThrows<RuntimeJsonMappingException> {
75+
defaultMapper.treeToValue<String>(NullNode.instance)
76+
}
77+
}
78+
79+
@Test
80+
fun convertValueAny() {
81+
assertThrows<RuntimeJsonMappingException> {
82+
defaultMapper.convertValue<String>(null)
83+
}
84+
}
85+
86+
@Test
87+
fun readValueTypedJsonParser() {
88+
val reader = defaultMapper.reader()
89+
val src = reader.createParser("null")
90+
assertThrows<RuntimeJsonMappingException> {
91+
reader.readValueTyped<String>(src)
92+
}
93+
}
94+
}
95+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package io.github.projectmapk.jackson.module.kogera.zPorted
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 io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper
9+
import io.github.projectmapk.jackson.module.kogera.readValues
10+
import io.github.projectmapk.jackson.module.kogera.readValuesTyped
11+
import org.junit.jupiter.api.Assertions.assertDoesNotThrow
12+
import org.junit.jupiter.api.Assertions.assertEquals
13+
import org.junit.jupiter.api.Nested
14+
import org.junit.jupiter.api.Test
15+
import org.junit.jupiter.api.assertThrows
16+
17+
class ReadValuesTest {
18+
class MyStrDeser : StdDeserializer<String>(String::class.java) {
19+
override fun deserialize(
20+
p: JsonParser,
21+
ctxt: DeserializationContext
22+
): String? = p.valueAsString.takeIf { it != "bar" }
23+
}
24+
25+
@Nested
26+
inner class CheckTypeMismatchTest {
27+
val mapper = jacksonObjectMapper().registerModule(
28+
object : SimpleModule() {
29+
init {
30+
addDeserializer(String::class.java, MyStrDeser())
31+
}
32+
}
33+
)!!
34+
35+
@Test
36+
fun readValuesJsonParserNext() {
37+
val src = mapper.createParser(""""foo"${"\n"}"bar"""")
38+
val itr = mapper.readValues<String>(src)
39+
40+
assertEquals("foo", itr.next())
41+
// TODO: It is expected to be checked after importing 2.19.
42+
// assertThrows<RuntimeJsonMappingException> {
43+
assertDoesNotThrow {
44+
itr.next()
45+
}
46+
}
47+
48+
@Test
49+
fun readValuesJsonParserNextValue() {
50+
val src = mapper.createParser(""""foo"${"\n"}"bar"""")
51+
val itr = mapper.readValues<String>(src)
52+
53+
assertEquals("foo", itr.nextValue())
54+
// TODO: It is expected to be checked after importing 2.19.
55+
// assertThrows<RuntimeJsonMappingException> {
56+
assertDoesNotThrow {
57+
itr.nextValue()
58+
}
59+
}
60+
61+
@Test
62+
fun readValuesTypedJsonParser() {
63+
val reader = mapper.reader()
64+
val src = reader.createParser(""""foo"${"\n"}"bar"""")
65+
val itr = reader.readValuesTyped<String>(src)
66+
67+
assertEquals("foo", itr.next())
68+
assertThrows<RuntimeJsonMappingException> {
69+
itr.next()
70+
}
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)