diff --git a/pom.xml b/pom.xml index 62e5b277..e5770e1c 100644 --- a/pom.xml +++ b/pom.xml @@ -254,7 +254,9 @@ com.fasterxml.jackson.module.kotlin.KotlinModule#getSingletonSupport() com.fasterxml.jackson.module.kotlin.SingletonSupport - + + com.fasterxml.jackson.module.kotlin.KotlinNamesAnnotationIntrospector#KotlinNamesAnnotationIntrospector(com.fasterxml.jackson.module.kotlin.ReflectionCache,boolean) + diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 26f66e30..f5ed2f9f 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -18,6 +18,7 @@ Contributors: # 2.19.0 (not yet released) WrongWrong (@k163377) +* #885: Performance improvement of strictNullChecks * #884: Changed the base class of MissingKotlinParameterException to InvalidNullException * #878: Fix for #876 * #868: Added test case for FAIL_ON_NULL_FOR_PRIMITIVES diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 61397af7..162d064b 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -17,6 +17,10 @@ Co-maintainers: ------------------------------------------------------------------------ 2.19.0 (not yet released) +#885: A new `StrictNullChecks` option(KotlinFeature.NewStrictNullChecks) has been added which greatly improves throughput. + Benchmarks show a consistent throughput drop of less than 2% when enabled (prior to the improvement, the worst throughput drop was more than 30%). + Note that the new backend changes the exception thrown to `InvalidNullException` and with it the error message. + Also note that the base class for `MissingKotlinParameterException` was changed to `InvalidNullException` in #884. #884: The base class for `MissingKotlinParameterException` has been changed to `InvalidNullException`. If you do not catch this exception or catch `MismatchedInputException`, the behavior is unchanged. If you are catching both `MismatchedKotlinParameterException` and `InvalidNullException`, you must catch `MismatchedKotlinParameterException` first. diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt index 52512e6d..e7e12d5b 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt @@ -1,5 +1,7 @@ package com.fasterxml.jackson.module.kotlin +import com.fasterxml.jackson.annotation.JsonSetter +import com.fasterxml.jackson.databind.exc.InvalidNullException import java.util.BitSet /** @@ -40,6 +42,11 @@ enum class KotlinFeature(internal val enabledByDefault: Boolean) { * may contain null values after deserialization. * Enabling it protects against this but has significant performance impact. */ + @Deprecated( + level = DeprecationLevel.WARNING, + message = "This option will be migrated to the new backend in 2.21.", + replaceWith = ReplaceWith("NewStrictNullChecks") + ) StrictNullChecks(enabledByDefault = false), /** @@ -66,7 +73,23 @@ enum class KotlinFeature(internal val enabledByDefault: Boolean) { * `@JsonFormat` annotations need to be declared either on getter using `@get:JsonFormat` or field using `@field:JsonFormat`. * See [jackson-module-kotlin#651] for details. */ - UseJavaDurationConversion(enabledByDefault = false); + UseJavaDurationConversion(enabledByDefault = false), + + /** + * New [StrictNullChecks] feature with improved throughput. + * Internally, it will be the same as if [JsonSetter] (contentNulls = FAIL) had been granted. + * Benchmarks show that it can check for illegal nulls with throughput nearly identical to the default (see [jackson-module-kotlin#719]). + * + * Note that in the new backend, the exception thrown has changed from [MissingKotlinParameterException] to [InvalidNullException]. + * The message will be changed accordingly. + * Since 2.19, the base class of [MissingKotlinParameterException] has also been changed to [InvalidNullException], + * so be careful when catching it. + * + * This is a temporary option for a phased backend migration, + * which will eventually be merged into [StrictNullChecks]. + * Also, specifying both this and [StrictNullChecks] is not permitted. + */ + NewStrictNullChecks(enabledByDefault = false); internal val bitSet: BitSet = (1 shl ordinal).toBitSet() diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt index d396324d..8b128958 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt @@ -7,6 +7,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyCollection import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyMap import com.fasterxml.jackson.module.kotlin.KotlinFeature.SingletonSupport import com.fasterxml.jackson.module.kotlin.KotlinFeature.StrictNullChecks +import com.fasterxml.jackson.module.kotlin.KotlinFeature.NewStrictNullChecks import com.fasterxml.jackson.module.kotlin.KotlinFeature.KotlinPropertyNameAsImplicitName import com.fasterxml.jackson.module.kotlin.KotlinFeature.UseJavaDurationConversion import java.util.* @@ -42,9 +43,10 @@ class KotlinModule private constructor( val nullToEmptyMap: Boolean = NullToEmptyMap.enabledByDefault, val nullIsSameAsDefault: Boolean = NullIsSameAsDefault.enabledByDefault, val singletonSupport: Boolean = SingletonSupport.enabledByDefault, - val strictNullChecks: Boolean = StrictNullChecks.enabledByDefault, + strictNullChecks: Boolean = StrictNullChecks.enabledByDefault, val kotlinPropertyNameAsImplicitName: Boolean = KotlinPropertyNameAsImplicitName.enabledByDefault, val useJavaDurationConversion: Boolean = UseJavaDurationConversion.enabledByDefault, + private val newStrictNullChecks: Boolean = NewStrictNullChecks.enabledByDefault, ) : SimpleModule(KotlinModule::class.java.name, PackageVersion.VERSION) { /* * Prior to 2.18, an older Enum called SingletonSupport was used to manage feature. @@ -64,6 +66,19 @@ class KotlinModule private constructor( ) val enabledSingletonSupport: Boolean get() = singletonSupport + private val oldStrictNullChecks: Boolean = strictNullChecks + + // To reduce the amount of destructive changes, no properties will be added to the public. + val strictNullChecks: Boolean = if (strictNullChecks) { + if (newStrictNullChecks) { + throw IllegalArgumentException("Enabling both StrictNullChecks and NewStrictNullChecks is not permitted.") + } + + true + } else { + newStrictNullChecks + } + companion object { // Increment when option is added private const val serialVersionUID = 3L @@ -84,6 +99,7 @@ class KotlinModule private constructor( builder.isEnabled(StrictNullChecks), builder.isEnabled(KotlinPropertyNameAsImplicitName), builder.isEnabled(UseJavaDurationConversion), + builder.isEnabled(NewStrictNullChecks), ) override fun setupModule(context: SetupContext) { @@ -95,7 +111,7 @@ class KotlinModule private constructor( val cache = ReflectionCache(reflectionCacheSize) - context.addValueInstantiators(KotlinInstantiators(cache, nullToEmptyCollection, nullToEmptyMap, nullIsSameAsDefault, strictNullChecks)) + context.addValueInstantiators(KotlinInstantiators(cache, nullToEmptyCollection, nullToEmptyMap, nullIsSameAsDefault, oldStrictNullChecks)) if (singletonSupport) { context.addBeanDeserializerModifier(KotlinBeanDeserializerModifier) @@ -109,7 +125,9 @@ class KotlinModule private constructor( nullIsSameAsDefault, useJavaDurationConversion )) - context.appendAnnotationIntrospector(KotlinNamesAnnotationIntrospector(cache, kotlinPropertyNameAsImplicitName)) + context.appendAnnotationIntrospector( + KotlinNamesAnnotationIntrospector(cache, newStrictNullChecks, kotlinPropertyNameAsImplicitName) + ) context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion)) context.addKeyDeserializers(KotlinKeyDeserializers) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt index 204ae7d4..877e24d4 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt @@ -1,6 +1,8 @@ package com.fasterxml.jackson.module.kotlin import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonSetter +import com.fasterxml.jackson.annotation.Nulls import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.cfg.MapperConfig import com.fasterxml.jackson.databind.introspect.Annotated @@ -12,8 +14,10 @@ import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector import com.fasterxml.jackson.databind.introspect.PotentialCreator import java.lang.reflect.Constructor import java.util.Locale +import kotlin.collections.getOrNull import kotlin.reflect.KClass import kotlin.reflect.KFunction +import kotlin.reflect.KParameter import kotlin.reflect.full.hasAnnotation import kotlin.reflect.full.memberProperties import kotlin.reflect.full.primaryConstructor @@ -22,6 +26,7 @@ import kotlin.reflect.jvm.javaType internal class KotlinNamesAnnotationIntrospector( private val cache: ReflectionCache, + private val strictNullChecks: Boolean, private val kotlinPropertyNameAsImplicitName: Boolean ) : NopAnnotationIntrospector() { private fun getterNameFromJava(member: AnnotatedMethod): String? { @@ -73,16 +78,26 @@ internal class KotlinNamesAnnotationIntrospector( } override fun refineDeserializationType(config: MapperConfig<*>, a: Annotated, baseType: JavaType): JavaType = - (a as? AnnotatedParameter)?.let { _ -> - cache.findKotlinParameter(a)?.let { param -> - val rawType = a.rawType - (param.type.classifier as? KClass<*>) - ?.java - ?.takeIf { it.isUnboxableValueClass() && it != rawType } - ?.let { config.constructType(it) } - } + findKotlinParameter(a)?.let { param -> + val rawType = a.rawType + (param.type.classifier as? KClass<*>) + ?.java + ?.takeIf { it.isUnboxableValueClass() && it != rawType } + ?.let { config.constructType(it) } } ?: baseType + override fun findSetterInfo(ann: Annotated): JsonSetter.Value = ann.takeIf { strictNullChecks } + ?.let { _ -> + findKotlinParameter(ann)?.let { param -> + if (param.requireStrictNullCheck(ann.type)) { + JsonSetter.Value.forContentNulls(Nulls.FAIL) + } else { + null + } + } + } + ?: super.findSetterInfo(ann) + override fun findDefaultCreator( config: MapperConfig<*>, valueClass: AnnotatedClass, @@ -106,8 +121,18 @@ internal class KotlinNamesAnnotationIntrospector( } private fun findKotlinParameterName(param: AnnotatedParameter): String? = cache.findKotlinParameter(param)?.name + + private fun findKotlinParameter(param: Annotated) = (param as? AnnotatedParameter) + ?.let { cache.findKotlinParameter(it) } } +private fun KParameter.markedNonNullAt(index: Int) = type.arguments.getOrNull(index)?.type?.isMarkedNullable == false + +private fun KParameter.requireStrictNullCheck(type: JavaType): Boolean = + ((type.isArrayType || type.isCollectionLikeType) && this.markedNonNullAt(0)) || + (type.isMapLikeType && this.markedNonNullAt(1)) + + // If it is not a Kotlin class or an Enum, Creator is not used private fun AnnotatedClass.creatableKotlinClass(): KClass<*>? = annotated .takeIf { it.isKotlinClass() && !it.isEnum } diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/DslTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/DslTest.kt index 018309b7..d448a3ef 100644 --- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/DslTest.kt +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/DslTest.kt @@ -6,7 +6,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullIsSameAsDefault import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyCollection import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyMap import com.fasterxml.jackson.module.kotlin.KotlinFeature.SingletonSupport -import com.fasterxml.jackson.module.kotlin.KotlinFeature.StrictNullChecks +import com.fasterxml.jackson.module.kotlin.KotlinFeature.NewStrictNullChecks import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Test import kotlin.test.assertEquals @@ -35,7 +35,7 @@ class DslTest { enable(NullToEmptyMap) enable(NullIsSameAsDefault) enable(SingletonSupport) - enable(StrictNullChecks) + enable(NewStrictNullChecks) } assertNotNull(module) diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModuleTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModuleTest.kt index f885ebd9..3c90dba0 100644 --- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModuleTest.kt +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModuleTest.kt @@ -6,9 +6,24 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import kotlin.test.assertNotNull class KotlinModuleTest { + // After the final migration is complete, this test will be removed. + @Test + fun strictNullChecksTests() { + assertTrue(kotlinModule { enable(StrictNullChecks) }.strictNullChecks) + assertTrue(kotlinModule { enable(NewStrictNullChecks) }.strictNullChecks) + + assertThrows { + kotlinModule { + enable(StrictNullChecks) + enable(NewStrictNullChecks) + } + } + } + @Test fun builder_Defaults() { val module = KotlinModule.Builder().build() diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/StrictNullChecksTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/StrictNullChecksTest.kt new file mode 100644 index 00000000..d0d74f09 --- /dev/null +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/StrictNullChecksTest.kt @@ -0,0 +1,139 @@ +package com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser + +import com.fasterxml.jackson.annotation.JsonSetter +import com.fasterxml.jackson.annotation.Nulls +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.exc.InvalidNullException +import com.fasterxml.jackson.module.kotlin.KotlinFeature +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.readValue +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class StrictNullChecksTest { + val mapper: ObjectMapper = ObjectMapper() + .registerModule( + KotlinModule.Builder() + .enable(KotlinFeature.NewStrictNullChecks) + .build() + ) + + class ArrayWrapper(val value: Array) + data class ListWrapper(val value: List) + data class MapWrapper(val value: Map) + + @Nested + inner class NonNullInput { + @Test + fun array() { + val expected = ArrayWrapper(arrayOf(1)) + val src = mapper.writeValueAsString(expected) + val result = mapper.readValue(src) + + Assertions.assertArrayEquals(expected.value, result.value) + } + + @Test + fun list() { + val expected = ListWrapper(listOf(1)) + val src = mapper.writeValueAsString(expected) + val result = mapper.readValue(src) + + Assertions.assertEquals(expected, result) + } + + @Test + fun map() { + val expected = MapWrapper(mapOf("foo" to 1)) + val src = mapper.writeValueAsString(expected) + val result = mapper.readValue(src) + + Assertions.assertEquals(expected, result) + } + } + + data class AnyWrapper(val value: Any) + + @Nested + inner class NullInput { + @Test + fun array() { + val src = mapper.writeValueAsString(AnyWrapper(arrayOf(null))) + assertThrows { mapper.readValue(src) } + } + + @Test + fun list() { + val src = mapper.writeValueAsString(AnyWrapper(arrayOf(null))) + assertThrows { mapper.readValue(src) } + } + + @Test + fun map() { + val src = mapper.writeValueAsString(AnyWrapper(mapOf("foo" to null))) + assertThrows { mapper.readValue(src) } + } + } + + class ContentNullsSkipArrayWrapper(@JsonSetter(contentNulls = Nulls.SKIP) val value: Array) + data class ContentNullsSkipListWrapper(@JsonSetter(contentNulls = Nulls.SKIP) val value: List) + data class ContentNullsSkipMapWrapper(@JsonSetter(contentNulls = Nulls.SKIP) val value: Map) + + @Nested + inner class CustomByAnnotationTest { + @Test + fun array() { + val expected = ContentNullsSkipArrayWrapper(emptyArray()) + val src = mapper.writeValueAsString(AnyWrapper(arrayOf(null))) + val result = mapper.readValue(src) + + Assertions.assertArrayEquals(expected.value, result.value) + } + + @Test + fun list() { + val expected = ContentNullsSkipListWrapper(emptyList()) + val src = mapper.writeValueAsString(AnyWrapper(listOf(null))) + val result = mapper.readValue(src) + + Assertions.assertEquals(expected, result) + } + + @Test + fun map() { + val expected = ContentNullsSkipMapWrapper(emptyMap()) + val src = mapper.writeValueAsString(AnyWrapper(mapOf("foo" to null))) + val result = mapper.readValue(src) + + Assertions.assertEquals(expected, result) + } + } + + class AnnotatedArrayWrapper(@JsonSetter(nulls = Nulls.SKIP) val value: Array = emptyArray()) + data class AnnotatedListWrapper(@JsonSetter(nulls = Nulls.SKIP) val value: List = emptyList()) + data class AnnotatedMapWrapper(@JsonSetter(nulls = Nulls.SKIP) val value: Map = emptyMap()) + + // If Default is specified by annotation, it is not overridden. + @Nested + inner class AnnotatedNullInput { + @Test + fun array() { + val src = mapper.writeValueAsString(AnyWrapper(arrayOf(null))) + assertThrows { mapper.readValue(src) } + } + + @Test + fun list() { + val src = mapper.writeValueAsString(AnyWrapper(arrayOf(null))) + assertThrows { mapper.readValue(src) } + } + + @Test + fun map() { + val src = mapper.writeValueAsString(AnyWrapper(mapOf("foo" to null))) + assertThrows { mapper.readValue(src) } + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/StrictNullChecksTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/StrictNullChecksTest.kt index ebf1ad61..2681e846 100644 --- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/StrictNullChecksTest.kt +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/StrictNullChecksTest.kt @@ -1,8 +1,8 @@ package com.fasterxml.jackson.module.kotlin.test import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.KotlinFeature.StrictNullChecks -import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException +import com.fasterxml.jackson.databind.exc.InvalidNullException +import com.fasterxml.jackson.module.kotlin.KotlinFeature.NewStrictNullChecks import com.fasterxml.jackson.module.kotlin.kotlinModule import com.fasterxml.jackson.module.kotlin.readValue import org.junit.jupiter.api.Assertions.assertArrayEquals @@ -13,7 +13,7 @@ import org.junit.jupiter.api.assertThrows import kotlin.test.assertNull class StrictNullChecksTest { - private val mapper = ObjectMapper().registerModule(kotlinModule { enable(StrictNullChecks) }) + private val mapper = ObjectMapper().registerModule(kotlinModule { enable(NewStrictNullChecks) }) /** collection tests */ @@ -30,7 +30,7 @@ class StrictNullChecksTest { @Test fun testListOfInt() { - assertThrows { + assertThrows { val json = """{"samples":[1, null]}""" mapper.readValue(json) } @@ -60,7 +60,7 @@ class StrictNullChecksTest { @Test fun testArrayOfInt() { - assertThrows { + assertThrows { val json = """{"samples":[1, null]}""" mapper.readValue(json) } @@ -90,7 +90,7 @@ class StrictNullChecksTest { @Test fun testMapOfStringToIntWithNullValue() { - assertThrows { + assertThrows { val json = """{ "samples": { "key": null } }""" mapper.readValue(json) } @@ -119,7 +119,7 @@ class StrictNullChecksTest { @Disabled // this is a hard problem to solve and is currently not addressed @Test fun testListOfGenericWithNullValue() { - assertThrows { + assertThrows { val json = """{"samples":[1, null]}""" mapper.readValue>>(json) } @@ -135,7 +135,7 @@ class StrictNullChecksTest { @Disabled // this is a hard problem to solve and is currently not addressed @Test fun testMapOfGenericWithNullValue() { - assertThrows { + assertThrows { val json = """{ "samples": { "key": null } }""" mapper.readValue>>(json) } @@ -151,7 +151,7 @@ class StrictNullChecksTest { @Disabled // this is a hard problem to solve and is currently not addressed @Test fun testArrayOfGenericWithNullValue() { - assertThrows { + assertThrows { val json = """{"samples":[1, null]}""" mapper.readValue>>(json) } diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/StrictNullChecksTestOld.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/StrictNullChecksTestOld.kt new file mode 100644 index 00000000..bbb15895 --- /dev/null +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/StrictNullChecksTestOld.kt @@ -0,0 +1,159 @@ +package com.fasterxml.jackson.module.kotlin.test + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinFeature.StrictNullChecks +import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException +import com.fasterxml.jackson.module.kotlin.kotlinModule +import com.fasterxml.jackson.module.kotlin.readValue +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertNull + +class StrictNullChecksTestOld { + private val mapper = ObjectMapper().registerModule(kotlinModule { enable(StrictNullChecks) }) + + /** collection tests */ + + private data class ClassWithListOfNullableInt(val samples: List) + + @Test + fun testListOfNullableInt() { + val json = """{"samples":[1, null]}""" + val stateObj = mapper.readValue(json) + assertEquals(listOf(1, null), stateObj.samples) + } + + private data class ClassWithListOfInt(val samples: List) + + @Test + fun testListOfInt() { + assertThrows { + val json = """{"samples":[1, null]}""" + mapper.readValue(json) + } + } + + private data class ClassWithNullableListOfInt(val samples: List?) + + @Test + fun testNullableListOfInt() { + val json = """{"samples": null}""" + val stateObj = mapper.readValue(json) + assertNull(stateObj.samples) + } + + /** array tests */ + + private data class ClassWithArrayOfNullableInt(val samples: Array) + + @Test + fun testArrayOfNullableInt() { + val json = """{"samples":[1, null]}""" + val stateObj = mapper.readValue(json) + assertArrayEquals(arrayOf(1, null), stateObj.samples) + } + + private data class ClassWithArrayOfInt(val samples: Array) + + @Test + fun testArrayOfInt() { + assertThrows { + val json = """{"samples":[1, null]}""" + mapper.readValue(json) + } + } + + private data class ClassWithNullableArrayOfInt(val samples: Array?) + + @Test + fun testNullableArrayOfInt() { + val json = """{"samples": null}""" + val stateObj = mapper.readValue(json) + assertNull(stateObj.samples) + } + + /** map tests */ + + private data class ClassWithMapOfStringToNullableInt(val samples: Map) + + @Test + fun testMapOfStringToNullableInt() { + val json = """{ "samples": { "key": null } }""" + val stateObj = mapper.readValue(json) + assertEquals(mapOf("key" to null), stateObj.samples) + } + + private data class ClassWithMapOfStringToInt(val samples: Map) + + @Test + fun testMapOfStringToIntWithNullValue() { + assertThrows { + val json = """{ "samples": { "key": null } }""" + mapper.readValue(json) + } + } + + private data class ClassWithNullableMapOfStringToInt(val samples: Map?) + + @Test + fun testNullableMapOfStringToInt() { + val json = """{"samples": null}""" + val stateObj = mapper.readValue(json) + assertNull(stateObj.samples) + } + + /** generics test */ + + private data class TestClass(val samples: T) + + @Test + fun testListOfGeneric() { + val json = """{"samples":[1, 2]}""" + val stateObj = mapper.readValue>>(json) + assertEquals(listOf(1, 2), stateObj.samples) + } + + @Disabled // this is a hard problem to solve and is currently not addressed + @Test + fun testListOfGenericWithNullValue() { + assertThrows { + val json = """{"samples":[1, null]}""" + mapper.readValue>>(json) + } + } + + @Test + fun testMapOfGeneric() { + val json = """{ "samples": { "key": 1 } }""" + val stateObj = mapper.readValue>>(json) + assertEquals(mapOf("key" to 1), stateObj.samples) + } + + @Disabled // this is a hard problem to solve and is currently not addressed + @Test + fun testMapOfGenericWithNullValue() { + assertThrows { + val json = """{ "samples": { "key": null } }""" + mapper.readValue>>(json) + } + } + + @Test + fun testArrayOfGeneric() { + val json = """{"samples":[1, 2]}""" + val stateObj = mapper.readValue>>(json) + assertArrayEquals(arrayOf(1, 2), stateObj.samples) + } + + @Disabled // this is a hard problem to solve and is currently not addressed + @Test + fun testArrayOfGenericWithNullValue() { + assertThrows { + val json = """{"samples":[1, null]}""" + mapper.readValue>>(json) + } + } +}