Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
235f6de
Update kogera version
k163377 Jan 25, 2025
346af9f
Merge pull request #268 from ProjectMapK/k163377-patch-2
k163377 Jan 25, 2025
e282fbf
Create FUNDING.yml
k163377 Jan 25, 2025
19d5d3d
Merge pull request #269 from ProjectMapK/k163377-patch-2
k163377 Jan 25, 2025
c6549fe
Porting of tests that were missing
k163377 Feb 8, 2025
87f36d3
Fixing flaky test
k163377 Feb 8, 2025
d2af3bf
Add test case to serialize Nothing?
k163377 Feb 8, 2025
c055a07
Merge pull request #271 from ProjectMapK/porting-tests
k163377 Feb 8, 2025
3f5a180
Fix to not call Lint twice
k163377 Feb 14, 2025
f468cce
Merge pull request #272 from ProjectMapK/k163377-patch-2
k163377 Feb 14, 2025
baabe3b
Add GitHub Actions to dependabot's management target
k163377 Feb 14, 2025
78141cd
Merge pull request #273 from ProjectMapK/k163377-patch-2
k163377 Feb 14, 2025
3d5e441
Bump the dependencies group with 4 updates
dependabot[bot] Feb 28, 2025
68af6c6
Downgrade JUnit5
k163377 Mar 1, 2025
09d41b3
Merge pull request #274 from ProjectMapK/dependabot/gradle/dependenci…
k163377 Mar 1, 2025
8427147
Add action-junit-report
k163377 Mar 1, 2025
f32479c
Merge pull request #275 from ProjectMapK/report
k163377 Mar 1, 2025
8f38c3f
Update jackson version
k163377 Mar 2, 2025
50eabca
Merge pull request #276 from ProjectMapK/k163377-patch-2
k163377 Mar 2, 2025
fd61ca8
Added serialization test for value class that wraps Map
k163377 Mar 2, 2025
856ac59
Merge pull request #277 from ProjectMapK/kotlin873
k163377 Mar 2, 2025
333c334
Add KeyDeserializers
k163377 Mar 20, 2024
2c41387
Added a test case that uses a custom KeyDeserializer
k163377 Mar 20, 2024
d82a607
Add KeyDeserializer for value class
k163377 Mar 20, 2024
8532e54
Add tests
k163377 Mar 20, 2024
9552e73
Formatting
k163377 Mar 2, 2025
bb2f0cc
Merge pull request #278 from ProjectMapK/porting-910
k163377 Mar 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# These are supported funding model platforms

github: [k163377]
13 changes: 10 additions & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/lint-and-test-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
10 changes: 10 additions & 0 deletions .github/workflows/test-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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'
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public class KotlinModule private constructor(
_deserializers = KotlinDeserializers(cache, useJavaDurationConversion)

_keySerializers = KotlinKeySerializers(cache)
_keyDeserializers = KotlinKeyDeserializers
_keyDeserializers = KotlinKeyDeserializers(cache)

_abstractTypes = ClosedRangeResolver

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<S, D : Any>(
private val creator: Method,
private val converter: ValueClassBoxConverter<S, D>
) : 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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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>(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
Expand All @@ -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
Expand All @@ -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")
}
}
Original file line number Diff line number Diff line change
@@ -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<Map<Primitive, String?>>("""{"1":null}""")
assertEquals(mapOf(Primitive(1) to null), result)
}

@Test
fun nonNullObject() {
val result = defaultMapper.readValue<Map<NonNullObject, String?>>("""{"foo":null}""")
assertEquals(mapOf(NonNullObject("foo") to null), result)
}

@Test
fun nullableObject() {
val result = defaultMapper.readValue<Map<NullableObject, String?>>("""{"bar":null}""")
assertEquals(mapOf(NullableObject("bar") to null), result)
}
}

data class Dst(
val p: Map<Primitive, String?>,
val nn: Map<NonNullObject, String?>,
val n: Map<NullableObject, String?>
)

@Test
fun wrapped() {
val src = """
{
"p":{"1":null},
"nn":{"foo":null},
"n":{"bar":null}
}
""".trimIndent()
val result = defaultMapper.readValue<Dst>(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<InvocationTargetException> {
defaultMapper.readValue<Map<HasCheckConstructor, String?>>("""{"-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<JsonMappingException> {
defaultMapper.readValue<Map<Wrapper, String?>>("""{"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<Map<Wrapper, String?>>("""{"foo-bar":null}""")
val expected = mapOf(Wrapper(Wrapped("foo", "bar")) to null)

assertEquals(expected, result)
}
}
Loading
Loading