diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Extensions.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Extensions.kt index 7d967c84..ea9fbc1b 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Extensions.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Extensions.kt @@ -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 @@ -52,28 +53,138 @@ public fun ObjectMapper.registerKotlinModule( public inline fun jacksonTypeRef(): TypeReference = object : TypeReference() {} +/** + * It is public due to Kotlin restrictions, but should not be used externally. + */ +public inline fun 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 ObjectMapper.readValue(jp: JsonParser): T = readValue(jp, jacksonTypeRef()) + .checkTypeMismatch() + +// TODO: After importing 2.19, import the changes in kotlin-module and uncomment the tests. public inline fun ObjectMapper.readValues( jp: JsonParser ): MappingIterator = readValues(jp, jacksonTypeRef()) +/** + * 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 ObjectMapper.readValue(src: File): T = readValue(src, jacksonTypeRef()) + .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 ObjectMapper.readValue(src: URL): T = readValue(src, jacksonTypeRef()) + .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 ObjectMapper.readValue(content: String): T = readValue(content, jacksonTypeRef()) + .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 ObjectMapper.readValue(src: Reader): T = readValue(src, jacksonTypeRef()) + .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 ObjectMapper.readValue(src: InputStream): T = readValue(src, jacksonTypeRef()) + .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 ObjectMapper.readValue(src: ByteArray): T = readValue(src, jacksonTypeRef()) + .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 ObjectMapper.treeToValue( + n: TreeNode +): T = readValue(this.treeAsTokens(n), jacksonTypeRef()).checkTypeMismatch() -public inline fun ObjectMapper.treeToValue(n: TreeNode): T = readValue(treeAsTokens(n), jacksonTypeRef()) +/** + * 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 ObjectMapper.convertValue(from: Any?): T = convertValue(from, jacksonTypeRef()) + .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 ObjectReader.readValueTyped(jp: JsonParser): T = readValue(jp, jacksonTypeRef()) -public inline fun ObjectReader.readValuesTyped( - jp: JsonParser -): Iterator = readValues(jp, jacksonTypeRef()) + .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 ObjectReader.readValuesTyped(jp: JsonParser): Iterator { + val values = readValues(jp, jacksonTypeRef()) + + return object : Iterator by values { + override fun next(): T = values.next().checkTypeMismatch() + } +} public inline fun ObjectReader.treeToValue( n: TreeNode -): T? = readValue(treeAsTokens(n), jacksonTypeRef()) +): T? = readValue(this.treeAsTokens(n), jacksonTypeRef()) public inline fun ObjectMapper.addMixIn(): ObjectMapper = addMixIn(T::class.java, U::class.java) public inline fun JsonMapper.Builder.addMixIn(): JsonMapper.Builder = addMixIn( diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/TestCommons.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/TestCommons.kt index ce921d5a..04746ca2 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/TestCommons.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/TestCommons.kt @@ -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 @@ -33,3 +37,16 @@ internal inline fun 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 +} diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt index cfde84ae..c9ec3da5 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt @@ -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 @@ -42,10 +43,8 @@ class WithoutCustomDeserializeMethodTest { // failing @Test fun nullString() { - assertThrows("#209 has been fixed.") { - val result = defaultMapper.readValue("null") - assertEquals(NullableObject(null), result) - } + val result = defaultMapper.readValue("null") + assertNotEquals(NullableObject(null), result, "#209 has been fixed.") } } } diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/deserializer/SpecifiedForObjectMapperTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/deserializer/SpecifiedForObjectMapperTest.kt index c7b1776b..692080c7 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/deserializer/SpecifiedForObjectMapperTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/deserializer/SpecifiedForObjectMapperTest.kt @@ -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 { @@ -49,10 +49,8 @@ class SpecifiedForObjectMapperTest { // failing @Test fun nullString() { - assertThrows("#209 has been fixed.") { - val result = mapper.readValue("null") - assertEquals(NullableObject("null-value-deser"), result) - } + val result = mapper.readValue("null") + assertNotEquals(NullableObject("null-value-deser"), result, "#209 has been fixed.") } } } diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/ReadValueTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/ReadValueTest.kt new file mode 100644 index 00000000..53e74594 --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/ReadValueTest.kt @@ -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 { + defaultMapper.readValue(src) + }.printStackTrace() + } + + @Test + fun file() { + val src = createTempJson("null") + assertThrows { + defaultMapper.readValue(src) + } + } + + // Not implemented because a way to test without mocks was not found + // @Test + // fun url() { + // } + + @Test + fun string() { + val src = "null" + assertThrows { + defaultMapper.readValue(src) + } + } + + @Test + fun reader() { + val src = StringReader("null") + assertThrows { + defaultMapper.readValue(src) + } + } + + @Test + fun inputStream() { + val src = "null".byteInputStream() + assertThrows { + defaultMapper.readValue(src) + } + } + + @Test + fun byteArray() { + val src = "null".toByteArray() + assertThrows { + defaultMapper.readValue(src) + } + } + + @Test + fun treeToValueTreeNode() { + assertThrows { + defaultMapper.treeToValue(NullNode.instance) + } + } + + @Test + fun convertValueAny() { + assertThrows { + defaultMapper.convertValue(null) + } + } + + @Test + fun readValueTypedJsonParser() { + val reader = defaultMapper.reader() + val src = reader.createParser("null") + assertThrows { + reader.readValueTyped(src) + } + } + } +} diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/ReadValuesTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/ReadValuesTest.kt new file mode 100644 index 00000000..10585272 --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/ReadValuesTest.kt @@ -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::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(src) + + assertEquals("foo", itr.next()) + // TODO: It is expected to be checked after importing 2.19. + // assertThrows { + assertDoesNotThrow { + itr.next() + } + } + + @Test + fun readValuesJsonParserNextValue() { + val src = mapper.createParser(""""foo"${"\n"}"bar"""") + val itr = mapper.readValues(src) + + assertEquals("foo", itr.nextValue()) + // TODO: It is expected to be checked after importing 2.19. + // assertThrows { + assertDoesNotThrow { + itr.nextValue() + } + } + + @Test + fun readValuesTypedJsonParser() { + val reader = mapper.reader() + val src = reader.createParser(""""foo"${"\n"}"bar"""") + val itr = reader.readValuesTyped(src) + + assertEquals("foo", itr.next()) + assertThrows { + itr.next() + } + } + } +}