Skip to content

Commit

Permalink
Merge pull request #50 from ProjectMapK/value-param-annotations
Browse files Browse the repository at this point in the history
Partial fix for #46
  • Loading branch information
k163377 authored Jan 22, 2023
2 parents bc386f9 + 7676043 commit 6bac5b9
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.fasterxml.jackson.module.kotlin

import kotlinx.metadata.Flag
import kotlinx.metadata.KmClass
import kotlinx.metadata.KmClassifier
import kotlinx.metadata.KmConstructor
import kotlinx.metadata.KmProperty
import kotlinx.metadata.KmType
Expand Down Expand Up @@ -67,6 +68,9 @@ internal val defaultConstructorMarker: Class<*> by lazy {
// but are ignored because they do not result in errors in internal use cases.
internal fun String.reconstructClass(): Class<*> = Class.forName(this.replace(".", "$").replace("/", "."))

internal fun KmType.reconstructClassOrNull(): Class<*>? = (classifier as? KmClassifier.Class)
?.let { kotlin.runCatching { it.name.reconstructClass() }.getOrNull() }

internal fun KmClass.findKmConstructor(constructor: Constructor<*>): KmConstructor? {
val descHead = constructor.parameterTypes.toDescString()
val desc = descHead + "V"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.fasterxml.jackson.module.kotlin

import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.SerializationConfig
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor
import com.fasterxml.jackson.databind.introspect.BasicBeanDescription
import com.fasterxml.jackson.databind.introspect.BasicClassIntrospector
import com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector

// If the constructor has value class parameter,
// annotations given to synthetic constructor parameter is injected into that constructor parameter.
private fun AnnotatedConstructor.injectSyntheticAnnotations() {
val constructor = annotated

@Suppress("UNCHECKED_CAST")
val syntheticParams = constructor.parameterTypes.copyOf(constructor.parameterCount + 1)
.apply { this[constructor.parameterCount] = defaultConstructorMarker } as Array<Class<*>>
// Try to get syntheticConstructor, and if not, do nothing.
val syntheticConstructor = runCatching { declaringClass.getDeclaredConstructor(*syntheticParams) }
.getOrNull()
?: return

(0 until constructor.parameterCount).forEach { i ->
val map = getParameterAnnotations(i)
syntheticConstructor.parameterAnnotations[i].forEach { map.add(it) }
}
}

internal class KotlinBeanDescription(coll: POJOPropertiesCollector) : BasicBeanDescription(coll) {
init {
this.constructors?.forEach { it.injectSyntheticAnnotations() }
}
}

// Almost all copies of super @2.14.1
internal object KotlinClassIntrospector : BasicClassIntrospector() {
override fun forSerialization(
config: SerializationConfig,
type: JavaType,
r: MixInResolver
): BasicBeanDescription {
// minor optimization: for some JDK types do minimal introspection
return _findStdTypeDesc(config, type)
// As per [databind#550], skip full introspection for some of standard
// structured types as well
?: _findStdJdkCollectionDesc(config, type)
?: run {
val coll = collectProperties(config, type, r, true)

if (type.rawClass.annotations.any { it is Metadata }) {
KotlinBeanDescription(coll)
} else {
BasicBeanDescription.forDeserialization(coll)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ public class KotlinModule private constructor(
)
context.appendAnnotationIntrospector(KotlinNamesAnnotationIntrospector(this, strictNullChecks, cache))

context.setClassIntrospector(KotlinClassIntrospector)

context.addDeserializers(KotlinDeserializers())
context.addKeyDeserializers(KotlinKeyDeserializers)
context.addSerializers(KotlinSerializers())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,11 @@ internal class KotlinNamesAnnotationIntrospector(
}
val kotlinProperty = cache.getKmClass(getter.declaringClass)?.findPropertyByGetter(getter)

return (kotlinProperty?.returnType?.classifier as? KmClassifier.Class)?.let { classifier ->
// Since there was no way to directly determine whether returnType is a value class or not,
// Class is restored and processed.
// If the cost of this process is significant, consider caching it.
runCatching { classifier.name.reconstructClass() }
.getOrNull()
?.takeIf { takePredicate(it) }
// Since there was no way to directly determine whether returnType is a value class or not,
// Class is restored and processed.
// If the cost of this process is significant, consider caching it.
return kotlinProperty?.returnType?.reconstructClassOrNull()?.let { clazz ->
clazz.takeIf(takePredicate)
?.let { ValueClassBoxConverter(getter.returnType, it) }
}
}
Expand All @@ -163,12 +161,9 @@ internal class KotlinNamesAnnotationIntrospector(
}

private fun ValueParameter.createValueClassUnboxConverterOrNull(rawType: Class<*>): ValueClassUnboxConverter<*>? {
return (this.type.classifier as? KmClassifier.Class)?.let { classifier ->
runCatching { classifier.name.reconstructClass() }
.getOrNull()
?.takeIf { it.isUnboxableValueClass() && it != rawType }
?.let { ValueClassUnboxConverter(it) }
}
return type.reconstructClassOrNull()
?.takeIf { it.isUnboxableValueClass() && it != rawType }
?.let { ValueClassUnboxConverter(it) }
}

// If the collection type argument cannot be obtained, treat it as nullable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import com.fasterxml.jackson.module.kotlin._integration.ser.value_class.serializ
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.testPrettyWriter
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Test

class ByAnnotationTest {
Expand All @@ -14,6 +13,8 @@ class ByAnnotationTest {
}

data class NonNullSrc(
@JsonSerialize(using = Primitive.Serializer::class)
val paramAnn: Primitive,
@get:JsonSerialize(using = Primitive.Serializer::class)
val getterAnn: Primitive,
@field:JsonSerialize(using = Primitive.Serializer::class)
Expand All @@ -22,20 +23,23 @@ class ByAnnotationTest {

@Test
fun nonNull() {
val src = NonNullSrc(Primitive(0), Primitive(1))
val src = NonNullSrc(Primitive(0), Primitive(1), Primitive(2))

assertEquals(
"""
{
"getterAnn" : 100,
"fieldAnn" : 101
"paramAnn" : 100,
"getterAnn" : 101,
"fieldAnn" : 102
}
""".trimIndent(),
writer.writeValueAsString(src)
)
}

data class NullableSrc(
@JsonSerialize(using = Primitive.Serializer::class)
val paramAnn: Primitive?,
@get:JsonSerialize(using = Primitive.Serializer::class)
val getterAnn: Primitive?,
@field:JsonSerialize(using = Primitive.Serializer::class)
Expand All @@ -44,13 +48,14 @@ class ByAnnotationTest {

@Test
fun nullableWithoutNull() {
val src = NullableSrc(Primitive(0), Primitive(1))
val src = NullableSrc(Primitive(0), Primitive(1), Primitive(2))

assertEquals(
"""
{
"getterAnn" : 100,
"fieldAnn" : 101
"paramAnn" : 100,
"getterAnn" : 101,
"fieldAnn" : 102
}
""".trimIndent(),
writer.writeValueAsString(src)
Expand All @@ -59,39 +64,17 @@ class ByAnnotationTest {

@Test
fun nullableWithNull() {
val src = NullableSrc(null, null)
val src = NullableSrc(null, null, null)

assertEquals(
"""
{
"paramAnn" : null,
"getterAnn" : null,
"fieldAnn" : null
}
""".trimIndent(),
writer.writeValueAsString(src)
)
}

data class Failing(
@JsonSerialize(using = Primitive.Serializer::class)
val nonNull: Primitive,
@JsonSerialize(using = Primitive.Serializer::class)
val nullable: Primitive?
)

// #46
@Test
fun failing() {
val src = Failing(Primitive(0), Primitive(1))

assertNotEquals(
"""
{
"nonNull" : 100,
"nullable" : 101
}
""".trimIndent(),
writer.writeValueAsString(src)
)
}
}

0 comments on commit 6bac5b9

Please sign in to comment.