Skip to content

Added type match check to read functions #295

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

Merged
merged 1 commit into from
Apr 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -52,28 +53,138 @@ public fun ObjectMapper.registerKotlinModule(

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

/**
* It is public due to Kotlin restrictions, but should not be used externally.
*/
public 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].
*/
public inline fun <reified T> ObjectMapper.readValue(jp: JsonParser): T = readValue(jp, jacksonTypeRef<T>())
.checkTypeMismatch()

// TODO: After importing 2.19, import the changes in kotlin-module and uncomment the tests.
public inline fun <reified T> ObjectMapper.readValues(
jp: JsonParser
): MappingIterator<T> = readValues(jp, jacksonTypeRef<T>())
Comment on lines +85 to 88
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could not import because of required changes to MappingIterator in 2.19.


/**
* 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].
*/
public 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].
*/
public 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].
*/
public inline fun <reified T> ObjectMapper.readValue(content: String): T = readValue(content, 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].
*/
public 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].
*/
public 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].
*/
public 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].
*/
public inline fun <reified T> ObjectMapper.treeToValue(
n: TreeNode
): T = readValue(this.treeAsTokens(n), jacksonTypeRef<T>()).checkTypeMismatch()

public inline fun <reified T> ObjectMapper.treeToValue(n: TreeNode): T = readValue(treeAsTokens(n), jacksonTypeRef<T>())
/**
* 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].
*/
public inline fun <reified T> ObjectMapper.convertValue(from: Any?): T = convertValue(from, jacksonTypeRef<T>())
.checkTypeMismatch()

/**
* Shorthand for [ObjectReader.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 [ObjectReader].
*/
public inline fun <reified T> ObjectReader.readValueTyped(jp: JsonParser): T = readValue(jp, jacksonTypeRef<T>())
public inline fun <reified T> ObjectReader.readValuesTyped(
jp: JsonParser
): Iterator<T> = readValues(jp, jacksonTypeRef<T>())
.checkTypeMismatch()

/**
* Shorthand for [ObjectReader.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 [ObjectReader].
*/
public 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>()
}
}
public inline fun <reified T> ObjectReader.treeToValue(
n: TreeNode
): T? = readValue(treeAsTokens(n), jacksonTypeRef<T>())
): T? = readValue(this.treeAsTokens(n), jacksonTypeRef<T>())

public inline fun <reified T, reified U> ObjectMapper.addMixIn(): ObjectMapper = addMixIn(T::class.java, U::class.java)
public inline fun <reified T, reified U> JsonMapper.Builder.addMixIn(): JsonMapper.Builder = addMixIn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.ObjectWriter
import org.junit.jupiter.api.Assertions.assertEquals
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets
import kotlin.reflect.KParameter
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
Expand Down Expand Up @@ -33,3 +37,16 @@ internal inline fun <reified T : Any> assertReflectEquals(expected: T, actual: T
assertEquals(it.get(expected), it.get(actual))
}
}

internal fun createTempJson(json: String): File {
val file = File.createTempFile("temp", ".json")
file.deleteOnExit()
OutputStreamWriter(
FileOutputStream(file),
StandardCharsets.UTF_8
).use { writer ->
writer.write(json)
writer.flush()
}
return file
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper
import io.github.projectmapk.jackson.module.kogera.readValue
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
Expand Down Expand Up @@ -42,10 +43,8 @@ class WithoutCustomDeserializeMethodTest {
// failing
@Test
fun nullString() {
assertThrows<NullPointerException>("#209 has been fixed.") {
val result = defaultMapper.readValue<NullableObject>("null")
assertEquals(NullableObject(null), result)
}
val result = defaultMapper.readValue<NullableObject?>("null")
assertNotEquals(NullableObject(null), result, "#209 has been fixed.")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass
import io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass.NullableObject
import io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass.Primitive
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows

class SpecifiedForObjectMapperTest {
companion object {
Expand Down Expand Up @@ -49,10 +49,8 @@ class SpecifiedForObjectMapperTest {
// failing
@Test
fun nullString() {
assertThrows<NullPointerException>("#209 has been fixed.") {
val result = mapper.readValue<NullableObject>("null")
assertEquals(NullableObject("null-value-deser"), result)
}
val result = mapper.readValue<NullableObject?>("null")
assertNotEquals(NullableObject("null-value-deser"), result, "#209 has been fixed.")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.github.projectmapk.jackson.module.kogera.zPorted

import com.fasterxml.jackson.databind.RuntimeJsonMappingException
import com.fasterxml.jackson.databind.node.NullNode
import io.github.projectmapk.jackson.module.kogera.createTempJson
import io.github.projectmapk.jackson.module.kogera.defaultMapper
import io.github.projectmapk.jackson.module.kogera.readValue
import io.github.projectmapk.jackson.module.kogera.readValueTyped
import io.github.projectmapk.jackson.module.kogera.treeToValue
import io.github.projectmapk.jackson.module.kogera.convertValue
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,73 @@
package io.github.projectmapk.jackson.module.kogera.zPorted

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 io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper
import io.github.projectmapk.jackson.module.kogera.readValues
import io.github.projectmapk.jackson.module.kogera.readValuesTyped
import org.junit.jupiter.api.Assertions.assertDoesNotThrow
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows

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())
// TODO: It is expected to be checked after importing 2.19.
// assertThrows<RuntimeJsonMappingException> {
assertDoesNotThrow {
itr.next()
}
}

@Test
fun readValuesJsonParserNextValue() {
val src = mapper.createParser(""""foo"${"\n"}"bar"""")
val itr = mapper.readValues<String>(src)

assertEquals("foo", itr.nextValue())
// TODO: It is expected to be checked after importing 2.19.
// assertThrows<RuntimeJsonMappingException> {
assertDoesNotThrow {
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()
}
}
}
}
Loading