From 01369cca996764610de09a3a4c4299a9acbf19e3 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 25 May 2025 11:38:30 +0900 Subject: [PATCH 01/21] Update kogera version --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 8c883c3d..d8d577a1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,7 +18,7 @@ val jacksonVersion = libs.versions.jackson.get() val generatedSrcPath = "${layout.buildDirectory.get()}/generated/kotlin" group = groupStr -version = "${jacksonVersion}-beta24" +version = "${jacksonVersion}-beta25" repositories { mavenCentral() From fc9fbc8389849a3df2c2fcdc40d047dbe72899f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 May 2025 10:47:04 +0000 Subject: [PATCH 02/21] Bump org.junit:junit-bom from 5.12.2 to 5.13.0 in the dependencies group Bumps the dependencies group with 1 update: [org.junit:junit-bom](https://github.com/junit-team/junit5). Updates `org.junit:junit-bom` from 5.12.2 to 5.13.0 - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.12.2...r5.13.0) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-version: 5.13.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index af4d776b..02f3d292 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.0.21" # Mainly for CI, it can be rewritten by environment variable. jackson = "2.19.0" # test libs -junit = "5.12.2" +junit = "5.13.0" [libraries] kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib" } From 50c5f6b681c2b2558f35e225d0d16342b2cf92fb Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 31 May 2025 16:33:34 +0900 Subject: [PATCH 03/21] Add comments --- .../KotlinFallbackAnnotationIntrospector.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinFallbackAnnotationIntrospector.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinFallbackAnnotationIntrospector.kt index ed302303..776780ce 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinFallbackAnnotationIntrospector.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinFallbackAnnotationIntrospector.kt @@ -80,7 +80,11 @@ internal class KotlinFallbackAnnotationIntrospector( override fun findSerializationConverter(a: Annotated): Converter<*, *>? = when (a) { // Find a converter to handle the case where the getter returns an unboxed value from the value class. is AnnotatedMethod -> cache.findBoxedReturnType(a.member)?.let { + // To make annotations that process JavaDuration work, + // it is necessary to set up the conversion to JavaDuration here. + // This conversion will cause the deserialization settings for KotlinDuration to be ignored. if (useJavaDurationConversion && it == KOTLIN_DURATION_CLASS) { + // For early return, the same process is placed as the branch regarding AnnotatedClass. if (a.rawReturnType == KOTLIN_DURATION_CLASS) { KotlinToJavaDurationConverter } else { From 2bc34193607c0a545ca0c1050a1aae496a58777a Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 31 May 2025 19:11:52 +0900 Subject: [PATCH 04/21] Add a test cases to serialize value classes in default --- .../WithoutCustomSerializeMethodTest.kt | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/WithoutCustomSerializeMethodTest.kt diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/WithoutCustomSerializeMethodTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/WithoutCustomSerializeMethodTest.kt new file mode 100644 index 00000000..8688dc6f --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/WithoutCustomSerializeMethodTest.kt @@ -0,0 +1,173 @@ +package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass + +import io.github.projectmapk.jackson.module.kogera.defaultMapper +import io.github.projectmapk.jackson.module.kogera.testPrettyWriter +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class WithoutCustomSerializeMethodTest { + @JvmInline + value class Primitive(val v: Int) + + @JvmInline + value class NonNullObject(val v: String) + + @JvmInline + value class NullableObject(val v: String?) + + @JvmInline + value class NullablePrimitive(val v: Int?) + + @JvmInline + value class TwoUnitPrimitive(val v: Long) + + private val writer = defaultMapper.testPrettyWriter() + + @Nested + inner class DirectSerializeTest { + @Test + fun primitive() { + val result = writer.writeValueAsString(Primitive(1)) + assertEquals("1", result) + } + + @Test + fun nonNullObject() { + val result = writer.writeValueAsString(NonNullObject("foo")) + assertEquals("\"foo\"", result) + } + + @Suppress("ClassName") + @Nested + inner class NullableObject_ { + @Test + fun value() { + val result = writer.writeValueAsString(NullableObject("foo")) + assertEquals("\"foo\"", result) + } + + @Test + fun nullValue() { + val result = writer.writeValueAsString(NullableObject(null)) + assertEquals("null", result) + } + } + + @Suppress("ClassName") + @Nested + inner class NullablePrimitive_ { + @Test + fun value() { + val result = writer.writeValueAsString(NullablePrimitive(1)) + assertEquals("1", result) + } + + @Test + fun nullValue() { + val result = writer.writeValueAsString(NullablePrimitive(null)) + assertEquals("null", result) + } + } + + @Test + fun twoUnitPrimitive() { + val result = writer.writeValueAsString(TwoUnitPrimitive(1)) + assertEquals("1", result) + } + } + + data class Src( + val pNn: Primitive, + val pN: Primitive?, + val nnoNn: NonNullObject, + val nnoN: NonNullObject?, + val noNn: NullableObject, + val noN: NullableObject?, + val npNn: NullablePrimitive, + val npN: NullablePrimitive?, + val tupNn: TwoUnitPrimitive, + val tupN: TwoUnitPrimitive?, + ) + + @Test + fun withoutNull() { + val src = Src( + Primitive(1), + Primitive(2), + NonNullObject("foo"), + NonNullObject("bar"), + NullableObject("baz"), + NullableObject("qux"), + NullablePrimitive(1), + NullablePrimitive(2), + TwoUnitPrimitive(3), + TwoUnitPrimitive(4), + ) + val result = writer.writeValueAsString(src) + + assertEquals( + """ + { + "pNn" : 1, + "pN" : 2, + "nnoNn" : "foo", + "nnoN" : "bar", + "noNn" : "baz", + "noN" : "qux", + "npNn" : 1, + "npN" : 2, + "tupNn" : 3, + "tupN" : 4 + } + """.trimIndent(), + result, + ) + } + + @Test + fun withNull() { + val src = Src( + Primitive(1), + null, + NonNullObject("foo"), + null, + NullableObject(null), + null, + NullablePrimitive(null), + null, + TwoUnitPrimitive(3), + null, + ) + val result = writer.writeValueAsString(src) + + assertEquals( + """ + { + "pNn" : 1, + "pN" : null, + "nnoNn" : "foo", + "nnoN" : null, + "noNn" : null, + "noN" : null, + "npNn" : null, + "npN" : null, + "tupNn" : 3, + "tupN" : null + } + """.trimIndent(), + result, + ) + } + + @JvmInline + value class HasToString(val value: Int) { + override fun toString(): String = "Custom($value)" + } + + @Test + fun toStringTest() { + val result = writer.writeValueAsString(HasToString(42)) + assertEquals("42", result) + } +} From 6e44f90a318296693c5659128a716f0a42b8cb55 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 31 May 2025 19:15:36 +0900 Subject: [PATCH 05/21] Fix redundant package names --- .../nullableObject/{byAnnotation => }/NonNullValueTest.kt | 2 +- .../nullableObject/{byAnnotation => }/NullValueTest.kt | 2 +- .../nullablePrimitive/{byAnnotation => }/NonNullValueTest.kt | 2 +- .../nullablePrimitive/{byAnnotation => }/NullValueTest.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/{byAnnotation => }/NonNullValueTest.kt (96%) rename src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/{byAnnotation => }/NullValueTest.kt (96%) rename src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/{byAnnotation => }/NonNullValueTest.kt (96%) rename src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/{byAnnotation => }/NullValueTest.kt (96%) diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/byAnnotation/NonNullValueTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/NonNullValueTest.kt similarity index 96% rename from src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/byAnnotation/NonNullValueTest.kt rename to src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/NonNullValueTest.kt index 269b6c35..b5b79d53 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/byAnnotation/NonNullValueTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/NonNullValueTest.kt @@ -1,4 +1,4 @@ -package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullableObject.byAnnotation +package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullableObject import com.fasterxml.jackson.databind.annotation.JsonSerialize import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/byAnnotation/NullValueTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/NullValueTest.kt similarity index 96% rename from src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/byAnnotation/NullValueTest.kt rename to src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/NullValueTest.kt index a7ac6a75..1e5c875f 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/byAnnotation/NullValueTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/NullValueTest.kt @@ -1,4 +1,4 @@ -package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullableObject.byAnnotation +package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullableObject import com.fasterxml.jackson.databind.annotation.JsonSerialize import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/byAnnotation/NonNullValueTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/NonNullValueTest.kt similarity index 96% rename from src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/byAnnotation/NonNullValueTest.kt rename to src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/NonNullValueTest.kt index d6ff8a05..e1410a02 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/byAnnotation/NonNullValueTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/NonNullValueTest.kt @@ -1,4 +1,4 @@ -package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullablePrimitive.byAnnotation +package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullablePrimitive import com.fasterxml.jackson.databind.annotation.JsonSerialize import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/byAnnotation/NullValueTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/NullValueTest.kt similarity index 96% rename from src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/byAnnotation/NullValueTest.kt rename to src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/NullValueTest.kt index 5edc4f32..72f1e392 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/byAnnotation/NullValueTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/NullValueTest.kt @@ -1,4 +1,4 @@ -package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullablePrimitive.byAnnotation +package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullablePrimitive import com.fasterxml.jackson.databind.annotation.JsonSerialize import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper From 3ea6dc7a3c3ee339ff978abbec3d872ec52dd6d4 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 31 May 2025 19:21:19 +0900 Subject: [PATCH 06/21] Add a test cases to register serializers to Mapper and serialize value classes --- .../SpecifiedForObjectMapperTest.kt | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/SpecifiedForObjectMapperTest.kt diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/SpecifiedForObjectMapperTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/SpecifiedForObjectMapperTest.kt new file mode 100644 index 00000000..5fddde40 --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/SpecifiedForObjectMapperTest.kt @@ -0,0 +1,160 @@ +package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer + +import com.fasterxml.jackson.databind.module.SimpleModule +import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper +import io.github.projectmapk.jackson.module.kogera.testPrettyWriter +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class SpecifiedForObjectMapperTest { + companion object { + val mapper = jacksonObjectMapper().apply { + val module = SimpleModule().apply { + this.addSerializer(Primitive::class.java, Primitive.Serializer()) + this.addSerializer(NonNullObject::class.java, NonNullObject.Serializer()) + this.addSerializer(NullableObject::class.java, NullableObject.Serializer()) + this.addSerializer(NullablePrimitive::class.java, NullablePrimitive.Serializer()) + this.addSerializer(TwoUnitPrimitive::class.java, TwoUnitPrimitive.Serializer()) + } + this.registerModule(module) + } + val writer = mapper.testPrettyWriter() + } + + @Nested + inner class DirectSerialize { + @Test + fun primitive() { + val result = writer.writeValueAsString(Primitive(1)) + assertEquals("101", result) + } + + @Test + fun nonNullObject() { + val result = writer.writeValueAsString(NonNullObject("foo")) + assertEquals("\"foo-ser\"", result) + } + + @Suppress("ClassName") + @Nested + inner class NullableObject_ { + @Test + fun value() { + val result = writer.writeValueAsString(NullableObject("foo")) + assertEquals("\"foo-ser\"", result) + } + + @Test + fun nullValue() { + val result = writer.writeValueAsString(NullableObject(null)) + assertEquals("\"NULL\"", result) + } + } + + @Suppress("ClassName") + @Nested + inner class NullablePrimitive_ { + @Test + fun value() { + val result = writer.writeValueAsString(NullablePrimitive(1)) + assertEquals("101", result) + } + + @Test + fun nullValue() { + val result = writer.writeValueAsString(NullablePrimitive(null)) + assertEquals("\"NULL\"", result) + } + } + + @Test + fun twoUnitPrimitive() { + val result = writer.writeValueAsString(TwoUnitPrimitive(1)) + assertEquals("101", result) + } + } + + data class Src( + val pNn: Primitive, + val pN: Primitive?, + val nnoNn: NonNullObject, + val nnoN: NonNullObject?, + val noNn: NullableObject, + val noN: NullableObject?, + val npNn: NullablePrimitive, + val npN: NullablePrimitive?, + val tupNn: TwoUnitPrimitive, + val tupN: TwoUnitPrimitive?, + ) + + @Test + fun nonNull() { + val src = Src( + Primitive(1), + Primitive(2), + NonNullObject("foo"), + NonNullObject("bar"), + NullableObject("baz"), + NullableObject("qux"), + NullablePrimitive(3), + NullablePrimitive(4), + TwoUnitPrimitive(5), + TwoUnitPrimitive(6), + ) + val result = writer.writeValueAsString(src) + + assertEquals( + """ + { + "pNn" : 101, + "pN" : 102, + "nnoNn" : "foo-ser", + "nnoN" : "bar-ser", + "noNn" : "baz-ser", + "noN" : "qux-ser", + "npNn" : 103, + "npN" : 104, + "tupNn" : 105, + "tupN" : 106 + } + """.trimIndent(), + result, + ) + } + + @Test + fun withNull() { + val src = Src( + Primitive(1), + null, + NonNullObject("foo"), + null, + NullableObject(null), + null, + NullablePrimitive(null), + null, + TwoUnitPrimitive(5), + null, + ) + val result = writer.writeValueAsString(src) + + assertEquals( + """ + { + "pNn" : 101, + "pN" : null, + "nnoNn" : "foo-ser", + "nnoN" : null, + "noNn" : "NULL", + "noN" : null, + "npNn" : "NULL", + "npN" : null, + "tupNn" : 105, + "tupN" : null + } + """.trimIndent(), + result, + ) + } +} From 2fbbda159694725392e1133b3add9f62dabbab36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 10:39:34 +0000 Subject: [PATCH 07/21] Bump org.junit:junit-bom from 5.13.0 to 5.13.1 in the dependencies group Bumps the dependencies group with 1 update: [org.junit:junit-bom](https://github.com/junit-team/junit5). Updates `org.junit:junit-bom` from 5.13.0 to 5.13.1 - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.13.0...r5.13.1) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-version: 5.13.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 02f3d292..614c698c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.0.21" # Mainly for CI, it can be rewritten by environment variable. jackson = "2.19.0" # test libs -junit = "5.13.0" +junit = "5.13.1" [libraries] kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib" } From 93c718fc18cb2adcf4b50fd352cd52a5ad98c06e Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 24 May 2025 17:27:41 +0900 Subject: [PATCH 08/21] Modified to use MethodHandle for deserialization of value class --- .../jackson/module/kogera/Converters.kt | 13 +++++++++---- .../jackson/module/kogera/InternalCommons.kt | 3 +++ .../deser/deserializers/KotlinDeserializers.kt | 14 +++++++++----- .../WithoutCustomDeserializeMethodTest.kt | 5 ++--- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt index 5d9baf23..d9261387 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt @@ -5,6 +5,9 @@ import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer import com.fasterxml.jackson.databind.type.TypeFactory import com.fasterxml.jackson.databind.util.ClassUtil import com.fasterxml.jackson.databind.util.StdConverter +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles +import java.lang.invoke.MethodType /** * A converter that only performs box processing for the value class. @@ -16,12 +19,14 @@ internal class ValueClassBoxConverter( unboxedClass: Class, val boxedClass: Class, ) : StdConverter() { - private val boxMethod = boxedClass.getDeclaredMethod("box-impl", unboxedClass).apply { - ClassUtil.checkAndFixAccess(this, false) - } + val boxHandle: MethodHandle = MethodHandles.lookup().findStatic( + boxedClass, + "box-impl", + MethodType.methodType(boxedClass, unboxedClass), + ).asType(ANY_TO_ANY_METHOD_TYPE) @Suppress("UNCHECKED_CAST") - override fun convert(value: S): D = boxMethod.invoke(null, value) as D + override fun convert(value: S): D = boxHandle.invokeExact(value) as D val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) } } 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 d6f51e64..f2d31fb5 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 @@ -3,6 +3,7 @@ package io.github.projectmapk.jackson.module.kogera import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty import io.github.projectmapk.jackson.module.kogera.annotation.JsonKUnbox +import java.lang.invoke.MethodType import java.lang.reflect.AnnotatedElement import java.lang.reflect.Constructor import java.lang.reflect.Method @@ -106,3 +107,5 @@ internal val JSON_K_UNBOX_CLASS = JsonKUnbox::class.java internal val KOTLIN_DURATION_CLASS = KotlinDuration::class.java internal val CLOSED_FLOATING_POINT_RANGE_CLASS = ClosedFloatingPointRange::class.java internal val ANY_CLASS = Any::class.java + +internal val ANY_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, ANY_CLASS) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt index f0531313..5f173306 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt @@ -9,7 +9,7 @@ import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.exc.InvalidDefinitionException import com.fasterxml.jackson.databind.module.SimpleDeserializers -import com.fasterxml.jackson.databind.util.ClassUtil +import io.github.projectmapk.jackson.module.kogera.ANY_TO_ANY_METHOD_TYPE import io.github.projectmapk.jackson.module.kogera.KotlinDuration import io.github.projectmapk.jackson.module.kogera.ReflectionCache import io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter @@ -19,6 +19,8 @@ import io.github.projectmapk.jackson.module.kogera.hasCreatorAnnotation import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass import io.github.projectmapk.jackson.module.kogera.toSignature +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles import java.lang.reflect.Method import java.lang.reflect.Modifier @@ -89,13 +91,15 @@ internal object ULongDeserializer : StdDeserializer(ULong::class.java) { } internal class WrapsNullableValueClassBoxDeserializer( - private val creator: Method, - private val converter: ValueClassBoxConverter, + creator: Method, + converter: ValueClassBoxConverter, ) : WrapsNullableValueClassDeserializer(converter.boxedClass) { private val inputType: Class<*> = creator.parameterTypes[0] + private val handle: MethodHandle init { - ClassUtil.checkAndFixAccess(creator, false) + val unreflect = MethodHandles.lookup().unreflect(creator).asType(ANY_TO_ANY_METHOD_TYPE) + handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) } // Cache the result of wrapping null, since the result is always expected to be the same. @@ -108,7 +112,7 @@ internal class WrapsNullableValueClassBoxDeserializer( // it is necessary to call creator(e.g. constructor-impl) -> box-impl in that order. // Input is null only when called from KotlinValueInstantiator. @Suppress("UNCHECKED_CAST") - private fun instantiate(input: Any?): D = converter.convert(creator.invoke(null, input) as S) + private fun instantiate(input: Any?): D = handle.invokeExact(input) as D override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D { val input = p.readValueAs(inputType) 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 87aa5ec9..a84e16e5 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 @@ -8,7 +8,6 @@ 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 -import java.lang.reflect.InvocationTargetException class WithoutCustomDeserializeMethodTest { companion object { @@ -132,8 +131,8 @@ class WithoutCustomDeserializeMethodTest { @Test fun callConstructorCheckTest() { - val e = assertThrows { defaultMapper.readValue("-1") } - Assertions.assertTrue(e.cause === throwable) + val e = assertThrows { defaultMapper.readValue("-1") } + Assertions.assertTrue(e === throwable) } // If all JsonCreator tests are OK, no need to check throws from factory functions. From 607ba85c6074ee5a6a7a5675479d20987477ae9e Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 24 May 2025 17:38:14 +0900 Subject: [PATCH 09/21] Add specialized converter --- .../jackson/module/kogera/Converters.kt | 38 ++++++++++++++----- .../jackson/module/kogera/ReflectionCache.kt | 8 +++- .../deserializers/KotlinDeserializers.kt | 2 +- .../jackson/module/kogera/ser/Converters.kt | 4 +- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt index d9261387..c4f83dc5 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt @@ -9,26 +9,44 @@ import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType +internal sealed class ValueClassBoxConverter : StdConverter() { + abstract val boxedClass: Class + abstract val boxHandle: MethodHandle + + protected fun rawBoxHandle( + unboxedClass: Class<*>, + ): MethodHandle = MethodHandles.lookup().findStatic( + boxedClass, + "box-impl", + MethodType.methodType(boxedClass, unboxedClass), + ) + + val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) } +} + +internal class IntValueClassBoxConverter( + override val boxedClass: Class, +) : ValueClassBoxConverter() { + override val boxHandle: MethodHandle = rawBoxHandle(Int::class.java).asType(MethodType.methodType(ANY_CLASS, Int::class.java)) + + @Suppress("UNCHECKED_CAST") + override fun convert(value: Int): D = boxHandle.invokeExact(value) as D +} + /** * A converter that only performs box processing for the value class. * Note that constructor-impl is not called. * @param S is nullable because value corresponds to a nullable value class. * see [io.github.projectmapk.jackson.module.kogera.annotationIntrospector.KotlinFallbackAnnotationIntrospector.findNullSerializer] */ -internal class ValueClassBoxConverter( +internal class GenericValueClassBoxConverter( unboxedClass: Class, - val boxedClass: Class, -) : StdConverter() { - val boxHandle: MethodHandle = MethodHandles.lookup().findStatic( - boxedClass, - "box-impl", - MethodType.methodType(boxedClass, unboxedClass), - ).asType(ANY_TO_ANY_METHOD_TYPE) + override val boxedClass: Class, +) : ValueClassBoxConverter() { + override val boxHandle: MethodHandle = rawBoxHandle(unboxedClass).asType(ANY_TO_ANY_METHOD_TYPE) @Suppress("UNCHECKED_CAST") override fun convert(value: S): D = boxHandle.invokeExact(value) as D - - val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) } } internal class ValueClassUnboxConverter(val valueClass: Class) : StdConverter() { diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt index 01b8a460..d8a1ae34 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt @@ -93,7 +93,13 @@ internal class ReflectionCache(initialCacheSize: Int, maxCacheSize: Int) : Seria fun getValueClassBoxConverter(unboxedClass: Class<*>, valueClass: Class<*>): ValueClassBoxConverter<*, *> { val key = OtherCacheKey.ValueClassBoxConverter(valueClass) - return find(key) ?: putIfAbsent(key, ValueClassBoxConverter(unboxedClass, valueClass)) + return find(key) ?: putIfAbsent( + key, + when (unboxedClass) { + Int::class.java -> IntValueClassBoxConverter(valueClass) + else -> GenericValueClassBoxConverter(unboxedClass, valueClass) + }, + ) } fun getValueClassUnboxConverter(valueClass: Class<*>): ValueClassUnboxConverter<*> { diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt index 5f173306..6803dea6 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt @@ -99,7 +99,7 @@ internal class WrapsNullableValueClassBoxDeserializer( init { val unreflect = MethodHandles.lookup().unreflect(creator).asType(ANY_TO_ANY_METHOD_TYPE) - handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) + handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle.asType(ANY_TO_ANY_METHOD_TYPE)) } // Cache the result of wrapping null, since the result is always expected to be the same. diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/Converters.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/Converters.kt index 09dbba08..caacdad3 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/Converters.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/Converters.kt @@ -3,10 +3,10 @@ package io.github.projectmapk.jackson.module.kogera.ser import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.type.TypeFactory import com.fasterxml.jackson.databind.util.StdConverter +import io.github.projectmapk.jackson.module.kogera.GenericValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.JavaDuration import io.github.projectmapk.jackson.module.kogera.KOTLIN_DURATION_CLASS import io.github.projectmapk.jackson.module.kogera.KotlinDuration -import io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter import kotlin.time.toJavaDuration internal class SequenceToIteratorConverter(private val input: JavaType) : StdConverter, Iterator<*>>() { @@ -21,7 +21,7 @@ internal class SequenceToIteratorConverter(private val input: JavaType) : StdCon } internal object KotlinDurationValueToJavaDurationConverter : StdConverter() { - private val boxConverter by lazy { ValueClassBoxConverter(Long::class.java, KOTLIN_DURATION_CLASS) } + private val boxConverter by lazy { GenericValueClassBoxConverter(Long::class.java, KOTLIN_DURATION_CLASS) } override fun convert(value: Long): JavaDuration = KotlinToJavaDurationConverter.convert(boxConverter.convert(value)) } From 7c07968308cc7e9ed7fdaaf2dd785a143dcee059 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 24 May 2025 17:38:45 +0900 Subject: [PATCH 10/21] Add specialized deserializer --- .../deserializers/KotlinDeserializers.kt | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt index 6803dea6..a5c2b7d3 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt @@ -9,10 +9,12 @@ import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.exc.InvalidDefinitionException import com.fasterxml.jackson.databind.module.SimpleDeserializers +import io.github.projectmapk.jackson.module.kogera.ANY_CLASS import io.github.projectmapk.jackson.module.kogera.ANY_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.GenericValueClassBoxConverter +import io.github.projectmapk.jackson.module.kogera.IntValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.KotlinDuration import io.github.projectmapk.jackson.module.kogera.ReflectionCache -import io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.deser.JavaToKotlinDurationConverter import io.github.projectmapk.jackson.module.kogera.deser.WrapsNullableValueClassDeserializer import io.github.projectmapk.jackson.module.kogera.hasCreatorAnnotation @@ -21,6 +23,7 @@ import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass import io.github.projectmapk.jackson.module.kogera.toSignature import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles +import java.lang.invoke.MethodType import java.lang.reflect.Method import java.lang.reflect.Modifier @@ -90,16 +93,36 @@ internal object ULongDeserializer : StdDeserializer(ULong::class.java) { .readWithRangeCheck(p, p.bigIntegerValue) } +internal class WrapsIntValueClassBoxDeserializer( + creator: Method, + converter: IntValueClassBoxConverter, +) : StdDeserializer(converter.boxedClass) { + private val inputType: Class<*> = creator.parameterTypes[0] + private val handle: MethodHandle + + init { + val unreflect = MethodHandles.lookup().unreflect(creator) + .asType(MethodType.methodType(Int::class.java, ANY_CLASS)) + handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) + } + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D { + val input = p.readValueAs(inputType) + @Suppress("UNCHECKED_CAST") + return handle.invokeExact(input) as D + } +} + internal class WrapsNullableValueClassBoxDeserializer( creator: Method, - converter: ValueClassBoxConverter, + converter: GenericValueClassBoxConverter, ) : WrapsNullableValueClassDeserializer(converter.boxedClass) { private val inputType: Class<*> = creator.parameterTypes[0] private val handle: MethodHandle init { val unreflect = MethodHandles.lookup().unreflect(creator).asType(ANY_TO_ANY_METHOD_TYPE) - handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle.asType(ANY_TO_ANY_METHOD_TYPE)) + handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) } // Cache the result of wrapping null, since the result is always expected to be the same. @@ -173,7 +196,11 @@ internal class KotlinDeserializers( rawClass.isUnboxableValueClass() -> findValueCreator(type, rawClass, cache.getJmClass(rawClass)!!)?.let { val unboxedClass = it.returnType val converter = cache.getValueClassBoxConverter(unboxedClass, rawClass) - WrapsNullableValueClassBoxDeserializer(it, converter) + + when (converter) { + is IntValueClassBoxConverter -> WrapsIntValueClassBoxDeserializer(it, converter) + is GenericValueClassBoxConverter -> WrapsNullableValueClassBoxDeserializer(it, converter) + } } else -> null } From 37ab0c3826048ffbe14fd1ea3dca5262f9498514 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 24 May 2025 18:02:49 +0900 Subject: [PATCH 11/21] Add converters for common classes as wrapped values --- .../jackson/module/kogera/Converters.kt | 30 +++++++ .../jackson/module/kogera/ReflectionCache.kt | 4 + .../deserializers/KotlinDeserializers.kt | 89 +++++++++++++++++++ 3 files changed, 123 insertions(+) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt index c4f83dc5..00c88fcf 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.util.StdConverter import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType +import java.util.UUID internal sealed class ValueClassBoxConverter : StdConverter() { abstract val boxedClass: Class @@ -24,6 +25,7 @@ internal sealed class ValueClassBoxConverter : StdConverter( override val boxedClass: Class, ) : ValueClassBoxConverter() { @@ -33,6 +35,34 @@ internal class IntValueClassBoxConverter( override fun convert(value: Int): D = boxHandle.invokeExact(value) as D } +internal class LongValueClassBoxConverter( + override val boxedClass: Class, +) : ValueClassBoxConverter() { + override val boxHandle: MethodHandle = rawBoxHandle(Long::class.java).asType(MethodType.methodType(ANY_CLASS, Long::class.java)) + + @Suppress("UNCHECKED_CAST") + override fun convert(value: Long): D = boxHandle.invokeExact(value) as D +} + +internal class StringValueClassBoxConverter( + override val boxedClass: Class, +) : ValueClassBoxConverter() { + override val boxHandle: MethodHandle = rawBoxHandle(String::class.java).asType(MethodType.methodType(ANY_CLASS, String::class.java)) + + @Suppress("UNCHECKED_CAST") + override fun convert(value: String?): D = boxHandle.invokeExact(value) as D +} + +internal class JavaUuidValueClassBoxConverter( + override val boxedClass: Class, +) : ValueClassBoxConverter() { + override val boxHandle: MethodHandle = rawBoxHandle(UUID::class.java).asType(MethodType.methodType(ANY_CLASS, UUID::class.java)) + + @Suppress("UNCHECKED_CAST") + override fun convert(value: UUID?): D = boxHandle.invokeExact(value) as D +} +// endregion + /** * A converter that only performs box processing for the value class. * Note that constructor-impl is not called. diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt index d8a1ae34..9b7b7a2a 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt @@ -5,6 +5,7 @@ import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass import java.io.Serializable import java.lang.reflect.Method import java.util.Optional +import java.util.UUID // For ease of testing, maxCacheSize is limited only in KotlinModule. internal class ReflectionCache(initialCacheSize: Int, maxCacheSize: Int) : Serializable { @@ -97,6 +98,9 @@ internal class ReflectionCache(initialCacheSize: Int, maxCacheSize: Int) : Seria key, when (unboxedClass) { Int::class.java -> IntValueClassBoxConverter(valueClass) + Long::class.java -> LongValueClassBoxConverter(valueClass) + String::class.java -> StringValueClassBoxConverter(valueClass) + UUID::class.java -> JavaUuidValueClassBoxConverter(valueClass) else -> GenericValueClassBoxConverter(unboxedClass, valueClass) }, ) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt index a5c2b7d3..01e31a1c 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt @@ -13,8 +13,11 @@ import io.github.projectmapk.jackson.module.kogera.ANY_CLASS import io.github.projectmapk.jackson.module.kogera.ANY_TO_ANY_METHOD_TYPE import io.github.projectmapk.jackson.module.kogera.GenericValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.IntValueClassBoxConverter +import io.github.projectmapk.jackson.module.kogera.JavaUuidValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.KotlinDuration +import io.github.projectmapk.jackson.module.kogera.LongValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.ReflectionCache +import io.github.projectmapk.jackson.module.kogera.StringValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.deser.JavaToKotlinDurationConverter import io.github.projectmapk.jackson.module.kogera.deser.WrapsNullableValueClassDeserializer import io.github.projectmapk.jackson.module.kogera.hasCreatorAnnotation @@ -26,6 +29,7 @@ import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType import java.lang.reflect.Method import java.lang.reflect.Modifier +import java.util.UUID internal object SequenceDeserializer : StdDeserializer>(Sequence::class.java) { private fun readResolve(): Any = SequenceDeserializer @@ -113,6 +117,88 @@ internal class WrapsIntValueClassBoxDeserializer( } } +internal class WrapsLongValueClassBoxDeserializer( + creator: Method, + converter: LongValueClassBoxConverter, +) : StdDeserializer(converter.boxedClass) { + private val inputType: Class<*> = creator.parameterTypes[0] + private val handle: MethodHandle + + init { + val unreflect = MethodHandles.lookup().unreflect(creator) + .asType(MethodType.methodType(Long::class.java, ANY_CLASS)) + handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) + } + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D { + val input = p.readValueAs(inputType) + @Suppress("UNCHECKED_CAST") + return handle.invokeExact(input) as D + } +} + +internal class WrapsStringValueClassBoxDeserializer( + creator: Method, + converter: StringValueClassBoxConverter, +) : WrapsNullableValueClassDeserializer(converter.boxedClass) { + private val inputType: Class<*> = creator.parameterTypes[0] + private val handle: MethodHandle + + init { + val unreflect = MethodHandles.lookup().unreflect(creator) + .asType(MethodType.methodType(String::class.java, ANY_CLASS)) + handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) + } + + // Cache the result of wrapping null, since the result is always expected to be the same. + @get:JvmName("boxedNullValue") + private val boxedNullValue: D by lazy { instantiate(null) } + + override fun getBoxedNullValue(): D = boxedNullValue + + // To instantiate the value class in the same way as other classes, + // it is necessary to call creator(e.g. constructor-impl) -> box-impl in that order. + // Input is null only when called from KotlinValueInstantiator. + @Suppress("UNCHECKED_CAST") + private fun instantiate(input: Any?): D = handle.invokeExact(input) as D + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D { + val input = p.readValueAs(inputType) + return instantiate(input) + } +} + +internal class WrapsJavaUuidValueClassBoxDeserializer( + creator: Method, + converter: JavaUuidValueClassBoxConverter, +) : WrapsNullableValueClassDeserializer(converter.boxedClass) { + private val inputType: Class<*> = creator.parameterTypes[0] + private val handle: MethodHandle + + init { + val unreflect = MethodHandles.lookup().unreflect(creator) + .asType(MethodType.methodType(UUID::class.java, ANY_CLASS)) + handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) + } + + // Cache the result of wrapping null, since the result is always expected to be the same. + @get:JvmName("boxedNullValue") + private val boxedNullValue: D by lazy { instantiate(null) } + + override fun getBoxedNullValue(): D = boxedNullValue + + // To instantiate the value class in the same way as other classes, + // it is necessary to call creator(e.g. constructor-impl) -> box-impl in that order. + // Input is null only when called from KotlinValueInstantiator. + @Suppress("UNCHECKED_CAST") + private fun instantiate(input: Any?): D = handle.invokeExact(input) as D + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D { + val input = p.readValueAs(inputType) + return instantiate(input) + } +} + internal class WrapsNullableValueClassBoxDeserializer( creator: Method, converter: GenericValueClassBoxConverter, @@ -199,6 +285,9 @@ internal class KotlinDeserializers( when (converter) { is IntValueClassBoxConverter -> WrapsIntValueClassBoxDeserializer(it, converter) + is LongValueClassBoxConverter -> WrapsLongValueClassBoxDeserializer(it, converter) + is StringValueClassBoxConverter -> WrapsStringValueClassBoxDeserializer(it, converter) + is JavaUuidValueClassBoxConverter -> WrapsJavaUuidValueClassBoxDeserializer(it, converter) is GenericValueClassBoxConverter -> WrapsNullableValueClassBoxDeserializer(it, converter) } } From 6907b396136bed6ca4dced9db795ccb051784f88 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 24 May 2025 21:54:46 +0900 Subject: [PATCH 12/21] Corrected the position of factory function definition --- .../projectmapk/jackson/module/kogera/Converters.kt | 13 +++++++++++++ .../jackson/module/kogera/ReflectionCache.kt | 12 +----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt index 00c88fcf..a4c6f00c 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt @@ -23,6 +23,19 @@ internal sealed class ValueClassBoxConverter : StdConverter, + valueClass: Class<*>, + ): ValueClassBoxConverter<*, *> = when (unboxedClass) { + Int::class.java -> IntValueClassBoxConverter(valueClass) + Long::class.java -> LongValueClassBoxConverter(valueClass) + String::class.java -> StringValueClassBoxConverter(valueClass) + UUID::class.java -> JavaUuidValueClassBoxConverter(valueClass) + else -> GenericValueClassBoxConverter(unboxedClass, valueClass) + } + } } // region: Converters for common classes as wrapped values, add as needed. diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt index 9b7b7a2a..b0ee7d66 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt @@ -5,7 +5,6 @@ import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass import java.io.Serializable import java.lang.reflect.Method import java.util.Optional -import java.util.UUID // For ease of testing, maxCacheSize is limited only in KotlinModule. internal class ReflectionCache(initialCacheSize: Int, maxCacheSize: Int) : Serializable { @@ -94,16 +93,7 @@ internal class ReflectionCache(initialCacheSize: Int, maxCacheSize: Int) : Seria fun getValueClassBoxConverter(unboxedClass: Class<*>, valueClass: Class<*>): ValueClassBoxConverter<*, *> { val key = OtherCacheKey.ValueClassBoxConverter(valueClass) - return find(key) ?: putIfAbsent( - key, - when (unboxedClass) { - Int::class.java -> IntValueClassBoxConverter(valueClass) - Long::class.java -> LongValueClassBoxConverter(valueClass) - String::class.java -> StringValueClassBoxConverter(valueClass) - UUID::class.java -> JavaUuidValueClassBoxConverter(valueClass) - else -> GenericValueClassBoxConverter(unboxedClass, valueClass) - }, - ) + return find(key) ?: putIfAbsent(key, ValueClassBoxConverter.create(unboxedClass, valueClass)) } fun getValueClassUnboxConverter(valueClass: Class<*>): ValueClassUnboxConverter<*> { From 94245bee823e1e440dbb9ffbc6f170723b6275e0 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 24 May 2025 21:40:02 +0900 Subject: [PATCH 13/21] Fixed ValueClassUnboxConverter to use MethodHandle --- .../jackson/module/kogera/Converters.kt | 41 ++++++++++++++----- .../jackson/module/kogera/ReflectionCache.kt | 6 +-- .../argumentBucket/ArgumentBucket.kt | 4 +- .../valueInstantiator/creator/ValueCreator.kt | 4 +- .../ser/serializers/KotlinKeySerializers.kt | 6 +-- .../ser/serializers/KotlinSerializers.kt | 4 +- .../argumentBucket/ArgumentBucketTest.kt | 2 +- 7 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt index a4c6f00c..943d29a4 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt @@ -3,11 +3,12 @@ package io.github.projectmapk.jackson.module.kogera import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer import com.fasterxml.jackson.databind.type.TypeFactory -import com.fasterxml.jackson.databind.util.ClassUtil import com.fasterxml.jackson.databind.util.StdConverter import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType +import java.lang.reflect.Method +import java.lang.reflect.Type import java.util.UUID internal sealed class ValueClassBoxConverter : StdConverter() { @@ -92,17 +93,35 @@ internal class GenericValueClassBoxConverter( override fun convert(value: S): D = boxHandle.invokeExact(value) as D } -internal class ValueClassUnboxConverter(val valueClass: Class) : StdConverter() { - private val unboxMethod = valueClass.getDeclaredMethod("unbox-impl").apply { - ClassUtil.checkAndFixAccess(this, false) - } - - override fun convert(value: T): Any? = unboxMethod.invoke(value) +internal sealed class ValueClassUnboxConverter : StdConverter() { + abstract val valueClass: Class + abstract val unboxedType: Type + abstract val unboxHandle: MethodHandle - override fun getInputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(valueClass) - override fun getOutputType( - typeFactory: TypeFactory, - ): JavaType = typeFactory.constructType(unboxMethod.genericReturnType) + final override fun getInputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(valueClass) + final override fun getOutputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(unboxedType) val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) } + + companion object { + fun create(valueClass: Class<*>): ValueClassUnboxConverter<*, *> { + val unboxMethod = valueClass.getDeclaredMethod("unbox-impl") + val unboxedType = unboxMethod.genericReturnType + + return when (unboxedType) { + else -> GenericValueClassUnboxConverter(valueClass, unboxedType, unboxMethod) + } + } + } +} + +internal class GenericValueClassUnboxConverter( + override val valueClass: Class, + override val unboxedType: Type, + unboxMethod: Method, +) : ValueClassUnboxConverter() { + override val unboxHandle: MethodHandle = + MethodHandles.lookup().unreflect(unboxMethod).asType(MethodType.methodType(ANY_CLASS, ANY_CLASS)) + + override fun convert(value: T): Any? = unboxHandle.invokeExact(value) } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt index b0ee7d66..a600b080 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt @@ -34,7 +34,7 @@ internal class ReflectionCache(initialCacheSize: Int, maxCacheSize: Int) : Seria ) : OtherCacheKey, io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter<*, *>>() class ValueClassUnboxConverter( override val key: Class<*>, - ) : OtherCacheKey, io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter<*>>() + ) : OtherCacheKey, io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter<*, *>>() } private val cache = LRUMap(initialCacheSize, maxCacheSize) @@ -96,8 +96,8 @@ internal class ReflectionCache(initialCacheSize: Int, maxCacheSize: Int) : Seria return find(key) ?: putIfAbsent(key, ValueClassBoxConverter.create(unboxedClass, valueClass)) } - fun getValueClassUnboxConverter(valueClass: Class<*>): ValueClassUnboxConverter<*> { + fun getValueClassUnboxConverter(valueClass: Class<*>): ValueClassUnboxConverter<*, *> { val key = OtherCacheKey.ValueClassUnboxConverter(valueClass) - return find(key) ?: putIfAbsent(key, ValueClassUnboxConverter(valueClass)) + return find(key) ?: putIfAbsent(key, ValueClassUnboxConverter.create(valueClass)) } } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucket.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucket.kt index cb0dac48..1a275297 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucket.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucket.kt @@ -48,7 +48,7 @@ private fun IntArray.update(index: Int, operation: MaskOperation) { internal class BucketGenerator( parameterTypes: List>, valueParameters: List, - private val converters: List?>, + private val converters: List?>, ) { private val valueParameterSize: Int = parameterTypes.size private val originalAbsentArgs: Array @@ -99,7 +99,7 @@ internal class ArgumentBucket( val valueParameterSize: Int, val arguments: Array, val masks: IntArray, - private val converters: List?>, + private val converters: List?>, ) { /** * Sets the argument corresponding to index. diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueCreator.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueCreator.kt index ebf3d22b..dd45269b 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueCreator.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueCreator.kt @@ -63,8 +63,8 @@ internal sealed class ValueCreator { internal fun List.mapToConverters( rawTypes: List>, cache: ReflectionCache, -): List?> = mapIndexed { i, param -> +): List?> = mapIndexed { i, param -> param.reconstructedClassOrNull ?.takeIf { it.isUnboxableValueClass() && rawTypes[i] != it } ?.let { cache.getValueClassUnboxConverter(it) } -} as List?> // Cast to cheat generics +} as List?> // Cast to cheat generics diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinKeySerializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinKeySerializers.kt index 6ed9c124..9a7ac58c 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinKeySerializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinKeySerializers.kt @@ -16,7 +16,7 @@ import java.lang.reflect.Method import java.lang.reflect.Modifier internal class ValueClassUnboxKeySerializer( - private val converter: ValueClassUnboxConverter, + private val converter: ValueClassUnboxConverter, ) : StdSerializer(converter.valueClass) { override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { val unboxed = converter.convert(value) @@ -37,7 +37,7 @@ private fun Class<*>.getStaticJsonKeyGetter(): Method? = this.declaredMethods.fi } internal class ValueClassStaticJsonKeySerializer( - private val converter: ValueClassUnboxConverter, + private val converter: ValueClassUnboxConverter, private val staticJsonKeyGetter: Method, ) : StdSerializer(converter.valueClass) { private val keyType: Class<*> = staticJsonKeyGetter.returnType @@ -59,7 +59,7 @@ internal class ValueClassStaticJsonKeySerializer( // If create a function with a JsonValue in the value class, // it will be compiled as a static method (= cannot be processed properly by Jackson), // so use a ValueClassSerializer.StaticJsonValue to handle this. - fun createOrNull(converter: ValueClassUnboxConverter): StdSerializer? = converter.valueClass + fun createOrNull(converter: ValueClassUnboxConverter): StdSerializer? = converter.valueClass .getStaticJsonKeyGetter() ?.let { ValueClassStaticJsonKeySerializer(converter, it) } } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt index 90b303fe..332a1c91 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt @@ -55,7 +55,7 @@ private fun Class<*>.getStaticJsonValueGetter(): Method? = this.declaredMethods. } internal class ValueClassStaticJsonValueSerializer( - private val converter: ValueClassUnboxConverter, + private val converter: ValueClassUnboxConverter, private val staticJsonValueGetter: Method, ) : StdSerializer(converter.valueClass) { override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { @@ -70,7 +70,7 @@ internal class ValueClassStaticJsonValueSerializer( // If create a function with a JsonValue in the value class, // it will be compiled as a static method (= cannot be processed properly by Jackson), // so use a ValueClassSerializer.StaticJsonValue to handle this. - fun createOrNull(converter: ValueClassUnboxConverter): StdSerializer? = converter + fun createOrNull(converter: ValueClassUnboxConverter): StdSerializer? = converter .valueClass .getStaticJsonValueGetter() ?.let { ValueClassStaticJsonValueSerializer(converter, it) } diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucketTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucketTest.kt index f27e3246..6c220dc4 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucketTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucketTest.kt @@ -110,7 +110,7 @@ private class ArgumentBucketTest { @Test fun unboxTest() { @Suppress("UNCHECKED_CAST") - val converter = ValueClassUnboxConverter(V::class.java) as ValueClassUnboxConverter + val converter = ValueClassUnboxConverter.create(V::class.java) as ValueClassUnboxConverter val generator = BucketGenerator( listOf(Int::class.java, V::class.java), (1..2).map { mockValueParameter() }, From c482b775a523ef47519c21e144b02c5fc91a8604 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 24 May 2025 22:24:34 +0900 Subject: [PATCH 14/21] Add converters for common classes as wrapped values --- .../jackson/module/kogera/Converters.kt | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt index 943d29a4..a67e8f3d 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt @@ -109,12 +109,60 @@ internal sealed class ValueClassUnboxConverter : StdConverter val unboxedType = unboxMethod.genericReturnType return when (unboxedType) { + Int::class.java -> IntValueClassUnboxConverter(valueClass, unboxMethod) + Long::class.java -> LongValueClassUnboxConverter(valueClass, unboxMethod) + String::class.java -> StringValueClassUnboxConverter(valueClass, unboxMethod) + UUID::class.java -> JavaUuidValueClassUnboxConverter(valueClass, unboxMethod) else -> GenericValueClassUnboxConverter(valueClass, unboxedType, unboxMethod) } } } } +internal class IntValueClassUnboxConverter( + override val valueClass: Class, + unboxMethod: Method, +) : ValueClassUnboxConverter() { + override val unboxedType: Type get() = Int::class.java + override val unboxHandle: MethodHandle = + MethodHandles.lookup().unreflect(unboxMethod).asType(MethodType.methodType(Int::class.java, ANY_CLASS)) + + override fun convert(value: T): Int = unboxHandle.invokeExact(value) as Int +} + +internal class LongValueClassUnboxConverter( + override val valueClass: Class, + unboxMethod: Method, +) : ValueClassUnboxConverter() { + override val unboxedType: Type get() = Long::class.java + override val unboxHandle: MethodHandle = + MethodHandles.lookup().unreflect(unboxMethod).asType(MethodType.methodType(Long::class.java, ANY_CLASS)) + + override fun convert(value: T): Long = unboxHandle.invokeExact(value) as Long +} + +internal class StringValueClassUnboxConverter( + override val valueClass: Class, + unboxMethod: Method, +) : ValueClassUnboxConverter() { + override val unboxedType: Type get() = String::class.java + override val unboxHandle: MethodHandle = + MethodHandles.lookup().unreflect(unboxMethod).asType(MethodType.methodType(String::class.java, ANY_CLASS)) + + override fun convert(value: T): String? = unboxHandle.invokeExact(value) as String? +} + +internal class JavaUuidValueClassUnboxConverter( + override val valueClass: Class, + unboxMethod: Method, +) : ValueClassUnboxConverter() { + override val unboxedType: Type get() = UUID::class.java + override val unboxHandle: MethodHandle = + MethodHandles.lookup().unreflect(unboxMethod).asType(MethodType.methodType(UUID::class.java, ANY_CLASS)) + + override fun convert(value: T): UUID? = unboxHandle.invokeExact(value) as UUID? +} + internal class GenericValueClassUnboxConverter( override val valueClass: Class, override val unboxedType: Type, From e80e52556fc5400f012a0adba12b9d36aa1189c9 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 24 May 2025 22:36:14 +0900 Subject: [PATCH 15/21] Cache the acquisition process to constants as much as possible --- .../jackson/module/kogera/Converters.kt | 42 ++++---- .../jackson/module/kogera/InternalCommons.kt | 98 +++++++++++-------- .../deserializers/KotlinDeserializers.kt | 19 ++-- .../creator/MethodValueCreator.kt | 3 +- 4 files changed, 86 insertions(+), 76 deletions(-) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt index a67e8f3d..5d8253de 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt @@ -30,10 +30,10 @@ internal sealed class ValueClassBoxConverter : StdConverter, valueClass: Class<*>, ): ValueClassBoxConverter<*, *> = when (unboxedClass) { - Int::class.java -> IntValueClassBoxConverter(valueClass) - Long::class.java -> LongValueClassBoxConverter(valueClass) - String::class.java -> StringValueClassBoxConverter(valueClass) - UUID::class.java -> JavaUuidValueClassBoxConverter(valueClass) + INT_CLASS -> IntValueClassBoxConverter(valueClass) + LONG_CLASS -> LongValueClassBoxConverter(valueClass) + STRING_CLASS -> StringValueClassBoxConverter(valueClass) + JAVA_UUID_CLASS -> JavaUuidValueClassBoxConverter(valueClass) else -> GenericValueClassBoxConverter(unboxedClass, valueClass) } } @@ -43,7 +43,7 @@ internal sealed class ValueClassBoxConverter : StdConverter( override val boxedClass: Class, ) : ValueClassBoxConverter() { - override val boxHandle: MethodHandle = rawBoxHandle(Int::class.java).asType(MethodType.methodType(ANY_CLASS, Int::class.java)) + override val boxHandle: MethodHandle = rawBoxHandle(INT_CLASS).asType(INT_TO_ANY_METHOD_TYPE) @Suppress("UNCHECKED_CAST") override fun convert(value: Int): D = boxHandle.invokeExact(value) as D @@ -52,7 +52,7 @@ internal class IntValueClassBoxConverter( internal class LongValueClassBoxConverter( override val boxedClass: Class, ) : ValueClassBoxConverter() { - override val boxHandle: MethodHandle = rawBoxHandle(Long::class.java).asType(MethodType.methodType(ANY_CLASS, Long::class.java)) + override val boxHandle: MethodHandle = rawBoxHandle(LONG_CLASS).asType(LONG_TO_ANY_METHOD_TYPE) @Suppress("UNCHECKED_CAST") override fun convert(value: Long): D = boxHandle.invokeExact(value) as D @@ -61,7 +61,7 @@ internal class LongValueClassBoxConverter( internal class StringValueClassBoxConverter( override val boxedClass: Class, ) : ValueClassBoxConverter() { - override val boxHandle: MethodHandle = rawBoxHandle(String::class.java).asType(MethodType.methodType(ANY_CLASS, String::class.java)) + override val boxHandle: MethodHandle = rawBoxHandle(STRING_CLASS).asType(STRING_TO_ANY_METHOD_TYPE) @Suppress("UNCHECKED_CAST") override fun convert(value: String?): D = boxHandle.invokeExact(value) as D @@ -70,7 +70,7 @@ internal class StringValueClassBoxConverter( internal class JavaUuidValueClassBoxConverter( override val boxedClass: Class, ) : ValueClassBoxConverter() { - override val boxHandle: MethodHandle = rawBoxHandle(UUID::class.java).asType(MethodType.methodType(ANY_CLASS, UUID::class.java)) + override val boxHandle: MethodHandle = rawBoxHandle(JAVA_UUID_CLASS).asType(JAVA_UUID_TO_ANY_METHOD_TYPE) @Suppress("UNCHECKED_CAST") override fun convert(value: UUID?): D = boxHandle.invokeExact(value) as D @@ -109,10 +109,10 @@ internal sealed class ValueClassUnboxConverter : StdConverter val unboxedType = unboxMethod.genericReturnType return when (unboxedType) { - Int::class.java -> IntValueClassUnboxConverter(valueClass, unboxMethod) - Long::class.java -> LongValueClassUnboxConverter(valueClass, unboxMethod) - String::class.java -> StringValueClassUnboxConverter(valueClass, unboxMethod) - UUID::class.java -> JavaUuidValueClassUnboxConverter(valueClass, unboxMethod) + INT_CLASS -> IntValueClassUnboxConverter(valueClass, unboxMethod) + LONG_CLASS -> LongValueClassUnboxConverter(valueClass, unboxMethod) + STRING_CLASS -> StringValueClassUnboxConverter(valueClass, unboxMethod) + JAVA_UUID_CLASS -> JavaUuidValueClassUnboxConverter(valueClass, unboxMethod) else -> GenericValueClassUnboxConverter(valueClass, unboxedType, unboxMethod) } } @@ -123,9 +123,9 @@ internal class IntValueClassUnboxConverter( override val valueClass: Class, unboxMethod: Method, ) : ValueClassUnboxConverter() { - override val unboxedType: Type get() = Int::class.java + override val unboxedType: Type get() = INT_CLASS override val unboxHandle: MethodHandle = - MethodHandles.lookup().unreflect(unboxMethod).asType(MethodType.methodType(Int::class.java, ANY_CLASS)) + MethodHandles.lookup().unreflect(unboxMethod).asType(ANY_TO_INT_METHOD_TYPE) override fun convert(value: T): Int = unboxHandle.invokeExact(value) as Int } @@ -134,9 +134,9 @@ internal class LongValueClassUnboxConverter( override val valueClass: Class, unboxMethod: Method, ) : ValueClassUnboxConverter() { - override val unboxedType: Type get() = Long::class.java + override val unboxedType: Type get() = LONG_CLASS override val unboxHandle: MethodHandle = - MethodHandles.lookup().unreflect(unboxMethod).asType(MethodType.methodType(Long::class.java, ANY_CLASS)) + MethodHandles.lookup().unreflect(unboxMethod).asType(ANY_TO_LONG_METHOD_TYPE) override fun convert(value: T): Long = unboxHandle.invokeExact(value) as Long } @@ -145,9 +145,9 @@ internal class StringValueClassUnboxConverter( override val valueClass: Class, unboxMethod: Method, ) : ValueClassUnboxConverter() { - override val unboxedType: Type get() = String::class.java + override val unboxedType: Type get() = STRING_CLASS override val unboxHandle: MethodHandle = - MethodHandles.lookup().unreflect(unboxMethod).asType(MethodType.methodType(String::class.java, ANY_CLASS)) + MethodHandles.lookup().unreflect(unboxMethod).asType(ANY_TO_STRING_METHOD_TYPE) override fun convert(value: T): String? = unboxHandle.invokeExact(value) as String? } @@ -156,9 +156,9 @@ internal class JavaUuidValueClassUnboxConverter( override val valueClass: Class, unboxMethod: Method, ) : ValueClassUnboxConverter() { - override val unboxedType: Type get() = UUID::class.java + override val unboxedType: Type get() = JAVA_UUID_CLASS override val unboxHandle: MethodHandle = - MethodHandles.lookup().unreflect(unboxMethod).asType(MethodType.methodType(UUID::class.java, ANY_CLASS)) + MethodHandles.lookup().unreflect(unboxMethod).asType(ANY_TO_JAVA_UUID_METHOD_TYPE) override fun convert(value: T): UUID? = unboxHandle.invokeExact(value) as UUID? } @@ -169,7 +169,7 @@ internal class GenericValueClassUnboxConverter( unboxMethod: Method, ) : ValueClassUnboxConverter() { override val unboxHandle: MethodHandle = - MethodHandles.lookup().unreflect(unboxMethod).asType(MethodType.methodType(ANY_CLASS, ANY_CLASS)) + MethodHandles.lookup().unreflect(unboxMethod).asType(ANY_TO_ANY_METHOD_TYPE) override fun convert(value: T): Any? = unboxHandle.invokeExact(value) } 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 f2d31fb5..d88bfbf8 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 @@ -22,49 +22,6 @@ internal fun Class<*>.toKmClass(): KmClass? = getAnnotation(METADATA_CLASS)?.let internal fun Class<*>.isUnboxableValueClass() = this.isAnnotationPresent(JVM_INLINE_CLASS) -private val primitiveClassToDesc = mapOf( - Byte::class.java to 'B', - Char::class.java to 'C', - Double::class.java to 'D', - Float::class.java to 'F', - Int::class.java to 'I', - Long::class.java to 'J', - Short::class.java to 'S', - Boolean::class.java to 'Z', - Void.TYPE to 'V', -) - -// -> this.name.replace(".", "/") -private fun Class<*>.descName(): String { - val replaced = name.toCharArray().apply { - for (i in indices) { - if (this[i] == '.') this[i] = '/' - } - } - return String(replaced) -} - -private fun StringBuilder.appendDescriptor(clazz: Class<*>): StringBuilder = when { - clazz.isPrimitive -> append(primitiveClassToDesc.getValue(clazz)) - clazz.isArray -> append('[').appendDescriptor(clazz.componentType) - else -> append("L${clazz.descName()};") -} - -// -> this.joinToString(separator = "", prefix = "(", postfix = ")") { it.descriptor } -internal fun Array>.toDescBuilder(): StringBuilder = this - .fold(StringBuilder("(")) { acc, cur -> acc.appendDescriptor(cur) } - .append(')') - -internal fun Constructor<*>.toSignature(): JvmMethodSignature = JvmMethodSignature( - "", - parameterTypes.toDescBuilder().append('V').toString(), -) - -internal fun Method.toSignature(): JvmMethodSignature = JvmMethodSignature( - this.name, - parameterTypes.toDescBuilder().appendDescriptor(this.returnType).toString(), -) - internal val defaultConstructorMarker: Class<*> by lazy { Class.forName("kotlin.jvm.internal.DefaultConstructorMarker") } @@ -106,6 +63,61 @@ internal val JSON_PROPERTY_CLASS = JsonProperty::class.java internal val JSON_K_UNBOX_CLASS = JsonKUnbox::class.java internal val KOTLIN_DURATION_CLASS = KotlinDuration::class.java internal val CLOSED_FLOATING_POINT_RANGE_CLASS = ClosedFloatingPointRange::class.java +internal val INT_CLASS = Int::class.java +internal val LONG_CLASS = Long::class.java +internal val STRING_CLASS = String::class.java +internal val JAVA_UUID_CLASS = java.util.UUID::class.java internal val ANY_CLASS = Any::class.java internal val ANY_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, ANY_CLASS) +internal val ANY_TO_INT_METHOD_TYPE = MethodType.methodType(INT_CLASS, ANY_CLASS) +internal val ANY_TO_LONG_METHOD_TYPE = MethodType.methodType(LONG_CLASS, ANY_CLASS) +internal val ANY_TO_STRING_METHOD_TYPE = MethodType.methodType(STRING_CLASS, ANY_CLASS) +internal val ANY_TO_JAVA_UUID_METHOD_TYPE = MethodType.methodType(JAVA_UUID_CLASS, ANY_CLASS) +internal val INT_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, INT_CLASS) +internal val LONG_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, LONG_CLASS) +internal val STRING_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, STRING_CLASS) +internal val JAVA_UUID_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, JAVA_UUID_CLASS) + +private val primitiveClassToDesc = mapOf( + Byte::class.java to 'B', + Char::class.java to 'C', + Double::class.java to 'D', + Float::class.java to 'F', + INT_CLASS to 'I', + LONG_CLASS to 'J', + Short::class.java to 'S', + Boolean::class.java to 'Z', + Void.TYPE to 'V', +) + +// -> this.name.replace(".", "/") +private fun Class<*>.descName(): String { + val replaced = name.toCharArray().apply { + for (i in indices) { + if (this[i] == '.') this[i] = '/' + } + } + return String(replaced) +} + +private fun StringBuilder.appendDescriptor(clazz: Class<*>): StringBuilder = when { + clazz.isPrimitive -> append(primitiveClassToDesc.getValue(clazz)) + clazz.isArray -> append('[').appendDescriptor(clazz.componentType) + else -> append("L${clazz.descName()};") +} + +// -> this.joinToString(separator = "", prefix = "(", postfix = ")") { it.descriptor } +internal fun Array>.toDescBuilder(): StringBuilder = this + .fold(StringBuilder("(")) { acc, cur -> acc.appendDescriptor(cur) } + .append(')') + +internal fun Constructor<*>.toSignature(): JvmMethodSignature = JvmMethodSignature( + "", + parameterTypes.toDescBuilder().append('V').toString(), +) + +internal fun Method.toSignature(): JvmMethodSignature = JvmMethodSignature( + this.name, + parameterTypes.toDescBuilder().appendDescriptor(this.returnType).toString(), +) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt index 01e31a1c..57517fbe 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt @@ -9,8 +9,11 @@ import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.exc.InvalidDefinitionException import com.fasterxml.jackson.databind.module.SimpleDeserializers -import io.github.projectmapk.jackson.module.kogera.ANY_CLASS import io.github.projectmapk.jackson.module.kogera.ANY_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.ANY_TO_INT_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.ANY_TO_JAVA_UUID_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.ANY_TO_LONG_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.ANY_TO_STRING_METHOD_TYPE import io.github.projectmapk.jackson.module.kogera.GenericValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.IntValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.JavaUuidValueClassBoxConverter @@ -26,10 +29,8 @@ import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass import io.github.projectmapk.jackson.module.kogera.toSignature import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles -import java.lang.invoke.MethodType import java.lang.reflect.Method import java.lang.reflect.Modifier -import java.util.UUID internal object SequenceDeserializer : StdDeserializer>(Sequence::class.java) { private fun readResolve(): Any = SequenceDeserializer @@ -105,8 +106,7 @@ internal class WrapsIntValueClassBoxDeserializer( private val handle: MethodHandle init { - val unreflect = MethodHandles.lookup().unreflect(creator) - .asType(MethodType.methodType(Int::class.java, ANY_CLASS)) + val unreflect = MethodHandles.lookup().unreflect(creator).asType(ANY_TO_INT_METHOD_TYPE) handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) } @@ -125,8 +125,7 @@ internal class WrapsLongValueClassBoxDeserializer( private val handle: MethodHandle init { - val unreflect = MethodHandles.lookup().unreflect(creator) - .asType(MethodType.methodType(Long::class.java, ANY_CLASS)) + val unreflect = MethodHandles.lookup().unreflect(creator).asType(ANY_TO_LONG_METHOD_TYPE) handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) } @@ -145,8 +144,7 @@ internal class WrapsStringValueClassBoxDeserializer( private val handle: MethodHandle init { - val unreflect = MethodHandles.lookup().unreflect(creator) - .asType(MethodType.methodType(String::class.java, ANY_CLASS)) + val unreflect = MethodHandles.lookup().unreflect(creator).asType(ANY_TO_STRING_METHOD_TYPE) handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) } @@ -176,8 +174,7 @@ internal class WrapsJavaUuidValueClassBoxDeserializer( private val handle: MethodHandle init { - val unreflect = MethodHandles.lookup().unreflect(creator) - .asType(MethodType.methodType(UUID::class.java, ANY_CLASS)) + val unreflect = MethodHandles.lookup().unreflect(creator).asType(ANY_TO_JAVA_UUID_METHOD_TYPE) handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/MethodValueCreator.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/MethodValueCreator.kt index 5618ad29..8c221cb8 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/MethodValueCreator.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/MethodValueCreator.kt @@ -2,6 +2,7 @@ package io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.crea import com.fasterxml.jackson.databind.util.ClassUtil import io.github.projectmapk.jackson.module.kogera.ANY_CLASS +import io.github.projectmapk.jackson.module.kogera.INT_CLASS import io.github.projectmapk.jackson.module.kogera.ReflectionCache import io.github.projectmapk.jackson.module.kogera.call import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argumentBucket.ArgumentBucket @@ -51,7 +52,7 @@ internal class MethodValueCreator( temp[0] = companionObjectClass // companion object parameterTypes.copyInto(temp, 1) // parameter types for (i in (valueParameterSize + 1)..(valueParameterSize + maskSize)) { // masks - temp[i] = Int::class.java + temp[i] = INT_CLASS } temp[valueParameterSize + maskSize + 1] = ANY_CLASS // maker temp From f1a1aac6b18b02635e1e06abf3d4af99125c0d61 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 24 May 2025 23:32:36 +0900 Subject: [PATCH 16/21] Modified ValueClassStaticJsonValueSerializer to use MethodHandle --- .../ser/serializers/KotlinSerializers.kt | 76 +++++++++++++++++-- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt index 332a1c91..e037271d 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt @@ -9,9 +9,21 @@ import com.fasterxml.jackson.databind.SerializationConfig import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.module.SimpleSerializers import com.fasterxml.jackson.databind.ser.std.StdSerializer +import io.github.projectmapk.jackson.module.kogera.ANY_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.GenericValueClassUnboxConverter +import io.github.projectmapk.jackson.module.kogera.INT_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.IntValueClassUnboxConverter +import io.github.projectmapk.jackson.module.kogera.JAVA_UUID_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.JavaUuidValueClassUnboxConverter +import io.github.projectmapk.jackson.module.kogera.LONG_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.LongValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.ReflectionCache +import io.github.projectmapk.jackson.module.kogera.STRING_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.StringValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles import java.lang.reflect.Method import java.lang.reflect.Modifier import java.math.BigInteger @@ -54,17 +66,57 @@ private fun Class<*>.getStaticJsonValueGetter(): Method? = this.declaredMethods. Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonValue && it.value } } -internal class ValueClassStaticJsonValueSerializer( - private val converter: ValueClassUnboxConverter, - private val staticJsonValueGetter: Method, +internal sealed class ValueClassStaticJsonValueSerializer( + converter: ValueClassUnboxConverter, + staticJsonValueHandle: MethodHandle, ) : StdSerializer(converter.valueClass) { - override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { - val unboxed = converter.convert(value) - // As shown in the processing of the factory function, jsonValueGetter is always a static method. - val jsonValue: Any? = staticJsonValueGetter.invoke(null, unboxed) + private val handle: MethodHandle = MethodHandles.filterReturnValue(converter.unboxHandle, staticJsonValueHandle) + + final override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { + val jsonValue: Any? = handle.invokeExact(value) provider.defaultSerializeValue(jsonValue, gen) } + internal class WrapsInt( + converter: IntValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonValueSerializer( + converter, + MethodHandles.lookup().unreflect(staticJsonValueGetter).asType(INT_TO_ANY_METHOD_TYPE), + ) + + internal class WrapsLong( + converter: LongValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonValueSerializer( + converter, + MethodHandles.lookup().unreflect(staticJsonValueGetter).asType(LONG_TO_ANY_METHOD_TYPE), + ) + + internal class WrapsString( + converter: StringValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonValueSerializer( + converter, + MethodHandles.lookup().unreflect(staticJsonValueGetter).asType(STRING_TO_ANY_METHOD_TYPE), + ) + + internal class WrapsJavaUuid( + converter: JavaUuidValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonValueSerializer( + converter, + MethodHandles.lookup().unreflect(staticJsonValueGetter).asType(JAVA_UUID_TO_ANY_METHOD_TYPE), + ) + + internal class WrapsAny( + converter: GenericValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonValueSerializer( + converter, + MethodHandles.lookup().unreflect(staticJsonValueGetter).asType(ANY_TO_ANY_METHOD_TYPE), + ) + companion object { // `t` must be UnboxableValueClass. // If create a function with a JsonValue in the value class, @@ -73,7 +125,15 @@ internal class ValueClassStaticJsonValueSerializer( fun createOrNull(converter: ValueClassUnboxConverter): StdSerializer? = converter .valueClass .getStaticJsonValueGetter() - ?.let { ValueClassStaticJsonValueSerializer(converter, it) } + ?.let { + when (converter) { + is IntValueClassUnboxConverter -> WrapsInt(converter, it) + is LongValueClassUnboxConverter -> WrapsLong(converter, it) + is StringValueClassUnboxConverter -> WrapsString(converter, it) + is JavaUuidValueClassUnboxConverter -> WrapsJavaUuid(converter, it) + is GenericValueClassUnboxConverter -> WrapsAny(converter, it) + } + } } } From daa717174e9bcc4603eb93021a173d6f36bfd97c Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 25 May 2025 00:51:46 +0900 Subject: [PATCH 17/21] Modified ValueClassKeyDeserializer to use MethodHandle --- .../deserializers/KotlinKeyDeserializers.kt | 110 +++++++++++++++--- .../WithoutCustomDeserializeMethodTest.kt | 5 +- 2 files changed, 98 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinKeyDeserializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinKeyDeserializers.kt index 3a6f93c9..a337566b 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinKeyDeserializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinKeyDeserializers.kt @@ -9,15 +9,25 @@ import com.fasterxml.jackson.databind.KeyDeserializer import com.fasterxml.jackson.databind.deser.std.StdKeyDeserializer import com.fasterxml.jackson.databind.exc.InvalidDefinitionException import com.fasterxml.jackson.databind.module.SimpleKeyDeserializers -import com.fasterxml.jackson.databind.util.ClassUtil +import io.github.projectmapk.jackson.module.kogera.ANY_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.GenericValueClassBoxConverter +import io.github.projectmapk.jackson.module.kogera.INT_CLASS +import io.github.projectmapk.jackson.module.kogera.IntValueClassBoxConverter +import io.github.projectmapk.jackson.module.kogera.JAVA_UUID_CLASS +import io.github.projectmapk.jackson.module.kogera.JavaUuidValueClassBoxConverter +import io.github.projectmapk.jackson.module.kogera.LONG_CLASS +import io.github.projectmapk.jackson.module.kogera.LongValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.ReflectionCache +import io.github.projectmapk.jackson.module.kogera.STRING_CLASS +import io.github.projectmapk.jackson.module.kogera.StringValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass import io.github.projectmapk.jackson.module.kogera.toSignature +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles import java.lang.reflect.Method import java.math.BigInteger -import kotlin.metadata.isSecondary -import kotlin.metadata.jvm.signature +import java.util.UUID // The reason why key is treated as nullable is to match the tentative behavior of StdKeyDeserializer. // If StdKeyDeserializer is modified, need to modify this too. @@ -51,34 +61,100 @@ internal object ULongKeyDeserializer : StdKeyDeserializer(-1, ULong::class.java) } // The implementation is designed to be compatible with various creators, just in case. -internal class ValueClassKeyDeserializer( - private val creator: Method, - private val converter: ValueClassBoxConverter, +internal sealed class ValueClassKeyDeserializer( + converter: ValueClassBoxConverter, + creatorHandle: MethodHandle, ) : KeyDeserializer() { - private val unboxedClass: Class<*> = creator.parameterTypes[0] + private val boxedClass: Class = converter.boxedClass - init { - creator.apply { ClassUtil.checkAndFixAccess(this, false) } - } + protected abstract val unboxedClass: Class<*> + protected val handle: MethodHandle = MethodHandles.filterReturnValue(creatorHandle, converter.boxHandle) // Based on databind error // https://github.com/FasterXML/jackson-databind/blob/341f8d360a5f10b5e609d6ee0ea023bf597ce98a/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java#L624 private fun errorMessage(boxedType: JavaType): String = "Could not find (Map) Key deserializer for types " + "wrapped in $boxedType" - override fun deserializeKey(key: String?, ctxt: DeserializationContext): Any { + // Since the input to handle must be strict, invoke should be implemented in each class + protected abstract fun invokeExact(value: S): D + + final override fun deserializeKey(key: String?, ctxt: DeserializationContext): Any { val unboxedJavaType = ctxt.constructType(unboxedClass) return try { // findKeyDeserializer does not return null, and an exception will be thrown if not found. val value = ctxt.findKeyDeserializer(unboxedJavaType, null).deserializeKey(key, ctxt) @Suppress("UNCHECKED_CAST") - converter.convert(creator.invoke(null, value) as S) + invokeExact(value as S) } catch (e: InvalidDefinitionException) { - throw JsonMappingException.from(ctxt, errorMessage(ctxt.constructType(converter.boxedClass)), e) + throw JsonMappingException.from(ctxt, errorMessage(ctxt.constructType(boxedClass)), e) } } + internal sealed class WrapsSpecified( + converter: ValueClassBoxConverter, + creator: Method, + ) : ValueClassKeyDeserializer( + converter, + // Currently, only the primary constructor can be the creator of a key, so for specified types, + // the return type of the primary constructor and the input type of the box function are exactly the same. + // Therefore, performance is improved by omitting the asType call. + MethodHandles.lookup().unreflect(creator), + ) + + internal class WrapsInt( + converter: IntValueClassBoxConverter, + creator: Method, + ) : WrapsSpecified(converter, creator) { + override val unboxedClass: Class<*> get() = INT_CLASS + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: Int): D = handle.invokeExact(value) as D + } + + internal class WrapsLong( + converter: LongValueClassBoxConverter, + creator: Method, + ) : WrapsSpecified(converter, creator) { + override val unboxedClass: Class<*> get() = LONG_CLASS + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: Long): D = handle.invokeExact(value) as D + } + + internal class WrapsString( + converter: StringValueClassBoxConverter, + creator: Method, + ) : WrapsSpecified(converter, creator) { + override val unboxedClass: Class<*> get() = STRING_CLASS + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: String?): D = handle.invokeExact(value) as D + } + + internal class WrapsJavaUuid( + converter: JavaUuidValueClassBoxConverter, + creator: Method, + ) : WrapsSpecified(converter, creator) { + override val unboxedClass: Class<*> get() = JAVA_UUID_CLASS + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: UUID?): D = handle.invokeExact(value) as D + } + + internal class WrapsAny( + converter: GenericValueClassBoxConverter, + creator: Method, + ) : ValueClassKeyDeserializer( + converter, + MethodHandles.lookup().unreflect(creator).asType(ANY_TO_ANY_METHOD_TYPE), + ) { + override val unboxedClass: Class<*> = creator.returnType + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: S): D = handle.invokeExact(value) as D + } + companion object { fun createOrNull( valueClass: Class<*>, @@ -97,7 +173,13 @@ internal class ValueClassKeyDeserializer( val converter = cache.getValueClassBoxConverter(unboxedClass, valueClass) - ValueClassKeyDeserializer(it, converter) + when (converter) { + is IntValueClassBoxConverter -> WrapsInt(converter, it) + is LongValueClassBoxConverter -> WrapsLong(converter, it) + is StringValueClassBoxConverter -> WrapsString(converter, it) + is JavaUuidValueClassBoxConverter -> WrapsJavaUuid(converter, it) + is GenericValueClassBoxConverter -> WrapsAny(converter, it) + } } } } diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt index 35c0c7d1..466f59d8 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt @@ -17,7 +17,6 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import java.lang.reflect.InvocationTargetException import com.fasterxml.jackson.databind.KeyDeserializer as JacksonKeyDeserializer class WithoutCustomDeserializeMethodTest { @@ -98,10 +97,10 @@ class WithoutCustomDeserializeMethodTest { @Test fun callConstructorCheckTest() { - val e = assertThrows { + val e = assertThrows { defaultMapper.readValue>("""{"-1":null}""") } - assertTrue(e.cause === throwable) + assertTrue(e === throwable) } data class Wrapped(val first: String, val second: String) { From 29f955c69f7aba8e94d3e3c0cb3670f9bc679670 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 25 May 2025 02:08:14 +0900 Subject: [PATCH 18/21] Decrialization is made more efficient by reducing the asType conversions --- .../WrapsNullableValueClassDeserializer.java | 4 +- .../jackson/module/kogera/Converters.kt | 11 +- .../deserializers/KotlinDeserializers.kt | 145 +++++++++++------- 3 files changed, 95 insertions(+), 65 deletions(-) diff --git a/src/main/java/io/github/projectmapk/jackson/module/kogera/deser/WrapsNullableValueClassDeserializer.java b/src/main/java/io/github/projectmapk/jackson/module/kogera/deser/WrapsNullableValueClassDeserializer.java index 852dd097..12e19136 100644 --- a/src/main/java/io/github/projectmapk/jackson/module/kogera/deser/WrapsNullableValueClassDeserializer.java +++ b/src/main/java/io/github/projectmapk/jackson/module/kogera/deser/WrapsNullableValueClassDeserializer.java @@ -5,7 +5,7 @@ import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import io.github.projectmapk.jackson.module.kogera.deser.deserializers.WrapsNullableValueClassBoxDeserializer; +import io.github.projectmapk.jackson.module.kogera.deser.deserializers.WrapsAnyValueClassBoxDeserializer; import kotlin.jvm.JvmClassMappingKt; import kotlin.reflect.KClass; import org.jetbrains.annotations.NotNull; @@ -15,7 +15,7 @@ /** * An interface to be inherited by JsonDeserializer that handles value classes that may wrap nullable. - * @see WrapsNullableValueClassBoxDeserializer for implementation. + * @see WrapsAnyValueClassBoxDeserializer for implementation. */ // To ensure maximum compatibility with StdDeserializer, this class is written in Java. public abstract class WrapsNullableValueClassDeserializer extends StdDeserializer { diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt index 5d8253de..d7de9d73 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt @@ -37,12 +37,15 @@ internal sealed class ValueClassBoxConverter : StdConverter GenericValueClassBoxConverter(unboxedClass, valueClass) } } + + // If the wrapped type is explicitly specified, it is inherited for the sake of distinction + internal sealed class Specified : ValueClassBoxConverter() } // region: Converters for common classes as wrapped values, add as needed. internal class IntValueClassBoxConverter( override val boxedClass: Class, -) : ValueClassBoxConverter() { +) : ValueClassBoxConverter.Specified() { override val boxHandle: MethodHandle = rawBoxHandle(INT_CLASS).asType(INT_TO_ANY_METHOD_TYPE) @Suppress("UNCHECKED_CAST") @@ -51,7 +54,7 @@ internal class IntValueClassBoxConverter( internal class LongValueClassBoxConverter( override val boxedClass: Class, -) : ValueClassBoxConverter() { +) : ValueClassBoxConverter.Specified() { override val boxHandle: MethodHandle = rawBoxHandle(LONG_CLASS).asType(LONG_TO_ANY_METHOD_TYPE) @Suppress("UNCHECKED_CAST") @@ -60,7 +63,7 @@ internal class LongValueClassBoxConverter( internal class StringValueClassBoxConverter( override val boxedClass: Class, -) : ValueClassBoxConverter() { +) : ValueClassBoxConverter.Specified() { override val boxHandle: MethodHandle = rawBoxHandle(STRING_CLASS).asType(STRING_TO_ANY_METHOD_TYPE) @Suppress("UNCHECKED_CAST") @@ -69,7 +72,7 @@ internal class StringValueClassBoxConverter( internal class JavaUuidValueClassBoxConverter( override val boxedClass: Class, -) : ValueClassBoxConverter() { +) : ValueClassBoxConverter.Specified() { override val boxHandle: MethodHandle = rawBoxHandle(JAVA_UUID_CLASS).asType(JAVA_UUID_TO_ANY_METHOD_TYPE) @Suppress("UNCHECKED_CAST") diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt index 57517fbe..934bce83 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt @@ -9,18 +9,20 @@ import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.exc.InvalidDefinitionException import com.fasterxml.jackson.databind.module.SimpleDeserializers +import io.github.projectmapk.jackson.module.kogera.ANY_CLASS import io.github.projectmapk.jackson.module.kogera.ANY_TO_ANY_METHOD_TYPE -import io.github.projectmapk.jackson.module.kogera.ANY_TO_INT_METHOD_TYPE -import io.github.projectmapk.jackson.module.kogera.ANY_TO_JAVA_UUID_METHOD_TYPE -import io.github.projectmapk.jackson.module.kogera.ANY_TO_LONG_METHOD_TYPE -import io.github.projectmapk.jackson.module.kogera.ANY_TO_STRING_METHOD_TYPE import io.github.projectmapk.jackson.module.kogera.GenericValueClassBoxConverter +import io.github.projectmapk.jackson.module.kogera.INT_CLASS import io.github.projectmapk.jackson.module.kogera.IntValueClassBoxConverter +import io.github.projectmapk.jackson.module.kogera.JAVA_UUID_CLASS import io.github.projectmapk.jackson.module.kogera.JavaUuidValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.KotlinDuration +import io.github.projectmapk.jackson.module.kogera.LONG_CLASS import io.github.projectmapk.jackson.module.kogera.LongValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.ReflectionCache +import io.github.projectmapk.jackson.module.kogera.STRING_CLASS import io.github.projectmapk.jackson.module.kogera.StringValueClassBoxConverter +import io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.deser.JavaToKotlinDurationConverter import io.github.projectmapk.jackson.module.kogera.deser.WrapsNullableValueClassDeserializer import io.github.projectmapk.jackson.module.kogera.hasCreatorAnnotation @@ -31,6 +33,7 @@ import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles import java.lang.reflect.Method import java.lang.reflect.Modifier +import java.util.UUID internal object SequenceDeserializer : StdDeserializer>(Sequence::class.java) { private fun readResolve(): Any = SequenceDeserializer @@ -98,83 +101,101 @@ internal object ULongDeserializer : StdDeserializer(ULong::class.java) { .readWithRangeCheck(p, p.bigIntegerValue) } -internal class WrapsIntValueClassBoxDeserializer( +// If the creator does not perform type conversion, implement a unique deserializer for each for fast invocation. +internal sealed class NoConversionCreatorBoxDeserializer( creator: Method, - converter: IntValueClassBoxConverter, -) : StdDeserializer(converter.boxedClass) { - private val inputType: Class<*> = creator.parameterTypes[0] - private val handle: MethodHandle + converter: ValueClassBoxConverter, +) : WrapsNullableValueClassDeserializer(converter.boxedClass) { + protected abstract val inputType: Class<*> + protected val handle: MethodHandle init { - val unreflect = MethodHandles.lookup().unreflect(creator).asType(ANY_TO_INT_METHOD_TYPE) + val unreflect = MethodHandles.lookup().unreflect(creator) handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) } - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D { - val input = p.readValueAs(inputType) + // Since the input to handle must be strict, invoke should be implemented in each class + protected abstract fun invokeExact(value: S): D + + // Cache the result of wrapping null, since the result is always expected to be the same. + @get:JvmName("boxedNullValue") + private val boxedNullValue: D by lazy { + // For the sake of commonality, it is unavoidably called without checking. + // It is controlled by KotlinValueInstantiator, so it is not expected to reach this branch. @Suppress("UNCHECKED_CAST") - return handle.invokeExact(input) as D + invokeExact(null as S) } -} -internal class WrapsLongValueClassBoxDeserializer( - creator: Method, - converter: LongValueClassBoxConverter, -) : StdDeserializer(converter.boxedClass) { - private val inputType: Class<*> = creator.parameterTypes[0] - private val handle: MethodHandle + final override fun getBoxedNullValue(): D = boxedNullValue - init { - val unreflect = MethodHandles.lookup().unreflect(creator).asType(ANY_TO_LONG_METHOD_TYPE) - handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) + final override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D { + @Suppress("UNCHECKED_CAST") + return invokeExact(p.readValueAs(inputType) as S) } - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D { - val input = p.readValueAs(inputType) + internal class WrapsInt( + creator: Method, + converter: IntValueClassBoxConverter, + ) : NoConversionCreatorBoxDeserializer(creator, converter) { + override val inputType get() = INT_CLASS + @Suppress("UNCHECKED_CAST") - return handle.invokeExact(input) as D + override fun invokeExact(value: Int): D = handle.invokeExact(value) as D } -} -internal class WrapsStringValueClassBoxDeserializer( - creator: Method, - converter: StringValueClassBoxConverter, -) : WrapsNullableValueClassDeserializer(converter.boxedClass) { - private val inputType: Class<*> = creator.parameterTypes[0] - private val handle: MethodHandle + internal class WrapsLong( + creator: Method, + converter: LongValueClassBoxConverter, + ) : NoConversionCreatorBoxDeserializer(creator, converter) { + override val inputType get() = LONG_CLASS - init { - val unreflect = MethodHandles.lookup().unreflect(creator).asType(ANY_TO_STRING_METHOD_TYPE) - handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: Long): D = handle.invokeExact(value) as D } - // Cache the result of wrapping null, since the result is always expected to be the same. - @get:JvmName("boxedNullValue") - private val boxedNullValue: D by lazy { instantiate(null) } + internal class WrapsString( + creator: Method, + converter: StringValueClassBoxConverter, + ) : NoConversionCreatorBoxDeserializer(creator, converter) { + override val inputType get() = STRING_CLASS - override fun getBoxedNullValue(): D = boxedNullValue + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: String?): D = handle.invokeExact(value) as D + } - // To instantiate the value class in the same way as other classes, - // it is necessary to call creator(e.g. constructor-impl) -> box-impl in that order. - // Input is null only when called from KotlinValueInstantiator. - @Suppress("UNCHECKED_CAST") - private fun instantiate(input: Any?): D = handle.invokeExact(input) as D + internal class WrapsJavaUuid( + creator: Method, + converter: JavaUuidValueClassBoxConverter, + ) : NoConversionCreatorBoxDeserializer(creator, converter) { + override val inputType get() = JAVA_UUID_CLASS - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D { - val input = p.readValueAs(inputType) - return instantiate(input) + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: UUID?): D = handle.invokeExact(value) as D + } + + companion object { + fun create(creator: Method, converter: ValueClassBoxConverter.Specified) = when (converter) { + is IntValueClassBoxConverter -> WrapsInt(creator, converter) + is LongValueClassBoxConverter -> WrapsLong(creator, converter) + is StringValueClassBoxConverter -> WrapsString(creator, converter) + is JavaUuidValueClassBoxConverter -> WrapsJavaUuid(creator, converter) + } } } -internal class WrapsJavaUuidValueClassBoxDeserializer( +// Even if the creator performs type conversion, it is distinguished +// because a speedup due to rtype matching of filterReturnValue can be expected for the specified type. +internal class HasConversionCreatorWrapsSpecifiedBoxDeserializer( creator: Method, - converter: JavaUuidValueClassBoxConverter, + private val inputType: Class<*>, + converter: ValueClassBoxConverter, ) : WrapsNullableValueClassDeserializer(converter.boxedClass) { - private val inputType: Class<*> = creator.parameterTypes[0] private val handle: MethodHandle init { - val unreflect = MethodHandles.lookup().unreflect(creator).asType(ANY_TO_JAVA_UUID_METHOD_TYPE) + val unreflect = MethodHandles.lookup().unreflect(creator).run { + asType(type().changeParameterType(0, ANY_CLASS)) + } handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) } @@ -196,11 +217,11 @@ internal class WrapsJavaUuidValueClassBoxDeserializer( } } -internal class WrapsNullableValueClassBoxDeserializer( +internal class WrapsAnyValueClassBoxDeserializer( creator: Method, + private val inputType: Class<*>, converter: GenericValueClassBoxConverter, ) : WrapsNullableValueClassDeserializer(converter.boxedClass) { - private val inputType: Class<*> = creator.parameterTypes[0] private val handle: MethodHandle init { @@ -281,11 +302,17 @@ internal class KotlinDeserializers( val converter = cache.getValueClassBoxConverter(unboxedClass, rawClass) when (converter) { - is IntValueClassBoxConverter -> WrapsIntValueClassBoxDeserializer(it, converter) - is LongValueClassBoxConverter -> WrapsLongValueClassBoxDeserializer(it, converter) - is StringValueClassBoxConverter -> WrapsStringValueClassBoxDeserializer(it, converter) - is JavaUuidValueClassBoxConverter -> WrapsJavaUuidValueClassBoxDeserializer(it, converter) - is GenericValueClassBoxConverter -> WrapsNullableValueClassBoxDeserializer(it, converter) + is ValueClassBoxConverter.Specified -> { + val inputType = it.parameterTypes[0] + + if (inputType == unboxedClass) { + NoConversionCreatorBoxDeserializer.create(it, converter) + } else { + HasConversionCreatorWrapsSpecifiedBoxDeserializer(it, inputType, converter) + } + } + is GenericValueClassBoxConverter -> + WrapsAnyValueClassBoxDeserializer(it, it.parameterTypes[0], converter) } } else -> null From 1579484a6e57303cca065b6897a2ae88ac21ec0c Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 25 May 2025 02:49:42 +0900 Subject: [PATCH 19/21] Functionalized for simplicity --- .../jackson/module/kogera/Converters.kt | 15 +++++---------- .../jackson/module/kogera/InternalCommons.kt | 5 +++++ .../deser/deserializers/KotlinDeserializers.kt | 14 ++++++-------- .../deser/deserializers/KotlinKeyDeserializers.kt | 6 ++++-- .../kogera/ser/serializers/KotlinSerializers.kt | 11 ++++++----- 5 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt index d7de9d73..7cfa252a 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt @@ -127,8 +127,7 @@ internal class IntValueClassUnboxConverter( unboxMethod: Method, ) : ValueClassUnboxConverter() { override val unboxedType: Type get() = INT_CLASS - override val unboxHandle: MethodHandle = - MethodHandles.lookup().unreflect(unboxMethod).asType(ANY_TO_INT_METHOD_TYPE) + override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_INT_METHOD_TYPE) override fun convert(value: T): Int = unboxHandle.invokeExact(value) as Int } @@ -138,8 +137,7 @@ internal class LongValueClassUnboxConverter( unboxMethod: Method, ) : ValueClassUnboxConverter() { override val unboxedType: Type get() = LONG_CLASS - override val unboxHandle: MethodHandle = - MethodHandles.lookup().unreflect(unboxMethod).asType(ANY_TO_LONG_METHOD_TYPE) + override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_LONG_METHOD_TYPE) override fun convert(value: T): Long = unboxHandle.invokeExact(value) as Long } @@ -149,8 +147,7 @@ internal class StringValueClassUnboxConverter( unboxMethod: Method, ) : ValueClassUnboxConverter() { override val unboxedType: Type get() = STRING_CLASS - override val unboxHandle: MethodHandle = - MethodHandles.lookup().unreflect(unboxMethod).asType(ANY_TO_STRING_METHOD_TYPE) + override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_STRING_METHOD_TYPE) override fun convert(value: T): String? = unboxHandle.invokeExact(value) as String? } @@ -160,8 +157,7 @@ internal class JavaUuidValueClassUnboxConverter( unboxMethod: Method, ) : ValueClassUnboxConverter() { override val unboxedType: Type get() = JAVA_UUID_CLASS - override val unboxHandle: MethodHandle = - MethodHandles.lookup().unreflect(unboxMethod).asType(ANY_TO_JAVA_UUID_METHOD_TYPE) + override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_JAVA_UUID_METHOD_TYPE) override fun convert(value: T): UUID? = unboxHandle.invokeExact(value) as UUID? } @@ -171,8 +167,7 @@ internal class GenericValueClassUnboxConverter( override val unboxedType: Type, unboxMethod: Method, ) : ValueClassUnboxConverter() { - override val unboxHandle: MethodHandle = - MethodHandles.lookup().unreflect(unboxMethod).asType(ANY_TO_ANY_METHOD_TYPE) + override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_ANY_METHOD_TYPE) override fun convert(value: T): Any? = unboxHandle.invokeExact(value) } 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 d88bfbf8..d77871e7 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 @@ -3,6 +3,8 @@ package io.github.projectmapk.jackson.module.kogera import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty import io.github.projectmapk.jackson.module.kogera.annotation.JsonKUnbox +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType import java.lang.reflect.AnnotatedElement import java.lang.reflect.Constructor @@ -79,6 +81,9 @@ internal val LONG_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, LONG_CLA internal val STRING_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, STRING_CLASS) internal val JAVA_UUID_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, JAVA_UUID_CLASS) +internal fun unreflect(method: Method): MethodHandle = MethodHandles.lookup().unreflect(method) +internal fun unreflectAsType(method: Method, type: MethodType): MethodHandle = unreflect(method).asType(type) + private val primitiveClassToDesc = mapOf( Byte::class.java to 'B', Char::class.java to 'C', diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt index 934bce83..387d906c 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt @@ -29,6 +29,8 @@ import io.github.projectmapk.jackson.module.kogera.hasCreatorAnnotation import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass import io.github.projectmapk.jackson.module.kogera.toSignature +import io.github.projectmapk.jackson.module.kogera.unreflect +import io.github.projectmapk.jackson.module.kogera.unreflectAsType import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles import java.lang.reflect.Method @@ -107,12 +109,8 @@ internal sealed class NoConversionCreatorBoxDeserializer( converter: ValueClassBoxConverter, ) : WrapsNullableValueClassDeserializer(converter.boxedClass) { protected abstract val inputType: Class<*> - protected val handle: MethodHandle - - init { - val unreflect = MethodHandles.lookup().unreflect(creator) - handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) - } + protected val handle: MethodHandle = MethodHandles + .filterReturnValue(unreflect(creator), converter.boxHandle) // Since the input to handle must be strict, invoke should be implemented in each class protected abstract fun invokeExact(value: S): D @@ -193,7 +191,7 @@ internal class HasConversionCreatorWrapsSpecifiedBoxDeserializer( private val handle: MethodHandle init { - val unreflect = MethodHandles.lookup().unreflect(creator).run { + val unreflect = unreflect(creator).run { asType(type().changeParameterType(0, ANY_CLASS)) } handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) @@ -225,7 +223,7 @@ internal class WrapsAnyValueClassBoxDeserializer( private val handle: MethodHandle init { - val unreflect = MethodHandles.lookup().unreflect(creator).asType(ANY_TO_ANY_METHOD_TYPE) + val unreflect = unreflectAsType(creator, ANY_TO_ANY_METHOD_TYPE) handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinKeyDeserializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinKeyDeserializers.kt index a337566b..d0803ce3 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinKeyDeserializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinKeyDeserializers.kt @@ -23,6 +23,8 @@ import io.github.projectmapk.jackson.module.kogera.StringValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass import io.github.projectmapk.jackson.module.kogera.toSignature +import io.github.projectmapk.jackson.module.kogera.unreflect +import io.github.projectmapk.jackson.module.kogera.unreflectAsType import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles import java.lang.reflect.Method @@ -99,7 +101,7 @@ internal sealed class ValueClassKeyDeserializer( // Currently, only the primary constructor can be the creator of a key, so for specified types, // the return type of the primary constructor and the input type of the box function are exactly the same. // Therefore, performance is improved by omitting the asType call. - MethodHandles.lookup().unreflect(creator), + unreflect(creator), ) internal class WrapsInt( @@ -147,7 +149,7 @@ internal sealed class ValueClassKeyDeserializer( creator: Method, ) : ValueClassKeyDeserializer( converter, - MethodHandles.lookup().unreflect(creator).asType(ANY_TO_ANY_METHOD_TYPE), + unreflectAsType(creator, ANY_TO_ANY_METHOD_TYPE), ) { override val unboxedClass: Class<*> = creator.returnType diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt index e037271d..d7e19e56 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt @@ -22,6 +22,7 @@ import io.github.projectmapk.jackson.module.kogera.STRING_TO_ANY_METHOD_TYPE import io.github.projectmapk.jackson.module.kogera.StringValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass +import io.github.projectmapk.jackson.module.kogera.unreflectAsType import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles import java.lang.reflect.Method @@ -82,7 +83,7 @@ internal sealed class ValueClassStaticJsonValueSerializer( staticJsonValueGetter: Method, ) : ValueClassStaticJsonValueSerializer( converter, - MethodHandles.lookup().unreflect(staticJsonValueGetter).asType(INT_TO_ANY_METHOD_TYPE), + unreflectAsType(staticJsonValueGetter, INT_TO_ANY_METHOD_TYPE), ) internal class WrapsLong( @@ -90,7 +91,7 @@ internal sealed class ValueClassStaticJsonValueSerializer( staticJsonValueGetter: Method, ) : ValueClassStaticJsonValueSerializer( converter, - MethodHandles.lookup().unreflect(staticJsonValueGetter).asType(LONG_TO_ANY_METHOD_TYPE), + unreflectAsType(staticJsonValueGetter, LONG_TO_ANY_METHOD_TYPE), ) internal class WrapsString( @@ -98,7 +99,7 @@ internal sealed class ValueClassStaticJsonValueSerializer( staticJsonValueGetter: Method, ) : ValueClassStaticJsonValueSerializer( converter, - MethodHandles.lookup().unreflect(staticJsonValueGetter).asType(STRING_TO_ANY_METHOD_TYPE), + unreflectAsType(staticJsonValueGetter, STRING_TO_ANY_METHOD_TYPE), ) internal class WrapsJavaUuid( @@ -106,7 +107,7 @@ internal sealed class ValueClassStaticJsonValueSerializer( staticJsonValueGetter: Method, ) : ValueClassStaticJsonValueSerializer( converter, - MethodHandles.lookup().unreflect(staticJsonValueGetter).asType(JAVA_UUID_TO_ANY_METHOD_TYPE), + unreflectAsType(staticJsonValueGetter, JAVA_UUID_TO_ANY_METHOD_TYPE), ) internal class WrapsAny( @@ -114,7 +115,7 @@ internal sealed class ValueClassStaticJsonValueSerializer( staticJsonValueGetter: Method, ) : ValueClassStaticJsonValueSerializer( converter, - MethodHandles.lookup().unreflect(staticJsonValueGetter).asType(ANY_TO_ANY_METHOD_TYPE), + unreflectAsType(staticJsonValueGetter, ANY_TO_ANY_METHOD_TYPE), ) companion object { From 4ccec130c72dcbaadc49e405e933c10a984f5dfc Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 14 Jun 2025 19:43:04 +0900 Subject: [PATCH 20/21] Fixed return value to a more concrete type --- .../module/kogera/ser/serializers/KotlinSerializers.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt index d7e19e56..811942fe 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt @@ -123,7 +123,9 @@ internal sealed class ValueClassStaticJsonValueSerializer( // If create a function with a JsonValue in the value class, // it will be compiled as a static method (= cannot be processed properly by Jackson), // so use a ValueClassSerializer.StaticJsonValue to handle this. - fun createOrNull(converter: ValueClassUnboxConverter): StdSerializer? = converter + fun createOrNull( + converter: ValueClassUnboxConverter, + ): ValueClassStaticJsonValueSerializer? = converter .valueClass .getStaticJsonValueGetter() ?.let { From 81a5c8feced5a316137105e4057eb8a9de47036e Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 14 Jun 2025 19:55:07 +0900 Subject: [PATCH 21/21] Modified ValueClassStaticJsonKeySerializer to use MethodHandle --- .../ser/serializers/KotlinKeySerializers.kt | 103 +++++++++++++++--- 1 file changed, 88 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinKeySerializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinKeySerializers.kt index 9a7ac58c..e7e7dd26 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinKeySerializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinKeySerializers.kt @@ -9,9 +9,23 @@ import com.fasterxml.jackson.databind.SerializationConfig import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.module.SimpleSerializers import com.fasterxml.jackson.databind.ser.std.StdSerializer +import io.github.projectmapk.jackson.module.kogera.ANY_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.GenericValueClassUnboxConverter +import io.github.projectmapk.jackson.module.kogera.INT_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.IntValueClassUnboxConverter +import io.github.projectmapk.jackson.module.kogera.JAVA_UUID_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.JavaUuidValueClassUnboxConverter +import io.github.projectmapk.jackson.module.kogera.LONG_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.LongValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.ReflectionCache +import io.github.projectmapk.jackson.module.kogera.STRING_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.StringValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass +import io.github.projectmapk.jackson.module.kogera.unreflectAsType +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles +import java.lang.invoke.MethodType import java.lang.reflect.Method import java.lang.reflect.Modifier @@ -31,21 +45,18 @@ internal class ValueClassUnboxKeySerializer( } } -// Class must be UnboxableValueClass. -private fun Class<*>.getStaticJsonKeyGetter(): Method? = this.declaredMethods.find { method -> - Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonKey && it.value } -} - -internal class ValueClassStaticJsonKeySerializer( - private val converter: ValueClassUnboxConverter, - private val staticJsonKeyGetter: Method, +internal sealed class ValueClassStaticJsonKeySerializer( + converter: ValueClassUnboxConverter, + staticJsonValueGetter: Method, + methodType: MethodType, ) : StdSerializer(converter.valueClass) { - private val keyType: Class<*> = staticJsonKeyGetter.returnType + private val keyType: Class<*> = staticJsonValueGetter.returnType + private val handle: MethodHandle = unreflectAsType(staticJsonValueGetter, methodType).let { + MethodHandles.filterReturnValue(converter.unboxHandle, it) + } - override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { - val unboxed = converter.convert(value) - // As shown in the processing of the factory function, jsonValueGetter is always a static method. - val jsonKey: Any? = staticJsonKeyGetter.invoke(null, unboxed) + final override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { + val jsonKey: Any? = handle.invokeExact(value) val serializer = jsonKey ?.let { provider.findKeySerializer(keyType, null) } @@ -54,14 +65,76 @@ internal class ValueClassStaticJsonKeySerializer( serializer.serialize(jsonKey, gen, provider) } + internal class WrapsInt( + converter: IntValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonKeySerializer( + converter, + staticJsonValueGetter, + INT_TO_ANY_METHOD_TYPE, + ) + + internal class WrapsLong( + converter: LongValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonKeySerializer( + converter, + staticJsonValueGetter, + LONG_TO_ANY_METHOD_TYPE, + ) + + internal class WrapsString( + converter: StringValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonKeySerializer( + converter, + staticJsonValueGetter, + STRING_TO_ANY_METHOD_TYPE, + ) + + internal class WrapsJavaUuid( + converter: JavaUuidValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonKeySerializer( + converter, + staticJsonValueGetter, + JAVA_UUID_TO_ANY_METHOD_TYPE, + ) + + internal class WrapsAny( + converter: GenericValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonKeySerializer( + converter, + staticJsonValueGetter, + ANY_TO_ANY_METHOD_TYPE, + + ) + companion object { + // Class must be UnboxableValueClass. + private fun Class<*>.getStaticJsonKeyGetter(): Method? = this.declaredMethods.find { method -> + Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonKey && it.value } + } + // `t` must be UnboxableValueClass. // If create a function with a JsonValue in the value class, // it will be compiled as a static method (= cannot be processed properly by Jackson), // so use a ValueClassSerializer.StaticJsonValue to handle this. - fun createOrNull(converter: ValueClassUnboxConverter): StdSerializer? = converter.valueClass + fun createOrNull( + converter: ValueClassUnboxConverter, + ): ValueClassStaticJsonKeySerializer? = converter + .valueClass .getStaticJsonKeyGetter() - ?.let { ValueClassStaticJsonKeySerializer(converter, it) } + ?.let { + when (converter) { + is IntValueClassUnboxConverter -> WrapsInt(converter, it) + is LongValueClassUnboxConverter -> WrapsLong(converter, it) + is StringValueClassUnboxConverter -> WrapsString(converter, it) + is JavaUuidValueClassUnboxConverter -> WrapsJavaUuid(converter, it) + is GenericValueClassUnboxConverter -> WrapsAny(converter, it) + } + } } }