diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 9f8b89aa..281c7322 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -19,6 +19,7 @@ Contributors: WrongWrong (@k163377) * #687: Optimize and Refactor KotlinValueInstantiator.createFromObjectWith +* #686: Add KotlinPropertyNameAsImplicitName option * #685: Streamline default value management for KotlinFeatures * #684: Update Kotlin Version to 1.6 * #682: Remove MissingKotlinParameterException and replace with MismatchedInputException diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 7de33801..f9372401 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -21,6 +21,9 @@ Co-maintainers: #687: Optimize and Refactor KotlinValueInstantiator.createFromObjectWith. This improves deserialization throughput about 1.3 ~ 1.5 times faster. https://github.com/FasterXML/jackson-module-kotlin/pull/687#issuecomment-1637365799 +#686: Added KotlinPropertyNameAsImplicitName feature to use Kotlin property names as implicit names for getters. + Enabling this feature eliminates some of the problems summarized in #630, + but also causes some behavioral changes and performance degradation. #685: Streamline default value management for KotlinFeatures. This improves the initialization cost of kotlin-module a little. #684: Kotlin 1.5 has been deprecated and the minimum supported Kotlin version will be updated to 1.6. diff --git a/src/main/kotlin/tools/jackson/module/kotlin/KotlinFeature.kt b/src/main/kotlin/tools/jackson/module/kotlin/KotlinFeature.kt index f268d33c..ee3763b8 100644 --- a/src/main/kotlin/tools/jackson/module/kotlin/KotlinFeature.kt +++ b/src/main/kotlin/tools/jackson/module/kotlin/KotlinFeature.kt @@ -1,7 +1,6 @@ package tools.jackson.module.kotlin import java.util.BitSet -import kotlin.math.pow /** * @see KotlinModule.Builder @@ -42,7 +41,24 @@ enum class KotlinFeature(private val enabledByDefault: Boolean) { * may contain null values after deserialization. * Enabling it protects against this but has significant performance impact. */ - StrictNullChecks(enabledByDefault = false); + StrictNullChecks(enabledByDefault = false), + + /** + * By enabling this feature, the property name on Kotlin is used as the implicit name for the getter. + * + * By default, the getter name is used during serialization. + * This name may be different from the parameter/field name, in which case serialization results + * may be incorrect or annotations may malfunction. + * See [jackson-module-kotlin#630] for details. + * + * By enabling this feature, such malfunctions will not occur. + * + * On the other hand, enabling this option increases the amount of reflection processing, + * which may result in performance degradation for both serialization and deserialization. + * In addition, the adjustment of behavior using get:JvmName is disabled. + * Note also that this feature does not apply to setters. + */ + KotlinPropertyNameAsImplicitName(enabledByDefault = false); internal val bitSet: BitSet = (1 shl ordinal).toBitSet() diff --git a/src/main/kotlin/tools/jackson/module/kotlin/KotlinModule.kt b/src/main/kotlin/tools/jackson/module/kotlin/KotlinModule.kt index 427001af..661ed551 100644 --- a/src/main/kotlin/tools/jackson/module/kotlin/KotlinModule.kt +++ b/src/main/kotlin/tools/jackson/module/kotlin/KotlinModule.kt @@ -1,18 +1,15 @@ package tools.jackson.module.kotlin -import java.util.* - import kotlin.reflect.KClass - import tools.jackson.databind.MapperFeature import tools.jackson.databind.module.SimpleModule - import tools.jackson.module.kotlin.KotlinFeature.NullIsSameAsDefault import tools.jackson.module.kotlin.KotlinFeature.NullToEmptyCollection import tools.jackson.module.kotlin.KotlinFeature.NullToEmptyMap import tools.jackson.module.kotlin.KotlinFeature.StrictNullChecks import tools.jackson.module.kotlin.SingletonSupport.CANONICALIZE import tools.jackson.module.kotlin.SingletonSupport.DISABLED +import java.util.* private const val metadataFqName = "kotlin.Metadata" @@ -55,8 +52,9 @@ class KotlinModule @Deprecated( val nullToEmptyMap: Boolean = false, val nullIsSameAsDefault: Boolean = false, val singletonSupport: SingletonSupport = DISABLED, - val strictNullChecks: Boolean = false -) : SimpleModule(KotlinModule::class.java.name, tools.jackson.module.kotlin.PackageVersion.VERSION) { + val strictNullChecks: Boolean = false, + val useKotlinPropertyNameForGetter: Boolean = false +) : SimpleModule(KotlinModule::class.java.name, PackageVersion.VERSION) { init { if (!KotlinVersion.CURRENT.isAtLeast(1, 5)) { // Kotlin 1.4 was deprecated when this process was introduced(jackson-module-kotlin 2.15). @@ -103,7 +101,8 @@ class KotlinModule @Deprecated( builder.isEnabled(KotlinFeature.SingletonSupport) -> CANONICALIZE else -> DISABLED }, - builder.isEnabled(StrictNullChecks) + builder.isEnabled(StrictNullChecks), + builder.isEnabled(KotlinFeature.KotlinPropertyNameAsImplicitName) ) companion object { @@ -132,7 +131,13 @@ class KotlinModule @Deprecated( } context.insertAnnotationIntrospector(KotlinAnnotationIntrospector(context, cache, nullToEmptyCollection, nullToEmptyMap, nullIsSameAsDefault)) - context.appendAnnotationIntrospector(KotlinNamesAnnotationIntrospector(this, cache, ignoredClassesForImplyingJsonCreator)) + context.appendAnnotationIntrospector( + KotlinNamesAnnotationIntrospector( + this, + cache, + ignoredClassesForImplyingJsonCreator, + useKotlinPropertyNameForGetter) + ) context.addDeserializers(KotlinDeserializers()) context.addKeyDeserializers(KotlinKeyDeserializers) diff --git a/src/main/kotlin/tools/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt b/src/main/kotlin/tools/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt index 2c62e748..0937359e 100644 --- a/src/main/kotlin/tools/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt +++ b/src/main/kotlin/tools/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt @@ -3,16 +3,13 @@ package tools.jackson.module.kotlin import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonCreator.Mode import com.fasterxml.jackson.annotation.JsonProperty -import tools.jackson.databind.PropertyName import tools.jackson.databind.cfg.MapperConfig import tools.jackson.databind.introspect.Annotated import tools.jackson.databind.introspect.AnnotatedConstructor -import tools.jackson.databind.introspect.AnnotatedField import tools.jackson.databind.introspect.AnnotatedMember import tools.jackson.databind.introspect.AnnotatedMethod import tools.jackson.databind.introspect.AnnotatedParameter import tools.jackson.databind.introspect.NopAnnotationIntrospector -import tools.jackson.databind.util.BeanUtil import java.lang.reflect.Constructor import java.lang.reflect.Method import java.util.Locale @@ -25,34 +22,57 @@ import kotlin.reflect.full.hasAnnotation import kotlin.reflect.full.memberProperties import kotlin.reflect.full.primaryConstructor import kotlin.reflect.jvm.internal.KotlinReflectionInternalError +import kotlin.reflect.jvm.javaGetter import kotlin.reflect.jvm.javaType import kotlin.reflect.jvm.kotlinFunction -internal class KotlinNamesAnnotationIntrospector(val module: KotlinModule, val cache: ReflectionCache, val ignoredClassesForImplyingJsonCreator: Set>) : NopAnnotationIntrospector() { +internal class KotlinNamesAnnotationIntrospector( + val module: KotlinModule, + val cache: ReflectionCache, + val ignoredClassesForImplyingJsonCreator: Set>, + val useKotlinPropertyNameForGetter: Boolean +) : NopAnnotationIntrospector() { + private fun getterNameFromJava(member: AnnotatedMethod): String? { + val name = member.name + + // The reason for truncating after `-` is to truncate the random suffix + // given after the value class accessor name. + return when { + name.startsWith("get") -> name.takeIf { it.contains("-") }?.let { _ -> + name.substringAfter("get") + .replaceFirstChar { it.lowercase(Locale.getDefault()) } + .substringBefore('-') + } + // since 2.15: support Kotlin's way of handling "isXxx" backed properties where + // logical property name needs to remain "isXxx" and not become "xxx" as with Java Beans + // (see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html and + // https://github.com/FasterXML/jackson-databind/issues/2527 and + // https://github.com/FasterXML/jackson-module-kotlin/issues/340 + // for details) + name.startsWith("is") -> if (name.contains("-")) name.substringAfter("-") else name + else -> null + } + } + + private fun getterNameFromKotlin(member: AnnotatedMethod): String? { + val getter = member.member + + return member.member.declaringClass.takeIf { it.isKotlinClass() }?.let { clazz -> + clazz.kotlin.memberProperties.find { it.javaGetter == getter } + ?.let { it.name } + } + } + + // since 2.4 override fun findImplicitPropertyName(config: MapperConfig<*>, member: AnnotatedMember): String? { if (!member.declaringClass.isKotlinClass()) return null - val name = member.name - return when (member) { is AnnotatedMethod -> if (member.parameterCount == 0) { - // The reason for truncating after `-` is to truncate the random suffix - // given after the value class accessor name. - when { - name.startsWith("get") -> name.takeIf { it.contains("-") }?.let { _ -> - name.substringAfter("get") - .replaceFirstChar { it.lowercase(Locale.getDefault()) } - .substringBefore('-') - } - // since 2.15: support Kotlin's way of handling "isXxx" backed properties where - // logical property name needs to remain "isXxx" and not become "xxx" as with Java Beans - // (see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html and - // https://github.com/FasterXML/jackson-databind/issues/2527 and - // https://github.com/FasterXML/jackson-module-kotlin/issues/340 - // for details) - name.startsWith("is") -> if (name.contains("-")) name.substringAfter("-") else name - else -> null - } + if (useKotlinPropertyNameForGetter) { + // Fall back to default if it is a getter-like function + getterNameFromKotlin(member) ?: getterNameFromJava(member) + } else getterNameFromJava(member) } else null is AnnotatedParameter -> findKotlinParameterName(member) else -> null diff --git a/src/test/kotlin/tools/jackson/module/kotlin/test/github/Github630.kt b/src/test/kotlin/tools/jackson/module/kotlin/test/github/Github630.kt new file mode 100644 index 00000000..67e25480 --- /dev/null +++ b/src/test/kotlin/tools/jackson/module/kotlin/test/github/Github630.kt @@ -0,0 +1,40 @@ +package tools.jackson.module.kotlin.test.github + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinFeature +import com.fasterxml.jackson.module.kotlin.KotlinModule +import org.junit.Test +import kotlin.test.assertEquals + +class Github630 { + private val mapper = ObjectMapper() + .registerModule(KotlinModule.Builder().enable(KotlinFeature.KotlinPropertyNameAsImplicitName).build())!! + + 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) + ) + } +}