From b201287199b575f593a134c059b677dd8ebb6645 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 27 Jul 2025 21:58:41 +0900 Subject: [PATCH 1/8] Changed to throw a new exception instead of using the old exception closes #617 --- .../kotlin/KotlinInvalidNullException.java | 65 +++++++++++++++++++ .../module/kotlin/KotlinValueInstantiator.kt | 13 ++-- 2 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/fasterxml/jackson/module/kotlin/KotlinInvalidNullException.java 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..8f7b5ab82 --- /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; + +// Since there was no way to override Java getter in a user-friendly way, it is defined in Java. See KT-6653. +// 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/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) { From 279e639fbc9778047712bb63c1c33166bfc029d5 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 27 Jul 2025 22:01:48 +0900 Subject: [PATCH 2/8] Fix tests --- .../jackson/module/kotlin/test/NullToDefaultTests.kt | 4 ++-- .../fasterxml/jackson/module/kotlin/test/github/Github168.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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) } From 2630290458495d1cc07fa15419e79176333bf54f Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 27 Jul 2025 22:04:53 +0900 Subject: [PATCH 3/8] Update docs --- .../fasterxml/jackson/module/kotlin/Exceptions.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) 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. From 4d39dd4a83d33a72e48f391c3362ee7d3a81a483 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 27 Jul 2025 22:16:47 +0900 Subject: [PATCH 4/8] Add tests --- .../kotlin/KotlinInvalidNullExceptionTest.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinInvalidNullExceptionTest.kt 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) + } +} From 25b940e78b70df6cd6b5b02d28339e824e50041f Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 27 Jul 2025 22:23:11 +0900 Subject: [PATCH 5/8] Fix comment wrt https://github.com/FasterXML/jackson-module-kotlin/pull/1025#discussion_r2233980707 --- .../jackson/module/kotlin/KotlinInvalidNullException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/fasterxml/jackson/module/kotlin/KotlinInvalidNullException.java b/src/main/java/com/fasterxml/jackson/module/kotlin/KotlinInvalidNullException.java index 8f7b5ab82..2883e4660 100644 --- a/src/main/java/com/fasterxml/jackson/module/kotlin/KotlinInvalidNullException.java +++ b/src/main/java/com/fasterxml/jackson/module/kotlin/KotlinInvalidNullException.java @@ -7,7 +7,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -// Since there was no way to override Java getter in a user-friendly way, it is defined in Java. See KT-6653. +// 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 From e2e6b487e0d48f66d659c317313414fc6670d458 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 27 Jul 2025 22:25:00 +0900 Subject: [PATCH 6/8] Suppress DEPRECATION_ERROR --- .../jackson/module/kotlin/MissingKotlinParameterExceptionTest.kt | 1 + 1 file changed, 1 insertion(+) 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() From 4ab43080b25ae0c86f7711d6cbc30c5979bdeb70 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 3 Aug 2025 12:41:58 +0900 Subject: [PATCH 7/8] Fix to test that KotlinInvalidNullException is thrown --- .../jackson/module/kotlin/test/github/Github32.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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": [ From 50f5527f0e6a8ef28ba81a8cf3c55c86ee64a78f Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 3 Aug 2025 12:47:57 +0900 Subject: [PATCH 8/8] Update release notes wrt #1025 --- release-notes/CREDITS-2.x | 1 + release-notes/VERSION-2.x | 5 +++++ 2 files changed, 6 insertions(+) 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.