Skip to content

Commit

Permalink
Merge branch '2.13' of github.com:FasterXML/jackson-module-kotlin int…
Browse files Browse the repository at this point in the history
…o 2.13
  • Loading branch information
cowtowncoder committed Mar 6, 2022
2 parents 71cfa26 + 99389f3 commit 601bc71
Show file tree
Hide file tree
Showing 12 changed files with 757 additions and 156 deletions.
1 change: 1 addition & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ wrongwrong (k163377@github)
* #456: Refactor KNAI.findImplicitPropertyName()
* #449: Refactor AnnotatedMethod.hasRequiredMarker()
* #521: Fixed lookup of instantiators
* #527: Improvements to serialization of `value class`.

Dmitri Domanine (novtor@github)
* Contributed fix for #490: Missing value of type JsonNode? is deserialized as NullNode instead of null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.fasterxml.jackson.module.kotlin

import kotlin.reflect.KFunction
import kotlin.reflect.jvm.isAccessible

internal class ConstructorValueCreator<T>(override val callable: KFunction<T>) : ValueCreator<T>() {
override val accessible: Boolean = callable.isAccessible

init {
// To prevent the call from failing, save the initial value and then rewrite the flag.
if (!accessible) callable.isAccessible = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ import com.fasterxml.jackson.databind.Module
import com.fasterxml.jackson.databind.cfg.MapperConfig
import com.fasterxml.jackson.databind.introspect.*
import com.fasterxml.jackson.databind.jsontype.NamedType
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import java.lang.reflect.AccessibleObject
import java.lang.reflect.Constructor
import java.lang.reflect.Field
import java.lang.reflect.Method
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty1
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.*


Expand Down Expand Up @@ -59,6 +62,45 @@ internal class KotlinAnnotationIntrospector(private val context: Module.SetupCon
return super.findCreatorAnnotation(config, a)
}

// Find a serializer to handle the case where the getter returns an unboxed value from the value class.
override fun findSerializer(am: Annotated): StdSerializer<*>? = when (am) {
is AnnotatedMethod -> {
val getter = am.member.apply {
// If the return value of the getter is a value class,
// it will be serialized properly without doing anything.
if (this.returnType.isUnboxableValueClass()) return null
}

val kotlinProperty = getter
.declaringClass
.kotlin
.let {
// KotlinReflectionInternalError is raised in GitHub167 test,
// but it looks like an edge case, so it is ignored.
try {
it.memberProperties
} catch (e: Error) {
null
}
}?.find { it.javaGetter == getter }

(kotlinProperty?.returnType?.classifier as? KClass<*>)
?.takeIf { it.isValue }
?.java
?.let { outerClazz ->
val innerClazz = getter.returnType

ValueClassStaticJsonValueSerializer.createdOrNull(outerClazz, innerClazz)
?: @Suppress("UNCHECKED_CAST") ValueClassBoxSerializer(outerClazz, innerClazz)
}
}
// Ignore the case of AnnotatedField, because JvmField cannot be set in the field of value class.
else -> null
}

// Perform proper serialization even if the value wrapped by the value class is null.
override fun findNullSerializer(am: Annotated) = findSerializer(am)

/**
* Subclasses can be detected automatically for sealed classes, since all possible subclasses are known
* at compile-time to Kotlin. This makes [com.fasterxml.jackson.annotation.JsonSubTypes] redundant.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector
import com.fasterxml.jackson.databind.util.BeanUtil
import java.lang.reflect.Constructor
import java.lang.reflect.Method
import java.util.*
import java.util.Locale
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.companionObject
import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.internal.KotlinReflectionInternalError
Expand Down Expand Up @@ -59,68 +60,44 @@ internal class KotlinNamesAnnotationIntrospector(val module: KotlinModule, val c
}

@Suppress("UNCHECKED_CAST")
override fun hasCreatorAnnotation(member: Annotated): Boolean {
private fun hasCreatorAnnotation(member: AnnotatedConstructor): Boolean {
// don't add a JsonCreator to any constructor if one is declared already

if (member is AnnotatedConstructor && !member.declaringClass.isEnum) {
// if has parameters, is a Kotlin class, and the parameters all have parameter annotations, then pretend we have a JsonCreator
if (member.parameterCount > 0 && member.declaringClass.isKotlinClass()) {
return cache.checkConstructorIsCreatorAnnotated(member) {
val kClass = cache.kotlinFromJava(member.declaringClass as Class<Any>)
val kConstructor = cache.kotlinFromJava(member.annotated as Constructor<Any>)
val kClass = cache.kotlinFromJava(member.declaringClass as Class<Any>)
.apply { if (this in ignoredClassesForImplyingJsonCreator) return false }
val kConstructor = cache.kotlinFromJava(member.annotated as Constructor<Any>) ?: return false

if (kConstructor != null) {
val isPrimaryConstructor = kClass.primaryConstructor == kConstructor ||
(kClass.primaryConstructor == null && kClass.constructors.size == 1)
// TODO: should we do this check or not? It could cause failures if we miss another way a property could be set
// val requiredProperties = kClass.declaredMemberProperties.filter {!it.returnType.isMarkedNullable }.map { it.name }.toSet()
// val areAllRequiredParametersInConstructor = kConstructor.parameters.all { requiredProperties.contains(it.name) }

val propertyNames = kClass.memberProperties.map { it.name }.toSet()
val propertyNames = kClass.memberProperties.map { it.name }.toSet()

fun KFunction<*>.isPossibleSingleString(): Boolean {
val result = parameters.size == 1 &&
parameters[0].name !in propertyNames &&
parameters[0].type.javaType == String::class.java &&
parameters[0].annotations.none { it.annotationClass.java == JsonProperty::class.java }
return result
}
return when {
kConstructor.isPossibleSingleString(propertyNames) -> false
kConstructor.parameters.any { it.name == null } -> false
!kClass.isPrimaryConstructor(kConstructor) -> false
else -> {
val anyConstructorHasJsonCreator = kClass.constructors
.filterOutSingleStringCallables(propertyNames)
.any { it.hasAnnotation<JsonCreator>() }

fun Collection<KFunction<*>>.filterOutSingleStringCallables(): Collection<KFunction<*>> {
return this.filter { !it.isPossibleSingleString() }
}
val anyCompanionMethodIsJsonCreator = member.type.rawClass.kotlin.companionObject?.declaredFunctions
?.filterOutSingleStringCallables(propertyNames)
?.any { it.hasAnnotation<JsonCreator>() && it.hasAnnotation<JvmStatic>() }
?: false

val anyConstructorHasJsonCreator = kClass.constructors.filterOutSingleStringCallables()
.any { it.annotations.any { it.annotationClass.java == JsonCreator::class.java }
}

val anyCompanionMethodIsJsonCreator = member.type.rawClass.kotlin.companionObject?.declaredFunctions
?.filterOutSingleStringCallables()?.any {
it.annotations.any { it.annotationClass.java == JvmStatic::class.java } &&
it.annotations.any { it.annotationClass.java == JsonCreator::class.java }
} ?: false

// TODO: should we do this check or not? It could cause failures if we miss another way a property could be set
// val requiredProperties = kClass.declaredMemberProperties.filter {!it.returnType.isMarkedNullable }.map { it.name }.toSet()
// val areAllRequiredParametersInConstructor = kConstructor.parameters.all { requiredProperties.contains(it.name) }

val areAllParametersValid = kConstructor.parameters.size == kConstructor.parameters.count { it.name != null }

val isSingleStringConstructor = kConstructor.isPossibleSingleString()

val implyCreatorAnnotation = isPrimaryConstructor
&& !(anyConstructorHasJsonCreator || anyCompanionMethodIsJsonCreator)
&& areAllParametersValid
&& !isSingleStringConstructor
&& kClass !in ignoredClassesForImplyingJsonCreator

implyCreatorAnnotation
} else {
false
}
}
!(anyConstructorHasJsonCreator || anyCompanionMethodIsJsonCreator)
}
}
return false
}

override fun hasCreatorAnnotation(member: Annotated): Boolean =
if (member is AnnotatedConstructor && member.isKotlinConstructorWithParameters())
cache.checkConstructorIsCreatorAnnotated(member) { hasCreatorAnnotation(it) }
else
false

@Suppress("UNCHECKED_CAST")
private fun findKotlinParameterName(param: AnnotatedParameter): String? {
return if (param.declaringClass.isKotlinClass()) {
Expand Down Expand Up @@ -165,3 +142,19 @@ internal class KotlinNamesAnnotationIntrospector(val module: KotlinModule, val c
ReplaceWith("with(receiver) { declaringClass.declaredMethods.any { it.name.contains('-') } }")
)
private fun AnnotatedMethod.isInlineClass() = declaringClass.declaredMethods.any { it.name.contains('-') }

// if has parameters, is a Kotlin class, and the parameters all have parameter annotations, then pretend we have a JsonCreator
private fun AnnotatedConstructor.isKotlinConstructorWithParameters(): Boolean =
parameterCount > 0 && declaringClass.isKotlinClass() && !declaringClass.isEnum

private fun KFunction<*>.isPossibleSingleString(propertyNames: Set<String>): Boolean = parameters.size == 1 &&
parameters[0].name !in propertyNames &&
parameters[0].type.javaType == String::class.java &&
!parameters[0].hasAnnotation<JsonProperty>()

private fun Collection<KFunction<*>>.filterOutSingleStringCallables(propertyNames: Set<String>): Collection<KFunction<*>> =
this.filter { !it.isPossibleSingleString(propertyNames) }

private fun KClass<*>.isPrimaryConstructor(kConstructor: KFunction<*>) = this.primaryConstructor.let {
it == kConstructor || (it == null && this.constructors.size == 1)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.fasterxml.jackson.module.kotlin

import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.BeanDescription
import com.fasterxml.jackson.databind.JavaType
Expand All @@ -8,6 +9,8 @@ import com.fasterxml.jackson.databind.SerializationConfig
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.Serializers
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.math.BigInteger

object SequenceSerializer : StdSerializer<Sequence<*>>(Sequence::class.java) {
Expand Down Expand Up @@ -42,16 +45,47 @@ object ULongSerializer : StdSerializer<ULong>(ULong::class.java) {
}
}

object ValueClassUnboxSerializer : StdSerializer<Any>(Any::class.java) {
override fun serialize(value: Any, gen: JsonGenerator, provider: SerializerProvider) {
val unboxed = value::class.java.getMethod("unbox-impl").invoke(value)
// Class must be UnboxableValueClass.
private fun Class<*>.getStaticJsonValueGetter(): Method? = this.declaredMethods
.find { method -> Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonValue } }

if (unboxed == null) {
provider.findNullValueSerializer(null).serialize(unboxed, gen, provider)
return
internal sealed class ValueClassSerializer<T : Any>(t: Class<T>) : StdSerializer<T>(t) {
object Unbox : ValueClassSerializer<Any>(Any::class.java) {
override fun serialize(value: Any, gen: JsonGenerator, provider: SerializerProvider) {
val unboxed = value::class.java.getMethod("unbox-impl").invoke(value)

if (unboxed == null) {
provider.findNullValueSerializer(null).serialize(unboxed, gen, provider)
return
}

provider.findValueSerializer(unboxed::class.java).serialize(unboxed, gen, provider)
}
}

class StaticJsonValue<T : Any>(
t: Class<T>, private val staticJsonValueGetter: Method
) : ValueClassSerializer<T>(t) {
private val unboxMethod: Method = t.getMethod("unbox-impl")

override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) {
val unboxed = unboxMethod.invoke(value)
// As shown in the processing of the factory function, jsonValueGetter is always a static method.
val jsonValue: Any? = staticJsonValueGetter.invoke(null, unboxed)
jsonValue
?.let { provider.findValueSerializer(it::class.java).serialize(it, gen, provider) }
?: provider.findNullValueSerializer(null).serialize(null, gen, provider)
}
}

provider.findValueSerializer(unboxed::class.java).serialize(unboxed, gen, provider)
companion object {
// `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 from(t: Class<*>): ValueClassSerializer<*> = t.getStaticJsonValueGetter()
?.let { StaticJsonValue(t, it) }
?: Unbox
}
}

Expand All @@ -60,14 +94,59 @@ internal class KotlinSerializers : Serializers.Base() {
config: SerializationConfig?,
type: JavaType,
beanDesc: BeanDescription?
): JsonSerializer<*>? = when {
Sequence::class.java.isAssignableFrom(type.rawClass) -> SequenceSerializer
UByte::class.java.isAssignableFrom(type.rawClass) -> UByteSerializer
UShort::class.java.isAssignableFrom(type.rawClass) -> UShortSerializer
UInt::class.java.isAssignableFrom(type.rawClass) -> UIntSerializer
ULong::class.java.isAssignableFrom(type.rawClass) -> ULongSerializer
// The priority of Unboxing needs to be lowered so as not to break the serialization of Unsigned Integers.
type.rawClass.isUnboxableValueClass() -> ValueClassUnboxSerializer
else -> null
): JsonSerializer<*>? {
val rawClass = type.rawClass

return when {
Sequence::class.java.isAssignableFrom(rawClass) -> SequenceSerializer
UByte::class.java.isAssignableFrom(rawClass) -> UByteSerializer
UShort::class.java.isAssignableFrom(rawClass) -> UShortSerializer
UInt::class.java.isAssignableFrom(rawClass) -> UIntSerializer
ULong::class.java.isAssignableFrom(rawClass) -> ULongSerializer
// The priority of Unboxing needs to be lowered so as not to break the serialization of Unsigned Integers.
rawClass.isUnboxableValueClass() -> ValueClassSerializer.from(rawClass)
else -> null
}
}
}

// This serializer is used to properly serialize the value class.
// The getter generated for the value class is special,
// so this class will not work properly when added to the Serializers
// (it is configured from KotlinAnnotationIntrospector.findSerializer).
internal class ValueClassBoxSerializer<T : Any>(
private val outerClazz: Class<out Any>, innerClazz: Class<T>
) : StdSerializer<T>(innerClazz) {
private val boxMethod = outerClazz.getMethod("box-impl", innerClazz)

override fun serialize(value: T?, gen: JsonGenerator, provider: SerializerProvider) {
// Values retrieved from getter are considered validated and constructor-impl is not executed.
val boxed = boxMethod.invoke(null, value)

provider.findValueSerializer(outerClazz).serialize(boxed, gen, provider)
}
}

internal class ValueClassStaticJsonValueSerializer<T> private constructor(
innerClazz: Class<T>,
private val staticJsonValueGetter: Method
) : StdSerializer<T>(innerClazz) {
override fun serialize(value: T?, gen: JsonGenerator, provider: SerializerProvider) {
// As shown in the processing of the factory function, jsonValueGetter is always a static method.
val jsonValue: Any? = staticJsonValueGetter.invoke(null, value)
jsonValue
?.let { provider.findValueSerializer(it::class.java).serialize(it, gen, provider) }
?: provider.findNullValueSerializer(null).serialize(null, gen, provider)
}

// Since JsonValue can be processed correctly if it is given to a non-static getter/field,
// this class will only process if it is a `static` method.
companion object {
fun <T> createdOrNull(
outerClazz: Class<out Any>,
innerClazz: Class<T>
): ValueClassStaticJsonValueSerializer<T>? = outerClazz
.getStaticJsonValueGetter()
?.let { ValueClassStaticJsonValueSerializer(innerClazz, it) }
}
}
Loading

0 comments on commit 601bc71

Please sign in to comment.