From 191126da475d92f99476e0406758176fcbbe71c8 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 9 Aug 2025 19:01:52 +0900 Subject: [PATCH 1/2] Fix to throw a new exception from https://github.com/FasterXML/jackson-module-kotlin/pull/1025 closes #375 --- .../kogera/KotlinInvalidNullException.java | 63 +++++++++++++++++++ .../jackson/module/kogera/InternalCommons.kt | 11 ++++ .../KotlinValueInstantiator.kt | 18 ++++-- 3 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 src/main/java/io/github/projectmapk/jackson/module/kogera/KotlinInvalidNullException.java diff --git a/src/main/java/io/github/projectmapk/jackson/module/kogera/KotlinInvalidNullException.java b/src/main/java/io/github/projectmapk/jackson/module/kogera/KotlinInvalidNullException.java new file mode 100644 index 00000000..5a133b17 --- /dev/null +++ b/src/main/java/io/github/projectmapk/jackson/module/kogera/KotlinInvalidNullException.java @@ -0,0 +1,63 @@ +package io.github.projectmapk.jackson.module.kogera; + +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; + +// 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( + @NotNull + String kotlinParameterName, + @NotNull + Class valueClass, + @NotNull + JsonParser p, + @NotNull + String msg, + @NotNull + PropertyName pname + ) { + super(p, msg, pname); + this.kotlinPropertyName = 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/io/github/projectmapk/jackson/module/kogera/InternalCommons.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/InternalCommons.kt index ab528715..37b73734 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/InternalCommons.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/InternalCommons.kt @@ -2,6 +2,8 @@ package io.github.projectmapk.jackson.module.kogera import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.PropertyName import io.github.projectmapk.jackson.module.kogera.annotation.JsonKUnbox import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles @@ -126,3 +128,12 @@ internal fun Method.toSignature(): JvmMethodSignature = JvmMethodSignature( this.name, parameterTypes.toDescBuilder().appendDescriptor(this.returnType).toString(), ) + +// Delegate for calling package-private constructor +internal fun kotlinInvalidNullException( + kotlinParameterName: String, + valueClass: Class<*>, + p: JsonParser, + msg: String, + pname: PropertyName, +) = KotlinInvalidNullException(kotlinParameterName, valueClass, p, msg, pname) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/KotlinValueInstantiator.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/KotlinValueInstantiator.kt index 9331ca4a..9a449424 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/KotlinValueInstantiator.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/KotlinValueInstantiator.kt @@ -11,13 +11,13 @@ import com.fasterxml.jackson.databind.deser.SettableBeanProperty import com.fasterxml.jackson.databind.deser.ValueInstantiator import com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator -import com.fasterxml.jackson.databind.exc.InvalidNullException import com.fasterxml.jackson.databind.module.SimpleValueInstantiators import io.github.projectmapk.jackson.module.kogera.ReflectionCache import io.github.projectmapk.jackson.module.kogera.deser.WrapsNullableValueClassDeserializer import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator.ConstructorValueCreator import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator.MethodValueCreator import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator.ValueCreator +import io.github.projectmapk.jackson.module.kogera.kotlinInvalidNullException import java.lang.reflect.Constructor import java.lang.reflect.Executable import java.lang.reflect.Method @@ -99,9 +99,19 @@ internal class KotlinValueInstantiator( } else { val isMissingAndRequired = isMissing && jsonProp.isRequired if (isMissingAndRequired || !(paramDef.isNullable || paramDef.isGenericType)) { - throw InvalidNullException - .from(ctxt, jsonProp.fullName, jsonProp.type) - .wrapWithPath(this.valueClass, jsonProp.name) + val kotlinParameterName = paramDef.name + val pname = jsonProp.name + val message = "Instantiation of ${this.valueTypeDesc} value failed for JSON property" + + " $pname due to missing (therefore NULL) value for creator parameter" + + " $kotlinParameterName which is a non-nullable type" + + throw kotlinInvalidNullException( + kotlinParameterName, + this.valueClass, + ctxt.parser, + message, + jsonProp.fullName, + ).wrapWithPath(this.valueClass, pname) } } } From ae03e3bd9f0a218f5b1299e15c683ed9c1c9925d Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 9 Aug 2025 19:12:23 +0900 Subject: [PATCH 2/2] Fix and add tests --- .../kogera/KotlinInvalidNullExceptionTest.kt | 34 +++++++++++++++++++ .../kogera/zPorted/test/NullToDefaultTests.kt | 7 ++-- .../kogera/zPorted/test/github/GitHub917.kt | 4 +-- .../kogera/zPorted/test/github/Github168.kt | 4 +-- .../kogera/zPorted/test/github/Github32.kt | 14 ++++---- 5 files changed, 48 insertions(+), 15 deletions(-) create mode 100644 src/test/kotlin/io/github/projectmapk/jackson/module/kogera/KotlinInvalidNullExceptionTest.kt diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/KotlinInvalidNullExceptionTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/KotlinInvalidNullExceptionTest.kt new file mode 100644 index 00000000..4343c9c8 --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/KotlinInvalidNullExceptionTest.kt @@ -0,0 +1,34 @@ +package io.github.projectmapk.jackson.module.kogera + +import com.fasterxml.jackson.annotation.JsonProperty +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +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/io/github/projectmapk/jackson/module/kogera/zPorted/test/NullToDefaultTests.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/NullToDefaultTests.kt index b71511a3..9a6809e1 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/NullToDefaultTests.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/NullToDefaultTests.kt @@ -1,8 +1,8 @@ package io.github.projectmapk.jackson.module.kogera.zPorted.test import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.exc.InvalidNullException import io.github.projectmapk.jackson.module.kogera.KotlinFeature.NullIsSameAsDefault +import io.github.projectmapk.jackson.module.kogera.KotlinInvalidNullException import io.github.projectmapk.jackson.module.kogera.kotlinModule import io.github.projectmapk.jackson.module.kogera.readValue import org.junit.jupiter.api.Assertions.assertTrue @@ -55,7 +55,7 @@ class TestNullToDefault { @Test fun shouldNotUseNullAsDefault() { - assertThrows { + assertThrows { createMapper(false).readValue( """{ "sku": "974", @@ -68,10 +68,9 @@ class TestNullToDefault { } } - // @Test(expected = MissingKotlinParameterException::class) @Test fun errorIfNotDefault() { - assertThrows { + assertThrows { createMapper(true).readValue( """{ "sku": "974", diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub917.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub917.kt index 96e4cf86..ce3645fc 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub917.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub917.kt @@ -3,7 +3,7 @@ package io.github.projectmapk.jackson.module.kogera.zPorted.test.github import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.OptBoolean -import com.fasterxml.jackson.databind.exc.InvalidNullException +import io.github.projectmapk.jackson.module.kogera.KotlinInvalidNullException import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper import io.github.projectmapk.jackson.module.kogera.readValue import org.junit.jupiter.api.Assertions.assertEquals @@ -20,7 +20,7 @@ class GitHub917 { val value = Failing(null) val json = mapper.writeValueAsString(value) - assertThrows { + assertThrows { val deserializedValue = mapper.readValue>(json) assertEquals(value ,deserializedValue) } diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github168.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github168.kt index a19b4d0d..28f4c654 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github168.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github168.kt @@ -1,7 +1,7 @@ package io.github.projectmapk.jackson.module.kogera.zPorted.test.github import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.databind.exc.InvalidNullException +import io.github.projectmapk.jackson.module.kogera.KotlinInvalidNullException import io.github.projectmapk.jackson.module.kogera.defaultMapper import io.github.projectmapk.jackson.module.kogera.readValue import org.junit.jupiter.api.Assertions.assertEquals @@ -20,7 +20,7 @@ class TestGithub168 { @Test fun testIfRequiredIsReallyRequiredWhenAbsent() { - assertThrows { + assertThrows { defaultMapper.readValue("""{"baz":"whatever"}""") } } diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github32.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github32.kt index 3cbb47b9..8c4296a7 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github32.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github32.kt @@ -1,7 +1,7 @@ package io.github.projectmapk.jackson.module.kogera.zPorted.test.github import com.fasterxml.jackson.databind.JsonMappingException -import com.fasterxml.jackson.databind.exc.InvalidNullException +import io.github.projectmapk.jackson.module.kogera.KotlinInvalidNullException import io.github.projectmapk.jackson.module.kogera.defaultMapper import io.github.projectmapk.jackson.module.kogera.readValue import org.junit.jupiter.api.Assertions.assertEquals @@ -22,8 +22,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( """ @@ -40,7 +40,7 @@ private class TestGithub32 { } @Test fun `null mandatory data class constructor param`() { - val thrown = assertThrows { + val thrown = assertThrows { defaultMapper.readValue( """ { @@ -57,7 +57,7 @@ private class TestGithub32 { } @Test fun `missing mandatory constructor param - nested in class with default constructor`() { - val thrown = assertThrows { + val thrown = assertThrows { defaultMapper.readValue( """ { @@ -75,7 +75,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( """ { @@ -93,7 +93,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( """ {