Skip to content

Commit 0babe5f

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

File tree

6 files changed

+307
-14
lines changed

6 files changed

+307
-14
lines changed

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

Lines changed: 116 additions & 5 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,138 @@ 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>())
83+
.checkTypeMismatch()
84+
85+
// TODO: After importing 2.19, import the changes in kotlin-module and uncomment the tests.
5686
public inline fun <reified T> ObjectMapper.readValues(
5787
jp: JsonParser
5888
): MappingIterator<T> = readValues(jp, jacksonTypeRef<T>())
5989

90+
/**
91+
* Shorthand for [ObjectMapper.readValue].
92+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
93+
* Other cases where the read value is of a different type than [T]
94+
* due to an incorrect customization to [ObjectMapper].
95+
*/
6096
public inline fun <reified T> ObjectMapper.readValue(src: File): T = readValue(src, jacksonTypeRef<T>())
97+
.checkTypeMismatch()
98+
99+
/**
100+
* Shorthand for [ObjectMapper.readValue].
101+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
102+
* Other cases where the read value is of a different type than [T]
103+
* due to an incorrect customization to [ObjectMapper].
104+
*/
61105
public inline fun <reified T> ObjectMapper.readValue(src: URL): T = readValue(src, jacksonTypeRef<T>())
106+
.checkTypeMismatch()
107+
108+
/**
109+
* Shorthand for [ObjectMapper.readValue].
110+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
111+
* Other cases where the read value is of a different type than [T]
112+
* due to an incorrect customization to [ObjectMapper].
113+
*/
62114
public inline fun <reified T> ObjectMapper.readValue(content: String): T = readValue(content, jacksonTypeRef<T>())
115+
.checkTypeMismatch()
116+
117+
/**
118+
* Shorthand for [ObjectMapper.readValue].
119+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
120+
* Other cases where the read value is of a different type than [T]
121+
* due to an incorrect customization to [ObjectMapper].
122+
*/
63123
public inline fun <reified T> ObjectMapper.readValue(src: Reader): T = readValue(src, jacksonTypeRef<T>())
124+
.checkTypeMismatch()
125+
126+
/**
127+
* Shorthand for [ObjectMapper.readValue].
128+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
129+
* Other cases where the read value is of a different type than [T]
130+
* due to an incorrect customization to [ObjectMapper].
131+
*/
64132
public inline fun <reified T> ObjectMapper.readValue(src: InputStream): T = readValue(src, jacksonTypeRef<T>())
133+
.checkTypeMismatch()
134+
135+
/**
136+
* Shorthand for [ObjectMapper.readValue].
137+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
138+
* Other cases where the read value is of a different type than [T]
139+
* due to an incorrect customization to [ObjectMapper].
140+
*/
65141
public inline fun <reified T> ObjectMapper.readValue(src: ByteArray): T = readValue(src, jacksonTypeRef<T>())
142+
.checkTypeMismatch()
143+
144+
/**
145+
* Shorthand for [ObjectMapper.readValue].
146+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
147+
* Other cases where the read value is of a different type than [T]
148+
* due to an incorrect customization to [ObjectMapper].
149+
*/
150+
public inline fun <reified T> ObjectMapper.treeToValue(
151+
n: TreeNode
152+
): T = readValue(this.treeAsTokens(n), jacksonTypeRef<T>()).checkTypeMismatch()
66153

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

163+
/**
164+
* Shorthand for [ObjectReader.readValue].
165+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
166+
* Other cases where the read value is of a different type than [T]
167+
* due to an incorrect customization to [ObjectReader].
168+
*/
70169
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>())
170+
.checkTypeMismatch()
171+
172+
/**
173+
* Shorthand for [ObjectReader.readValues].
174+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
175+
* Other cases where the read value is of a different type than [T]
176+
* due to an incorrect customization to [ObjectReader].
177+
*/
178+
public inline fun <reified T> ObjectReader.readValuesTyped(jp: JsonParser): Iterator<T> {
179+
val values = readValues(jp, jacksonTypeRef<T>())
180+
181+
return object : Iterator<T> by values {
182+
override fun next(): T = values.next().checkTypeMismatch<T>()
183+
}
184+
}
74185
public inline fun <reified T> ObjectReader.treeToValue(
75186
n: TreeNode
76-
): T? = readValue(treeAsTokens(n), jacksonTypeRef<T>())
187+
): T? = readValue(this.treeAsTokens(n), jacksonTypeRef<T>())
77188

78189
public inline fun <reified T, reified U> ObjectMapper.addMixIn(): ObjectMapper = addMixIn(T::class.java, U::class.java)
79190
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 & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ 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
12-
import org.junit.jupiter.api.assertThrows
1313

1414
class SpecifiedForObjectMapperTest {
1515
companion object {
@@ -49,10 +49,8 @@ class SpecifiedForObjectMapperTest {
4949
// failing
5050
@Test
5151
fun nullString() {
52-
assertThrows<NullPointerException>("#209 has been fixed.") {
53-
val result = mapper.readValue<NullableObject>("null")
54-
assertEquals(NullableObject("null-value-deser"), result)
55-
}
52+
val result = mapper.readValue<NullableObject?>("null")
53+
assertNotEquals(NullableObject("null-value-deser"), result, "#209 has been fixed.")
5654
}
5755
}
5856
}
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)