diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..93835bdf --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [k163377] diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 65ff6709..9d5d3355 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,13 +6,20 @@ version: 2 updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "friday" + time: "19:00" + timezone: "Asia/Tokyo" - package-ecosystem: "gradle" directory: "/" schedule: interval: "weekly" - # Friday 19:00 JST - day: "saturday" - time: "04:00" + day: "friday" + time: "19:00" + timezone: "Asia/Tokyo" groups: dependencies: patterns: diff --git a/.github/workflows/lint-and-test-dev.yml b/.github/workflows/lint-and-test-dev.yml index c14f7ad1..edf280da 100644 --- a/.github/workflows/lint-and-test-dev.yml +++ b/.github/workflows/lint-and-test-dev.yml @@ -31,14 +31,14 @@ on: permissions: contents: write # for Dependency submission + checks: write # for action-junit-report + pull-requests: write # for action-junit-report jobs: lint-and-test-dev: name: lint-and-test-dev runs-on: ubuntu-latest timeout-minutes: 15 - permissions: # for gradle-dependency-submission - contents: write steps: - name: Checkout uses: actions/checkout@v4 @@ -55,4 +55,9 @@ jobs: - name: Lint run: ./gradlew lintKotlin - name: Test - run: ./gradlew lintKotlin test + run: ./gradlew test + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + if: always() + with: + report_paths: '**/build/test-results/test/TEST-*.xml' diff --git a/.github/workflows/test-main.yml b/.github/workflows/test-main.yml index c8781f84..95c1852c 100644 --- a/.github/workflows/test-main.yml +++ b/.github/workflows/test-main.yml @@ -29,6 +29,11 @@ on: - "**.java" - .github/workflows/test-main.yml workflow_dispatch: + +permissions: + checks: write # for action-junit-report + pull-requests: write # for action-junit-report + jobs: test-main: strategy: @@ -72,3 +77,8 @@ jobs: uses: gradle/actions/setup-gradle@v4 - name: Test run: ./gradlew test + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + if: always() + with: + report_paths: '**/build/test-results/test/TEST-*.xml' diff --git a/build.gradle.kts b/build.gradle.kts index 322e6c6a..7f10e746 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,7 +16,7 @@ val jacksonVersion = libs.versions.jackson.get() val generatedSrcPath = "${layout.buildDirectory.get()}/generated/kotlin" group = groupStr -version = "${jacksonVersion}-beta17" +version = "${jacksonVersion}-beta18" repositories { mavenCentral() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dbc47b6f..9b32f876 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] kotlin = "1.9.25" # Mainly for CI, it can be rewritten by environment variable. -jackson = "2.18.2" +jackson = "2.18.3" # test libs junit = "5.11.4" @@ -16,7 +16,7 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } -mockk = "io.mockk:mockk:1.13.16" +mockk = "io.mockk:mockk:1.13.17" jackson-xml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-xml", version.ref = "jackson" } jackson-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/KotlinModule.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/KotlinModule.kt index 440899a9..1d130a18 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/KotlinModule.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/KotlinModule.kt @@ -79,7 +79,7 @@ public class KotlinModule private constructor( _deserializers = KotlinDeserializers(cache, useJavaDurationConversion) _keySerializers = KotlinKeySerializers(cache) - _keyDeserializers = KotlinKeyDeserializers + _keyDeserializers = KotlinKeyDeserializers(cache) _abstractTypes = ClosedRangeResolver 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 accc19ae..454b025f 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 @@ -4,9 +4,18 @@ import com.fasterxml.jackson.databind.BeanDescription import com.fasterxml.jackson.databind.DeserializationConfig import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JavaType +import com.fasterxml.jackson.databind.JsonMappingException 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 io.github.projectmapk.jackson.module.kogera.ReflectionCache +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 kotlinx.metadata.isSecondary +import kotlinx.metadata.jvm.signature +import java.lang.reflect.Method import java.math.BigInteger // The reason why key is treated as nullable is to match the tentative behavior of StdKeyDeserializer. @@ -40,18 +49,74 @@ internal object ULongKeyDeserializer : StdKeyDeserializer(-1, ULong::class.java) key?.let { ULongChecker.readWithRangeCheck(null, BigInteger(it)) } } -internal object KotlinKeyDeserializers : SimpleKeyDeserializers() { - private fun readResolve(): Any = KotlinKeyDeserializers +// The implementation is designed to be compatible with various creators, just in case. +internal class ValueClassKeyDeserializer( + private val creator: Method, + private val converter: ValueClassBoxConverter +) : KeyDeserializer() { + private val unboxedClass: Class<*> = creator.parameterTypes[0] + init { + creator.apply { if (!this.isAccessible) this.isAccessible = true } + } + + // 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 { + 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) + } catch (e: InvalidDefinitionException) { + throw JsonMappingException.from(ctxt, errorMessage(ctxt.constructType(converter.boxedClass)), e) + } + } + + companion object { + fun createOrNull( + valueClass: Class<*>, + cache: ReflectionCache + ): ValueClassKeyDeserializer<*, *>? { + val jmClass = cache.getJmClass(valueClass) ?: return null + val primaryKmConstructorSignature = + jmClass.constructors.first { !it.isSecondary }.signature + + // Only primary constructor is allowed as creator, regardless of visibility. + // This is because it is based on the WrapsNullableValueClassBoxDeserializer. + // Also, as far as I could research, there was no such functionality as JsonKeyCreator, + // so it was not taken into account. + return valueClass.declaredMethods.find { it.toSignature() == primaryKmConstructorSignature }?.let { + val unboxedClass = it.returnType + + val converter = cache.getValueClassBoxConverter(unboxedClass, valueClass) + + ValueClassKeyDeserializer(it, converter) + } + } + } +} + +internal class KotlinKeyDeserializers(private val cache: ReflectionCache) : SimpleKeyDeserializers() { override fun findKeyDeserializer( type: JavaType, config: DeserializationConfig?, beanDesc: BeanDescription? - ): KeyDeserializer? = when (type.rawClass) { - UByte::class.java -> UByteKeyDeserializer - UShort::class.java -> UShortKeyDeserializer - UInt::class.java -> UIntKeyDeserializer - ULong::class.java -> ULongKeyDeserializer - else -> null + ): KeyDeserializer? { + val rawClass = type.rawClass + + return when { + rawClass == UByte::class.java -> UByteKeyDeserializer + rawClass == UShort::class.java -> UShortKeyDeserializer + rawClass == UInt::class.java -> UIntKeyDeserializer + rawClass == ULong::class.java -> ULongKeyDeserializer + rawClass.isUnboxableValueClass() -> ValueClassKeyDeserializer.createOrNull(rawClass, cache) + else -> null + } } } diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/ValueClasses.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/ValueClasses.kt index f4f211cd..a7a05e5d 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/ValueClasses.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/ValueClasses.kt @@ -4,12 +4,17 @@ import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.deser.std.StdDeserializer import io.github.projectmapk.jackson.module.kogera.deser.WrapsNullableValueClassDeserializer +import com.fasterxml.jackson.databind.KeyDeserializer as JacksonKeyDeserializer @JvmInline value class Primitive(val v: Int) { class Deserializer : StdDeserializer(Primitive::class.java) { override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Primitive = Primitive(p.intValue + 100) } + + class KeyDeserializer : JacksonKeyDeserializer() { + override fun deserializeKey(key: String, ctxt: DeserializationContext) = Primitive(key.toInt() + 100) + } } @JvmInline @@ -18,6 +23,10 @@ value class NonNullObject(val v: String) { override fun deserialize(p: JsonParser, ctxt: DeserializationContext): NonNullObject = NonNullObject(p.valueAsString + "-deser") } + + class KeyDeserializer : JacksonKeyDeserializer() { + override fun deserializeKey(key: String, ctxt: DeserializationContext) = NonNullObject("$key-deser") + } } @JvmInline @@ -28,4 +37,8 @@ value class NullableObject(val v: String?) { override fun getBoxedNullValue(): NullableObject = NullableObject("null-value-deser") } + + class KeyDeserializer : JacksonKeyDeserializer() { + override fun deserializeKey(key: String, ctxt: DeserializationContext) = NullableObject("$key-deser") + } } 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 new file mode 100644 index 00000000..8ba40956 --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt @@ -0,0 +1,119 @@ +package io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass.mapKey + +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonMappingException +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException +import com.fasterxml.jackson.databind.module.SimpleModule +import io.github.projectmapk.jackson.module.kogera.defaultMapper +import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper +import io.github.projectmapk.jackson.module.kogera.readValue +import io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass.NonNullObject +import io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass.NullableObject +import io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass.Primitive +import org.junit.jupiter.api.Assertions.assertEquals +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 { + companion object { + val throwable = IllegalArgumentException("test") + } + + @Nested + inner class DirectDeserialize { + @Test + fun primitive() { + val result = defaultMapper.readValue>("""{"1":null}""") + assertEquals(mapOf(Primitive(1) to null), result) + } + + @Test + fun nonNullObject() { + val result = defaultMapper.readValue>("""{"foo":null}""") + assertEquals(mapOf(NonNullObject("foo") to null), result) + } + + @Test + fun nullableObject() { + val result = defaultMapper.readValue>("""{"bar":null}""") + assertEquals(mapOf(NullableObject("bar") to null), result) + } + } + + data class Dst( + val p: Map, + val nn: Map, + val n: Map + ) + + @Test + fun wrapped() { + val src = """ + { + "p":{"1":null}, + "nn":{"foo":null}, + "n":{"bar":null} + } + """.trimIndent() + val result = defaultMapper.readValue(src) + val expected = Dst( + mapOf(Primitive(1) to null), + mapOf(NonNullObject("foo") to null), + mapOf(NullableObject("bar") to null) + ) + + assertEquals(expected, result) + } + + @JvmInline + value class HasCheckConstructor(val value: Int) { + init { + if (value < 0) throw throwable + } + } + + @Test + fun callConstructorCheckTest() { + val e = assertThrows { + defaultMapper.readValue>("""{"-1":null}""") + } + assertTrue(e.cause === throwable) + } + + data class Wrapped(val first: String, val second: String) { + class KeyDeserializer : JacksonKeyDeserializer() { + override fun deserializeKey(key: String, ctxt: DeserializationContext) = + key.split("-").let { Wrapped(it[0], it[1]) } + } + } + + @JvmInline + value class Wrapper(val w: Wrapped) + + @Test + fun wrappedCustomObject() { + // If a type that cannot be deserialized is specified, the default is an error. + val thrown = assertThrows { + defaultMapper.readValue>("""{"foo-bar":null}""") + } + assertTrue(thrown.cause is InvalidDefinitionException) + + val mapper = jacksonObjectMapper() + .registerModule( + object : SimpleModule() { + init { + addKeyDeserializer(Wrapped::class.java, Wrapped.KeyDeserializer()) + } + } + ) + + val result = mapper.readValue>("""{"foo-bar":null}""") + val expected = mapOf(Wrapper(Wrapped("foo", "bar")) to null) + + assertEquals(expected, result) + } +} diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/keyDeserializer/SpecifiedForObjectMapperTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/keyDeserializer/SpecifiedForObjectMapperTest.kt new file mode 100644 index 00000000..ab2a28fe --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/keyDeserializer/SpecifiedForObjectMapperTest.kt @@ -0,0 +1,70 @@ +package io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass.mapKey.keyDeserializer + +import com.fasterxml.jackson.databind.module.SimpleModule +import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper +import io.github.projectmapk.jackson.module.kogera.readValue +import io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass.NonNullObject +import io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass.NullableObject +import io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass.Primitive +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.addKeyDeserializer(Primitive::class.java, Primitive.KeyDeserializer()) + this.addKeyDeserializer(NonNullObject::class.java, NonNullObject.KeyDeserializer()) + this.addKeyDeserializer(NullableObject::class.java, NullableObject.KeyDeserializer()) + } + this.registerModule(module) + } + } + + @Nested + inner class DirectDeserialize { + @Test + fun primitive() { + val result = mapper.readValue>("""{"1":null}""") + assertEquals(mapOf(Primitive(101) to null), result) + } + + @Test + fun nonNullObject() { + val result = mapper.readValue>("""{"foo":null}""") + assertEquals(mapOf(NonNullObject("foo-deser") to null), result) + } + + @Test + fun nullableObject() { + val result = mapper.readValue>("""{"bar":null}""") + assertEquals(mapOf(NullableObject("bar-deser") to null), result) + } + } + + data class Dst( + val p: Map, + val nn: Map, + val n: Map + ) + + @Test + fun wrapped() { + val src = """ + { + "p":{"1":null}, + "nn":{"foo":null}, + "n":{"bar":null} + } + """.trimIndent() + val result = mapper.readValue(src) + val expected = Dst( + mapOf(Primitive(101) to null), + mapOf(NonNullObject("foo-deser") to null), + mapOf(NullableObject("bar-deser") to null) + ) + + assertEquals(expected, result) + } +} diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/keyDeserializer/byAnnotation/SpecifiedForClassTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/keyDeserializer/byAnnotation/SpecifiedForClassTest.kt new file mode 100644 index 00000000..7ebe210a --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/keyDeserializer/byAnnotation/SpecifiedForClassTest.kt @@ -0,0 +1,37 @@ +package io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass.mapKey.keyDeserializer.byAnnotation + +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import io.github.projectmapk.jackson.module.kogera.defaultMapper +import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper +import io.github.projectmapk.jackson.module.kogera.readValue +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import com.fasterxml.jackson.databind.KeyDeserializer as JacksonKeyDeserializer + +class SpecifiedForClassTest { + @JsonDeserialize(keyUsing = Value.KeyDeserializer::class) + @JvmInline + value class Value(val v: Int) { + class KeyDeserializer : JacksonKeyDeserializer() { + override fun deserializeKey(key: String, ctxt: DeserializationContext) = Value(key.toInt() + 100) + } + } + + @Test + fun directDeserTest() { + val result = defaultMapper.readValue>("""{"1":null}""") + + assertEquals(mapOf(Value(101) to null), result) + } + + data class Wrapper(val v: Map) + + @Test + fun paramDeserTest() { + val mapper = jacksonObjectMapper() + val result = mapper.readValue("""{"v":{"1":null}}""") + + assertEquals(Wrapper(mapOf(Value(101) to null)), result) + } +} diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/keyDeserializer/byAnnotation/SpecifiedForPropertyTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/keyDeserializer/byAnnotation/SpecifiedForPropertyTest.kt new file mode 100644 index 00000000..1435890f --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/keyDeserializer/byAnnotation/SpecifiedForPropertyTest.kt @@ -0,0 +1,28 @@ +package io.github.projectmapk.jackson.module.kogera.zIntegration.deser.valueClass.mapKey.keyDeserializer.byAnnotation + +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper +import io.github.projectmapk.jackson.module.kogera.readValue +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import com.fasterxml.jackson.databind.KeyDeserializer as JacksonKeyDeserializer + +class SpecifiedForPropertyTest { + @JvmInline + value class Value(val v: Int) { + class KeyDeserializer : JacksonKeyDeserializer() { + override fun deserializeKey(key: String, ctxt: DeserializationContext) = Value(key.toInt() + 100) + } + } + + data class Wrapper(@JsonDeserialize(keyUsing = Value.KeyDeserializer::class) val v: Map) + + @Test + fun paramDeserTest() { + val mapper = jacksonObjectMapper() + val result = mapper.readValue("""{"v":{"1":null}}""") + + assertEquals(Wrapper(mapOf(Value(101) to null)), result) + } +} diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub314.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub314.kt new file mode 100644 index 00000000..0ec29be1 --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub314.kt @@ -0,0 +1,29 @@ +package io.github.projectmapk.jackson.module.kogera.zPorted.test.github + +import com.fasterxml.jackson.databind.MapperFeature +import io.github.projectmapk.jackson.module.kogera.jsonMapper +import io.github.projectmapk.jackson.module.kogera.kotlinModule +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class GitHub314 { + // Since Nothing? is compiled as a Void, it can be serialized by specifying ALLOW_VOID_VALUED_PROPERTIES + data object NothingData { + val data: Nothing? = null + } + + @Test + fun test() { + val expected = """{"data":null}""" + + val withoutKotlinModule = jsonMapper { enable(MapperFeature.ALLOW_VOID_VALUED_PROPERTIES) } + assertEquals(expected, withoutKotlinModule.writeValueAsString(NothingData)) + + val withKotlinModule = jsonMapper { + enable(MapperFeature.ALLOW_VOID_VALUED_PROPERTIES) + addModule(kotlinModule()) + } + + assertEquals(expected, withKotlinModule.writeValueAsString(NothingData)) + } +} diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub618.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub618.kt new file mode 100644 index 00000000..5aad570a --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub618.kt @@ -0,0 +1,30 @@ +package io.github.projectmapk.jackson.module.kogera.zPorted.test.github + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class GitHub618 { + @JsonSerialize(using = V.Serializer::class) + @JvmInline + value class V(val value: String) { + class Serializer : StdSerializer(V::class.java) { + override fun serialize(p0: V, p1: JsonGenerator, p2: SerializerProvider) { + p1.writeString(p0.toString()) + } + } + } + + data class D(val v: V?) + + @Test + fun test() { + val mapper = jacksonObjectMapper() + // expected: {"v":null}, but NullPointerException thrown + assertEquals("""{"v":null}""", mapper.writeValueAsString(D(null))) + } +} diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub625.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub625.kt new file mode 100644 index 00000000..18438770 --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub625.kt @@ -0,0 +1,55 @@ +package io.github.projectmapk.jackson.module.kogera.zPorted.test.github + +import com.fasterxml.jackson.annotation.JsonInclude +import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Test + +class GitHub625 { + @JvmInline + value class Primitive(val v: Int) + + @JvmInline + value class NonNullObject(val v: String) + + @JvmInline + value class NullableObject(val v: String?) + + @JsonInclude(value = JsonInclude.Include.NON_NULL, content = JsonInclude.Include.NON_NULL) + data class Dto( + val primitive: Primitive? = null, + val nonNullObject: NonNullObject? = null, + val nullableObject: NullableObject? = null + ) { + fun getPrimitiveGetter(): Primitive? = null + fun getNonNullObjectGetter(): NonNullObject? = null + fun getNullableObjectGetter(): NullableObject? = null + } + + @Test + fun test() { + val mapper = jacksonObjectMapper() + val dto = Dto() + assertEquals("{}", mapper.writeValueAsString(dto)) + } + + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_NULL) + data class FailingDto( + val nullableObject1: NullableObject = NullableObject(null), + val nullableObject2: NullableObject? = NullableObject(null), + val map: Map = mapOf("nullableObject" to NullableObject(null),) + ) { + fun getNullableObjectGetter1(): NullableObject = NullableObject(null) + fun getNullableObjectGetter2(): NullableObject? = NullableObject(null) + fun getMapGetter(): Map = mapOf("nullableObject" to NullableObject(null)) + } + + @Test + fun failing() { + val writer = jacksonObjectMapper() + val json = writer.writeValueAsString(FailingDto()) + + assertNotEquals("{}", json) + } +} diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub873.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub873.kt new file mode 100644 index 00000000..69d2b238 --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub873.kt @@ -0,0 +1,45 @@ +package io.github.projectmapk.jackson.module.kogera.zPorted.test.github + +import io.github.projectmapk.jackson.module.kogera.readValue +import io.github.projectmapk.jackson.module.kogera.defaultMapper +import org.junit.jupiter.api.Test + +class GitHub873 { + @Test + fun `should serialize value class`() { + + val person = Person( + mapOf( + "id" to "123", + "updated" to "2023-11-22 12:11:23", + "login" to "2024-01-15", + ), + ) + + val serialized = defaultMapper.writeValueAsString( + TimestampedPerson( + 123L, + Person(person.properties), + ) + ) + + val deserialized = defaultMapper.readValue(serialized) + + assert( + deserialized == TimestampedPerson( + 123L, + Person(person.properties), + ) + ) + } + + @JvmInline + value class Person( + val properties: Map, + ) + + data class TimestampedPerson( + val timestamp: Long, + val person: Person, + ) +} diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github464.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github464.kt index 89fa4474..cb2870dd 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github464.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github464.kt @@ -1,5 +1,6 @@ package io.github.projectmapk.jackson.module.kogera.zPorted.test.github +import com.fasterxml.jackson.annotation.JsonPropertyOrder import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.JsonSerializer import com.fasterxml.jackson.databind.ObjectMapper @@ -42,6 +43,20 @@ class Github464 { // val xyzzy: T get() = quux } + @JsonPropertyOrder( + "foo", + "bar", + "baz", + "qux", + "quux", + "corge", + "grault", + "garply", + "waldo", + "fred", + "plugh", + // "xyzzy" + ) class Poko( val foo: ValueClass, val bar: ValueClass?, @@ -150,20 +165,22 @@ class Github464 { } } - class SerializerPriorityTest { - @JvmInline - value class ValueBySerializer(val value: Int) + @JvmInline + value class ValueBySerializer(val value: Int) - object Serializer : StdSerializer(ValueBySerializer::class.java) { - override fun serialize(value: ValueBySerializer, gen: JsonGenerator, provider: SerializerProvider) { - gen.writeString(value.value.toString()) - } + object Serializer : StdSerializer(ValueBySerializer::class.java) { + override fun serialize(value: ValueBySerializer, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeString(value.value.toString()) } - object KeySerializer : StdSerializer(ValueBySerializer::class.java) { - override fun serialize(value: ValueBySerializer, gen: JsonGenerator, provider: SerializerProvider) { - gen.writeFieldName(value.value.toString()) - } + } + object KeySerializer : StdSerializer(ValueBySerializer::class.java) { + override fun serialize(value: ValueBySerializer, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeFieldName(value.value.toString()) } + } + + @Nested + inner class SerializerPriorityTest { private val target = mapOf(ValueBySerializer(1) to ValueBySerializer(2)) private val sm = SimpleModule() @@ -172,8 +189,7 @@ class Github464 { @Test fun simpleTest() { - val om: ObjectMapper = jacksonMapperBuilder() - .addModule(sm).build() + val om: ObjectMapper = jacksonMapperBuilder().addModule(sm).build() assertEquals("""{"1":"2"}""", om.writeValueAsString(target)) } diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github536.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github536.kt new file mode 100644 index 00000000..ee6bc6b7 --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github536.kt @@ -0,0 +1,54 @@ +package io.github.projectmapk.jackson.module.kogera.zPorted.test.github + +import com.fasterxml.jackson.annotation.JsonKey +import io.github.projectmapk.jackson.module.kogera.jacksonMapperBuilder +import io.github.projectmapk.jackson.module.kogera.testPrettyWriter +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class Github536 { + @JvmInline + value class JsonKeyGetter(val value: Int) { + @get:JsonKey + val jsonKey: String + get() = this.toString() + } + + interface IJsonKeyGetter { + @get:JsonKey + val jsonKey: String + get() = this.toString() + } + + @JvmInline + value class JsonKeyGetterImplementation(val value: Int) : IJsonKeyGetter + + @JvmInline + value class JsonKeyGetterImplementationDisabled(val value: Int) : IJsonKeyGetter { + @get:JsonKey(false) + override val jsonKey: String + get() = super.jsonKey + } + + private val writer = jacksonMapperBuilder().build().testPrettyWriter() + + @Test + fun test() { + val src = mapOf( + JsonKeyGetter(0) to 0, + JsonKeyGetterImplementation(1) to 1, + JsonKeyGetterImplementationDisabled(2) to 2 + ) + + assertEquals( + """ + { + "JsonKeyGetter(value=0)" : 0, + "JsonKeyGetterImplementation(value=1)" : 1, + "2" : 2 + } + """.trimIndent(), + writer.writeValueAsString(src) + ) + } +} diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github630.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github630.kt new file mode 100644 index 00000000..9ae99b60 --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/Github630.kt @@ -0,0 +1,37 @@ +package io.github.projectmapk.jackson.module.kogera.zPorted.test.github + +import com.fasterxml.jackson.annotation.JsonProperty +import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class Github630 { + private val mapper = jacksonObjectMapper() + + data class Dto( + // from #570, #603 + val FOO: Int = 0, + val bAr: Int = 0, + @JsonProperty("b") + val BAZ: Int = 0, + @JsonProperty("q") + val qUx: Int = 0, + // from #71 + internal val quux: Int = 0, + // from #434 + val `corge-corge`: Int = 0, + // additional + @get:JvmName("aaa") + val grault: Int = 0 + ) + + @Test + fun test() { + val dto = Dto() + + assertEquals( + """{"FOO":0,"bAr":0,"b":0,"q":0,"quux":0,"corge-corge":0,"grault":0}""", + mapper.writeValueAsString(dto) + ) + } +}