From e760c590b75dde8984a237f3d5a888381c6837d1 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 4 May 2025 22:19:32 +0900 Subject: [PATCH] Added consideration of JsonProperty.isRequired from https://github.com/FasterXML/jackson-module-kotlin/pull/929 --- .../KotlinPrimaryAnnotationIntrospector.kt | 13 ++++- .../kogera/zPorted/test/github/GitHub922.kt | 50 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinPrimaryAnnotationIntrospector.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinPrimaryAnnotationIntrospector.kt index 7251117b..4c676743 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinPrimaryAnnotationIntrospector.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinPrimaryAnnotationIntrospector.kt @@ -1,5 +1,7 @@ package io.github.projectmapk.jackson.module.kogera.annotationIntrospector +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.OptBoolean import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.introspect.Annotated import com.fasterxml.jackson.databind.introspect.AnnotatedField @@ -26,13 +28,22 @@ internal class KotlinPrimaryAnnotationIntrospector( private val nullToEmptyMap: Boolean, private val cache: ReflectionCache ) : NopAnnotationIntrospector() { + // If a new isRequired is explicitly specified or the old required is true, those values take precedence. + // In other cases, override is done by KotlinModule. + private fun JsonProperty.forceRequiredByAnnotation(): Boolean? = when { + isRequired != OptBoolean.DEFAULT -> isRequired.asBoolean() + required -> true + else -> null + } + // If JsonProperty.required is true, the behavior is clearly specified and the result is paramount. // Otherwise, the required is determined from the configuration and the definition on Kotlin. override fun hasRequiredMarker(m: AnnotatedMember): Boolean? { return cache.getJmClass(m.member.declaringClass)?.let { jmClass -> // To avoid overwriting processing by other modules, annotations are checked after JmClass has been obtained _findAnnotation(m, JSON_PROPERTY_CLASS) - ?.let { if (it.required) return true } + ?.forceRequiredByAnnotation() + ?.let { return it } when (m) { is AnnotatedField -> m.hasRequiredMarker(jmClass) diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub922.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub922.kt index 83fbb6e7..f5eac4e0 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub922.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/test/github/GitHub922.kt @@ -1,11 +1,16 @@ package io.github.projectmapk.jackson.module.kogera.zPorted.test.github +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.OptBoolean import com.fasterxml.jackson.databind.BeanDescription import com.fasterxml.jackson.databind.ObjectMapper import io.github.projectmapk.jackson.module.kogera.KotlinFeature +import io.github.projectmapk.jackson.module.kogera.defaultMapper import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import kotlin.reflect.full.memberProperties class GitHub922 { private inline fun ObjectMapper.introspectSerialization(): BeanDescription = @@ -29,4 +34,49 @@ class GitHub922 { assertTrue(desc.isRequired("list")) assertTrue(desc.isRequired("map")) } + + // isRequired_required_nullability_expected + @Suppress("PropertyName") + data class IsRequiredDto( + // region: isRequired takes precedence + @JsonProperty(isRequired = OptBoolean.FALSE, required = false) + val FALSE_false_nullable_false: String?, + @JsonProperty(isRequired = OptBoolean.FALSE, required = false) + val FALSE_false_nonNull_false: String, + @JsonProperty(isRequired = OptBoolean.FALSE, required = true) + val FALSE_true_nullable_false: String?, + @JsonProperty(isRequired = OptBoolean.FALSE, required = true) + val FALSE_true_nonNull_false: String, + @JsonProperty(isRequired = OptBoolean.TRUE, required = false) + val TRUE_false_nullable_true: String?, + @JsonProperty(isRequired = OptBoolean.TRUE, required = false) + val TRUE_false_nonNull_true: String, + @JsonProperty(isRequired = OptBoolean.TRUE, required = true) + val TRUE_true_nullable_true: String?, + @JsonProperty(isRequired = OptBoolean.TRUE, required = true) + val TRUE_true_nonNull_true: String, + // endregion + // region: If isRequired is the default, only overrides by required = true will work. + @JsonProperty(isRequired = OptBoolean.DEFAULT, required = false) + val DEFAULT_false_nullable_false: String?, + @JsonProperty(isRequired = OptBoolean.DEFAULT, required = false) + val DEFAULT_false_nonNull_true: String, + @JsonProperty(isRequired = OptBoolean.DEFAULT, required = true) + val DEFAULT_true_nullable_true: String?, + @JsonProperty(isRequired = OptBoolean.DEFAULT, required = true) + val DEFAULT_true_nonNull_true: String, + // endregion + ) + + @Test + fun `JsonProperty properly overrides required`() { + val desc = defaultMapper.introspectDeserialization() + + IsRequiredDto::class.memberProperties.forEach { prop -> + val name = prop.name + val expected = name.split("_").last().toBoolean() + + assertEquals(expected, desc.isRequired(name), name) + } + } }