-
-
Notifications
You must be signed in to change notification settings - Fork 179
Added type match check to read functions #937
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5dc7e21
d5008b2
c89e238
c9038f7
5d404ea
ec2f84e
f4a5449
7ee7c82
371a858
0eb0cfc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.JsonSerializer | |
import com.fasterxml.jackson.databind.MappingIterator | ||
import com.fasterxml.jackson.databind.ObjectMapper | ||
import com.fasterxml.jackson.databind.ObjectReader | ||
import com.fasterxml.jackson.databind.RuntimeJsonMappingException | ||
import com.fasterxml.jackson.databind.json.JsonMapper | ||
import com.fasterxml.jackson.databind.module.SimpleModule | ||
import com.fasterxml.jackson.databind.node.ArrayNode | ||
|
@@ -50,21 +51,132 @@ fun ObjectMapper.registerKotlinModule(initializer: KotlinModule.Builder.() -> Un | |
|
||
inline fun <reified T> jacksonTypeRef(): TypeReference<T> = object: TypeReference<T>() {} | ||
|
||
/** | ||
* It is public due to Kotlin restrictions, but should not be used externally. | ||
*/ | ||
inline fun <reified T> Any?.checkTypeMismatch(): T { | ||
// Basically, this check assumes that T is non-null and the value is null. | ||
// Since this can be caused by both input or ObjectMapper implementation errors, | ||
// a more abstract RuntimeJsonMappingException is thrown. | ||
if (this !is T) { | ||
val nullability = if (null is T) "?" else "(non-null)" | ||
|
||
// Since the databind implementation of MappingIterator throws RuntimeJsonMappingException, | ||
// JsonMappingException was not used to unify the behavior. | ||
throw RuntimeJsonMappingException( | ||
"Deserialized value did not match the specified type; " + | ||
"specified ${T::class.qualifiedName}${nullability} but was ${this?.let { it::class.qualifiedName }}" | ||
) | ||
} | ||
return this | ||
} | ||
|
||
/** | ||
* Shorthand for [ObjectMapper.readValue]. | ||
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null. | ||
* Other cases where the read value is of a different type than [T] | ||
* due to an incorrect customization to [ObjectMapper]. | ||
*/ | ||
inline fun <reified T> ObjectMapper.readValue(jp: JsonParser): T = readValue(jp, jacksonTypeRef<T>()) | ||
inline fun <reified T> ObjectMapper.readValues(jp: JsonParser): MappingIterator<T> = readValues(jp, jacksonTypeRef<T>()) | ||
.checkTypeMismatch() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Originally, there is an overhead of generating a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If performance is issue.... would simple There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. First, I confirmed that there was already an 8% drop in throughput before the change. The throughput is high enough to begin with, so I don't think the performance of this function will be an issue in basic use cases. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After a little more consideration, I decided not to make a performance statement because it is unlikely that the overhead of this function will actually dominate. |
||
/** | ||
* Shorthand for [ObjectMapper.readValues]. | ||
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null. | ||
* Other cases where the read value is of a different type than [T] | ||
* due to an incorrect customization to [ObjectMapper]. | ||
*/ | ||
inline fun <reified T> ObjectMapper.readValues(jp: JsonParser): MappingIterator<T> { | ||
val values = readValues(jp, jacksonTypeRef<T>()) | ||
|
||
return object : MappingIterator<T>(values) { | ||
override fun nextValue(): T = super.nextValue().checkTypeMismatch() | ||
} | ||
} | ||
|
||
inline fun <reified T> ObjectMapper.readValue(src: File): T = readValue(src, jacksonTypeRef<T>()) | ||
inline fun <reified T> ObjectMapper.readValue(src: URL): T = readValue(src, jacksonTypeRef<T>()) | ||
/** | ||
* Shorthand for [ObjectMapper.readValue]. | ||
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null. | ||
* Other cases where the read value is of a different type than [T] | ||
* due to an incorrect customization to [ObjectMapper]. | ||
*/ | ||
inline fun <reified T> ObjectMapper.readValue(src: File): T = readValue(src, jacksonTypeRef<T>()).checkTypeMismatch() | ||
/** | ||
* Shorthand for [ObjectMapper.readValue]. | ||
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null. | ||
* Other cases where the read value is of a different type than [T] | ||
* due to an incorrect customization to [ObjectMapper]. | ||
*/ | ||
inline fun <reified T> ObjectMapper.readValue(src: URL): T = readValue(src, jacksonTypeRef<T>()).checkTypeMismatch() | ||
/** | ||
* Shorthand for [ObjectMapper.readValue]. | ||
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null. | ||
* Other cases where the read value is of a different type than [T] | ||
* due to an incorrect customization to [ObjectMapper]. | ||
*/ | ||
inline fun <reified T> ObjectMapper.readValue(content: String): T = readValue(content, jacksonTypeRef<T>()) | ||
inline fun <reified T> ObjectMapper.readValue(src: Reader): T = readValue(src, jacksonTypeRef<T>()) | ||
.checkTypeMismatch() | ||
/** | ||
* Shorthand for [ObjectMapper.readValue]. | ||
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null. | ||
* Other cases where the read value is of a different type than [T] | ||
* due to an incorrect customization to [ObjectMapper]. | ||
*/ | ||
inline fun <reified T> ObjectMapper.readValue(src: Reader): T = readValue(src, jacksonTypeRef<T>()).checkTypeMismatch() | ||
/** | ||
* Shorthand for [ObjectMapper.readValue]. | ||
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null. | ||
* Other cases where the read value is of a different type than [T] | ||
* due to an incorrect customization to [ObjectMapper]. | ||
*/ | ||
inline fun <reified T> ObjectMapper.readValue(src: InputStream): T = readValue(src, jacksonTypeRef<T>()) | ||
.checkTypeMismatch() | ||
/** | ||
* Shorthand for [ObjectMapper.readValue]. | ||
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null. | ||
* Other cases where the read value is of a different type than [T] | ||
* due to an incorrect customization to [ObjectMapper]. | ||
*/ | ||
inline fun <reified T> ObjectMapper.readValue(src: ByteArray): T = readValue(src, jacksonTypeRef<T>()) | ||
|
||
.checkTypeMismatch() | ||
|
||
/** | ||
* Shorthand for [ObjectMapper.readValue]. | ||
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null. | ||
* Other cases where the read value is of a different type than [T] | ||
* due to an incorrect customization to [ObjectMapper]. | ||
*/ | ||
inline fun <reified T> ObjectMapper.treeToValue(n: TreeNode): T = readValue(this.treeAsTokens(n), jacksonTypeRef<T>()) | ||
.checkTypeMismatch() | ||
/** | ||
* Shorthand for [ObjectMapper.convertValue]. | ||
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null. | ||
* Other cases where the read value is of a different type than [T] | ||
* due to an incorrect customization to [ObjectMapper]. | ||
*/ | ||
inline fun <reified T> ObjectMapper.convertValue(from: Any?): T = convertValue(from, jacksonTypeRef<T>()) | ||
|
||
.checkTypeMismatch() | ||
|
||
/** | ||
* Shorthand for [ObjectMapper.readValue]. | ||
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null. | ||
* Other cases where the read value is of a different type than [T] | ||
* due to an incorrect customization to [ObjectMapper]. | ||
*/ | ||
inline fun <reified T> ObjectReader.readValueTyped(jp: JsonParser): T = readValue(jp, jacksonTypeRef<T>()) | ||
inline fun <reified T> ObjectReader.readValuesTyped(jp: JsonParser): Iterator<T> = readValues(jp, jacksonTypeRef<T>()) | ||
.checkTypeMismatch() | ||
/** | ||
* Shorthand for [ObjectMapper.readValues]. | ||
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null. | ||
* Other cases where the read value is of a different type than [T] | ||
* due to an incorrect customization to [ObjectMapper]. | ||
*/ | ||
inline fun <reified T> ObjectReader.readValuesTyped(jp: JsonParser): Iterator<T> { | ||
val values = readValues(jp, jacksonTypeRef<T>()) | ||
|
||
return object : Iterator<T> by values { | ||
override fun next(): T = values.next().checkTypeMismatch<T>() | ||
} | ||
} | ||
inline fun <reified T> ObjectReader.treeToValue(n: TreeNode): T? = readValue(this.treeAsTokens(n), jacksonTypeRef<T>()) | ||
|
||
inline fun <reified T, reified U> ObjectMapper.addMixIn(): ObjectMapper = this.addMixIn(T::class.java, U::class.java) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package com.fasterxml.jackson.module.kotlin | ||
|
||
import com.fasterxml.jackson.databind.RuntimeJsonMappingException | ||
import com.fasterxml.jackson.databind.node.NullNode | ||
import org.junit.jupiter.api.Nested | ||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.api.assertThrows | ||
import java.io.StringReader | ||
|
||
class ReadValueTest { | ||
@Nested | ||
inner class CheckTypeMismatchTest { | ||
@Test | ||
fun jsonParser() { | ||
val src = defaultMapper.createParser("null") | ||
assertThrows<RuntimeJsonMappingException> { | ||
defaultMapper.readValue<String>(src) | ||
}.printStackTrace() | ||
} | ||
|
||
@Test | ||
fun file() { | ||
val src = createTempJson("null") | ||
assertThrows<RuntimeJsonMappingException> { | ||
defaultMapper.readValue<String>(src) | ||
} | ||
} | ||
|
||
// Not implemented because a way to test without mocks was not found | ||
// @Test | ||
// fun url() { | ||
// } | ||
|
||
@Test | ||
fun string() { | ||
val src = "null" | ||
assertThrows<RuntimeJsonMappingException> { | ||
defaultMapper.readValue<String>(src) | ||
} | ||
} | ||
|
||
@Test | ||
fun reader() { | ||
val src = StringReader("null") | ||
assertThrows<RuntimeJsonMappingException> { | ||
defaultMapper.readValue<String>(src) | ||
} | ||
} | ||
|
||
@Test | ||
fun inputStream() { | ||
val src = "null".byteInputStream() | ||
assertThrows<RuntimeJsonMappingException> { | ||
defaultMapper.readValue<String>(src) | ||
} | ||
} | ||
|
||
@Test | ||
fun byteArray() { | ||
val src = "null".toByteArray() | ||
assertThrows<RuntimeJsonMappingException> { | ||
defaultMapper.readValue<String>(src) | ||
} | ||
} | ||
|
||
@Test | ||
fun treeToValueTreeNode() { | ||
assertThrows<RuntimeJsonMappingException> { | ||
defaultMapper.treeToValue<String>(NullNode.instance) | ||
} | ||
} | ||
|
||
@Test | ||
fun convertValueAny() { | ||
assertThrows<RuntimeJsonMappingException> { | ||
defaultMapper.convertValue<String>(null) | ||
} | ||
} | ||
|
||
@Test | ||
fun readValueTypedJsonParser() { | ||
val reader = defaultMapper.reader() | ||
val src = reader.createParser("null") | ||
assertThrows<RuntimeJsonMappingException> { | ||
reader.readValueTyped<String>(src) | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package com.fasterxml.jackson.module.kotlin | ||
|
||
import com.fasterxml.jackson.core.JsonParser | ||
import com.fasterxml.jackson.databind.DeserializationContext | ||
import com.fasterxml.jackson.databind.RuntimeJsonMappingException | ||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer | ||
import com.fasterxml.jackson.databind.module.SimpleModule | ||
import org.junit.jupiter.api.Nested | ||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.api.assertThrows | ||
import kotlin.test.assertEquals | ||
|
||
class ReadValuesTest { | ||
class MyStrDeser : StdDeserializer<String>(String::class.java) { | ||
override fun deserialize( | ||
p: JsonParser, | ||
ctxt: DeserializationContext | ||
): String? = p.valueAsString.takeIf { it != "bar" } | ||
} | ||
|
||
@Nested | ||
inner class CheckTypeMismatchTest { | ||
val mapper = jacksonObjectMapper().registerModule( | ||
object : SimpleModule() { | ||
init { | ||
addDeserializer(String::class.java, MyStrDeser()) | ||
} | ||
} | ||
)!! | ||
|
||
@Test | ||
fun readValuesJsonParserNext() { | ||
val src = mapper.createParser(""""foo"${"\n"}"bar"""") | ||
val itr = mapper.readValues<String>(src) | ||
|
||
assertEquals("foo", itr.next()) | ||
assertThrows<RuntimeJsonMappingException> { | ||
itr.next() | ||
} | ||
} | ||
|
||
@Test | ||
fun readValuesJsonParserNextValue() { | ||
val src = mapper.createParser(""""foo"${"\n"}"bar"""") | ||
val itr = mapper.readValues<String>(src) | ||
|
||
assertEquals("foo", itr.nextValue()) | ||
assertThrows<RuntimeJsonMappingException> { | ||
itr.nextValue() | ||
} | ||
} | ||
|
||
@Test | ||
fun readValuesTypedJsonParser() { | ||
val reader = mapper.reader() | ||
val src = reader.createParser(""""foo"${"\n"}"bar"""") | ||
val itr = reader.readValuesTyped<String>(src) | ||
|
||
assertEquals("foo", itr.next()) | ||
assertThrows<RuntimeJsonMappingException> { | ||
itr.next() | ||
} | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.