diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index cb72c754c..9af9b8ef5 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -18,6 +18,7 @@ Contributors: # 2.20.0 (not yet released) WrongWrong (@k163377) +* #1025: Deprecate MissingKotlinParameterException and replace with new exception * #1020: Fixed old StrictNullChecks to throw exceptions similar to those thrown by new StrictNullChecks * #1018: Use MethodHandle in processing related to value class * #969: Cleanup of deprecated contents diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 9a161f2c2..e214aef99 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -17,6 +17,11 @@ Co-maintainers: ------------------------------------------------------------------------ 2.20.0 (not yet released) +#1025: When a null is entered for a non-null parameter, the KotlinInvalidNullException is now thrown instead of the + deprecated MissingKotlinParameterException. + The new exception is a subclass of InvalidNullException. + See the comment below for information contained in this exception. + https://github.com/FasterXML/jackson-module-kotlin/issues/617#issuecomment-3124423585 #1020: Exceptions thrown by the old StrictNullChecks are now the similar to the new StrictNullChecks. This means that the old StrictNullChecks will no longer throw MissingKotlinParameterException. See PR for what is thrown and how error messages change. diff --git a/src/main/java/com/fasterxml/jackson/module/kotlin/KotlinInvalidNullException.java b/src/main/java/com/fasterxml/jackson/module/kotlin/KotlinInvalidNullException.java new file mode 100644 index 000000000..2883e4660 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/module/kotlin/KotlinInvalidNullException.java @@ -0,0 +1,65 @@ +package com.fasterxml.jackson.module.kotlin; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.PropertyName; +import com.fasterxml.jackson.databind.exc.InvalidNullException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +// Due to a limitation in KT-6653, there is no user-friendly way to override Java getters in Kotlin. +// The reason for not having detailed information(e.g. KParameter) is to keep the class Serializable. +/** + * Specialized {@link JsonMappingException} sub-class used to indicate that a mandatory Kotlin creator parameter was + * missing or null. + */ +public final class KotlinInvalidNullException extends InvalidNullException { + @NotNull + private final String kotlinPropertyName; + + KotlinInvalidNullException( + @Nullable + String kotlinParameterName, + @NotNull + Class valueClass, + @NotNull + JsonParser p, + @NotNull + String msg, + @NotNull + PropertyName pname + ) { + super(p, msg, pname); + // Basically, this will never be null, but it is handled here to avoid errors in unusual cases. + this.kotlinPropertyName = kotlinParameterName == null ? "UNKNOWN" : kotlinParameterName; + this._targetType = valueClass; + } + + /** + * @return Parameter name in Kotlin. + */ + @NotNull + public String getKotlinPropertyName() { + return kotlinPropertyName; + } + + // region: Override getters to make nullability explicit and to explain its role in this class. + /** + * @return Parameter name in Jackson. + */ + @NotNull + @Override + public PropertyName getPropertyName() { + return super.getPropertyName(); + } + + /** + * @return The {@link Class} object representing the class that declares the creator. + */ + @NotNull + @Override + public Class getTargetType() { + return super.getTargetType(); + } + // endregion +} diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Exceptions.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Exceptions.kt index fbd70c401..cbea39604 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Exceptions.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Exceptions.kt @@ -10,14 +10,10 @@ import kotlin.reflect.KParameter * parameter was missing or null. */ @Deprecated( - "It is recommended that InvalidNullException be referenced when possible," + - " as the change is discussed for 2.20 and later." + - " See #617 for details.", - ReplaceWith( - "InvalidNullException", - "com.fasterxml.jackson.databind.exc.InvalidNullException" - ), - DeprecationLevel.WARNING + "Since 2.20, this exception is no longer thrown and has been replaced by KotlinInvalidNullException. " + + "See #617 for details.", + ReplaceWith("KotlinInvalidNullException"), + DeprecationLevel.ERROR ) // When deserialized by the JDK, the parameter property will be null, ignoring nullability. // This is a temporary workaround for #572 and we will eventually remove this class. diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt index 27e6f0dcc..99fbaab76 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt @@ -90,15 +90,18 @@ internal class KotlinValueInstantiator( if (propType.requireEmptyValue()) { paramVal = valueDeserializer!!.getEmptyValue(ctxt) } else { + val pname = jsonProp.name val isMissingAndRequired = isMissing && jsonProp.isRequired // Since #310 reported that the calculation cost is high, isGenericTypeVar is determined last. if (isMissingAndRequired || (!paramType.isMarkedNullable && !paramType.isGenericTypeVar())) { - throw MissingKotlinParameterException( - parameter = paramDef, - processor = ctxt.parser, - msg = "Instantiation of ${this.valueTypeDesc} value failed for JSON property ${jsonProp.name} due to missing (therefore NULL) value for creator parameter ${paramDef.name} which is a non-nullable type" - ).wrapWithPath(this.valueClass, jsonProp.name) + throw KotlinInvalidNullException( + paramDef.name, + this.valueClass, + ctxt.parser, + "Instantiation of ${this.valueTypeDesc} value failed for JSON property $pname due to missing (therefore NULL) value for creator parameter ${paramDef.name} which is a non-nullable type", + jsonProp.fullName, + ).wrapWithPath(this.valueClass, pname) } } } else if (strictNullChecks) { diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinInvalidNullExceptionTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinInvalidNullExceptionTest.kt new file mode 100644 index 000000000..d52d2d87f --- /dev/null +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinInvalidNullExceptionTest.kt @@ -0,0 +1,34 @@ +package com.fasterxml.jackson.module.kotlin + +import com.fasterxml.jackson.annotation.JsonProperty +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals + +private data class Dto( + val foo: String, + @JsonProperty("bar") + val _bar: String +) + +class KotlinInvalidNullExceptionTest { + @Test + fun fooTest() { + val json = """{"bar":"bar"}""" + val ex = assertThrows { defaultMapper.readValue(json) } + + assertEquals("foo", ex.kotlinPropertyName) + assertEquals("foo", ex.propertyName.simpleName) + assertEquals(Dto::class, ex.targetType.kotlin) + } + + @Test + fun barTest() { + val json = """{"foo":"foo","bar":null}""" + val ex = assertThrows { defaultMapper.readValue(json) } + + assertEquals("_bar", ex.kotlinPropertyName) + assertEquals("bar", ex.propertyName.simpleName) + assertEquals(Dto::class, ex.targetType.kotlin) + } +} diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/MissingKotlinParameterExceptionTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/MissingKotlinParameterExceptionTest.kt index 713cb6614..af3eacfc8 100644 --- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/MissingKotlinParameterExceptionTest.kt +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/MissingKotlinParameterExceptionTest.kt @@ -5,6 +5,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull class MissingKotlinParameterExceptionTest { + @Suppress("DEPRECATION_ERROR") @Test fun jdkSerializabilityTest() { val param = ::MissingKotlinParameterException.parameters.first() diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/NullToDefaultTests.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/NullToDefaultTests.kt index 5e8dd818b..610e5aa1f 100644 --- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/NullToDefaultTests.kt +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/NullToDefaultTests.kt @@ -2,7 +2,7 @@ package com.fasterxml.jackson.module.kotlin.test import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullIsSameAsDefault -import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException +import com.fasterxml.jackson.module.kotlin.KotlinInvalidNullException import com.fasterxml.jackson.module.kotlin.kotlinModule import com.fasterxml.jackson.module.kotlin.readValue import org.junit.jupiter.api.Assertions.* @@ -142,7 +142,7 @@ class TestNullToDefault { @Test fun shouldThrowExceptionWhenProvidedNullForNotNullFieldWithoutDefault() { - assertThrows { + assertThrows { createMapper(true).readValue( """{ "text": null diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github168.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github168.kt index 64c16c45c..db1eb8cb3 100644 --- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github168.kt +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github168.kt @@ -1,7 +1,7 @@ package com.fasterxml.jackson.module.kotlin.test.github import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException +import com.fasterxml.jackson.module.kotlin.KotlinInvalidNullException import com.fasterxml.jackson.module.kotlin.defaultMapper import com.fasterxml.jackson.module.kotlin.readValue import org.junit.jupiter.api.Test @@ -20,7 +20,7 @@ class TestGithub168 { @Test fun testIfRequiredIsReallyRequiredWhenAbsent() { - assertThrows { + assertThrows { val obj = defaultMapper.readValue("""{"baz":"whatever"}""") assertEquals("whatever", obj.baz) } diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github32.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github32.kt index 1cacc9dad..2c6b613bc 100644 --- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github32.kt +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github32.kt @@ -1,7 +1,7 @@ package com.fasterxml.jackson.module.kotlin.test.github import com.fasterxml.jackson.databind.JsonMappingException -import com.fasterxml.jackson.databind.exc.MismatchedInputException +import com.fasterxml.jackson.module.kotlin.KotlinInvalidNullException import com.fasterxml.jackson.module.kotlin.defaultMapper import com.fasterxml.jackson.module.kotlin.readValue import org.junit.jupiter.api.Assertions.assertEquals @@ -19,8 +19,8 @@ private class TestGithub32 { } @Test fun `missing mandatory data class constructor param`() { - val thrown = assertThrows( - "MissingKotlinParameterException with missing `firstName` parameter" + val thrown = assertThrows( + "KotlinInvalidNullException with missing `firstName` parameter" ) { defaultMapper.readValue(""" { @@ -35,7 +35,7 @@ private class TestGithub32 { } @Test fun `null mandatory data class constructor param`() { - val thrown = assertThrows { + val thrown = assertThrows { defaultMapper.readValue(""" { "firstName": null, @@ -50,7 +50,7 @@ private class TestGithub32 { } @Test fun `missing mandatory constructor param - nested in class with default constructor`() { - val thrown = assertThrows { + val thrown = assertThrows { defaultMapper.readValue(""" { "person": { @@ -66,7 +66,7 @@ private class TestGithub32 { } @Test fun `missing mandatory constructor param - nested in class with single arg constructor`() { - val thrown = assertThrows { + val thrown = assertThrows { defaultMapper.readValue(""" { "person": { @@ -82,7 +82,7 @@ private class TestGithub32 { } @Test fun `missing mandatory constructor param - nested in class with List arg constructor`() { - val thrown = assertThrows { + val thrown = assertThrows { defaultMapper.readValue(""" { "people": [