Skip to content

Commit a9d3981

Browse files
authored
Merge pull request #952 from k163377/941
Change the `hasRequiredMarker` implementation to the 3.x specification
2 parents 4146ebf + c1a7528 commit a9d3981

File tree

6 files changed

+121
-135
lines changed

6 files changed

+121
-135
lines changed

release-notes/CREDITS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ Contributors:
1919

2020
# 3.0.0-rc4 (not yet released)
2121

22+
WrongWrong (@k163377)
23+
* #952: Change the `hasRequiredMarker` implementation to the 3.x specification
24+
2225
# 3.0.0-rc3 (13-Apr-2025)
26+
2327
WrongWrong (@k163377)
2428
* #945: Replace JacksonXmlRootElement used in the test with JsonRootName
2529

release-notes/VERSION

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ Former maintainers:
2424

2525
3.0.0-rc4 (not yet released)
2626

27+
#952: The `isRequired` result from `kotlin-module` no longer overrides the result from
28+
`JacksonAnnotationIntrospector` or other `AnnotationIntrospector`s.
29+
Tests have confirmed that `@JsonProperty(required = true)` for nullable parameters has been changed to be
30+
determined as `required`.
31+
2732
3.0.0-rc3 (13-Apr-2025)
2833

2934
#887: Change 3.0 to use `module-info.java` directly [JSTEP-11]
Lines changed: 1 addition & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,19 @@
11
package tools.jackson.module.kotlin
22

3-
import com.fasterxml.jackson.annotation.JsonProperty
4-
import com.fasterxml.jackson.annotation.OptBoolean
5-
import tools.jackson.databind.DeserializationFeature
6-
import tools.jackson.databind.JacksonModule
73
import tools.jackson.databind.cfg.MapperConfig
84
import tools.jackson.databind.introspect.Annotated
95
import tools.jackson.databind.introspect.AnnotatedClass
10-
import tools.jackson.databind.introspect.AnnotatedField
11-
import tools.jackson.databind.introspect.AnnotatedMember
126
import tools.jackson.databind.introspect.AnnotatedMethod
13-
import tools.jackson.databind.introspect.AnnotatedParameter
147
import tools.jackson.databind.introspect.NopAnnotationIntrospector
158
import tools.jackson.databind.jsontype.NamedType
169
import tools.jackson.databind.util.Converter
17-
import java.lang.reflect.AccessibleObject
18-
import java.lang.reflect.Field
19-
import java.lang.reflect.Method
20-
import kotlin.reflect.KFunction
21-
import kotlin.reflect.KMutableProperty1
22-
import kotlin.reflect.KParameter
23-
import kotlin.reflect.KProperty1
24-
import kotlin.reflect.KType
25-
import kotlin.reflect.full.createType
26-
import kotlin.reflect.full.declaredMemberProperties
27-
import kotlin.reflect.full.valueParameters
28-
import kotlin.reflect.jvm.javaGetter
29-
import kotlin.reflect.jvm.javaSetter
30-
import kotlin.reflect.jvm.javaType
31-
import kotlin.reflect.jvm.kotlinProperty
3210
import kotlin.time.Duration
3311

3412
internal class KotlinAnnotationIntrospector(
35-
private val context: JacksonModule.SetupContext,
3613
private val cache: ReflectionCache,
37-
private val nullToEmptyCollection: Boolean,
38-
private val nullToEmptyMap: Boolean,
39-
private val nullIsSameAsDefault: Boolean,
4014
private val useJavaDurationConversion: Boolean,
4115
) : NopAnnotationIntrospector() {
4216

43-
// TODO: implement nullIsSameAsDefault flag, which represents when TRUE that if something has a default value, it can be passed a null to default it
44-
// this likely impacts this class to be accurate about what COULD be considered required
45-
46-
// If a new isRequired is explicitly specified or the old required is true, those values take precedence.
47-
// In other cases, override is done by KotlinModule.
48-
private fun JsonProperty.forceRequiredByAnnotation(): Boolean? = when {
49-
isRequired != OptBoolean.DEFAULT -> isRequired.asBoolean()
50-
required -> true
51-
else -> null
52-
}
53-
54-
private fun AccessibleObject.forceRequiredByAnnotation(): Boolean? =
55-
getAnnotation(JsonProperty::class.java)?.forceRequiredByAnnotation()
56-
57-
override fun hasRequiredMarker(
58-
cfg : MapperConfig<*>,
59-
m: AnnotatedMember
60-
): Boolean? = m.takeIf { it.member.declaringClass.isKotlinClass() }?.let { _ ->
61-
cache.javaMemberIsRequired(m) {
62-
try {
63-
when (m) {
64-
is AnnotatedField -> m.hasRequiredMarker()
65-
is AnnotatedMethod -> m.hasRequiredMarker()
66-
is AnnotatedParameter -> m.hasRequiredMarker()
67-
else -> null
68-
}
69-
} catch (_: UnsupportedOperationException) {
70-
null
71-
}
72-
}
73-
}
74-
7517
override fun findSerializationConverter(config: MapperConfig<*>?, a: Annotated): Converter<*, *>? = when (a) {
7618
// Find a converter to handle the case where the getter returns an unboxed value from the value class.
7719
is AnnotatedMethod -> a.findValueClassReturnType()?.let {
@@ -105,7 +47,7 @@ internal class KotlinAnnotationIntrospector(
10547
* Subclasses can be detected automatically for sealed classes, since all possible subclasses are known
10648
* at compile-time to Kotlin. This makes [com.fasterxml.jackson.annotation.JsonSubTypes] redundant.
10749
*/
108-
override fun findSubtypes(cfg : MapperConfig<*>, a: Annotated): MutableList<NamedType>? = a.rawType
50+
override fun findSubtypes(cfg: MapperConfig<*>, a: Annotated): MutableList<NamedType>? = a.rawType
10951
.takeIf { it.isKotlinClass() }
11052
?.let { rawType ->
11153
rawType.kotlin.sealedSubclasses
@@ -114,70 +56,5 @@ internal class KotlinAnnotationIntrospector(
11456
.ifEmpty { null }
11557
}
11658

117-
private fun AnnotatedField.hasRequiredMarker(): Boolean? {
118-
val field = member as Field
119-
return field.forceRequiredByAnnotation()
120-
?: field.kotlinProperty?.returnType?.isRequired()
121-
}
122-
123-
// Since Kotlin's property has the same Type for each field, getter, and setter,
124-
// nullability can be determined from the returnType of KProperty.
125-
private fun KProperty1<*, *>.isRequiredByNullability() = returnType.isRequired()
126-
127-
// This could be a setter or a getter of a class property or
128-
// a setter-like/getter-like method.
129-
private fun AnnotatedMethod.hasRequiredMarker(): Boolean? = this.getRequiredMarkerFromCorrespondingAccessor()
130-
?: this.member.getRequiredMarkerFromAccessorLikeMethod()
131-
132-
private fun AnnotatedMethod.getRequiredMarkerFromCorrespondingAccessor(): Boolean? {
133-
member.declaringClass.kotlin.declaredMemberProperties.forEach { kProperty ->
134-
if (kProperty.javaGetter == this.member || (kProperty as? KMutableProperty1)?.javaSetter == this.member) {
135-
return member.forceRequiredByAnnotation() ?: kProperty.isRequiredByNullability()
136-
}
137-
}
138-
return null
139-
}
140-
141-
// Is the member method a regular method of the data class or
142-
private fun Method.getRequiredMarkerFromAccessorLikeMethod(): Boolean? = cache.kotlinFromJava(this)?.let { func ->
143-
forceRequiredByAnnotation() ?: when {
144-
func.isGetterLike() -> func.returnType.isRequired()
145-
// If nullToEmpty could be supported for setters,
146-
// a branch similar to AnnotatedParameter.hasRequiredMarker should be added.
147-
func.isSetterLike() -> func.valueParameters[0].isRequired()
148-
else -> null
149-
}
150-
}
151-
152-
private fun KFunction<*>.isGetterLike(): Boolean = parameters.size == 1
153-
private fun KFunction<*>.isSetterLike(): Boolean = parameters.size == 2 && returnType == UNIT_TYPE
154-
155-
private fun AnnotatedParameter.hasRequiredMarker(): Boolean? = getAnnotation(JsonProperty::class.java)
156-
?.forceRequiredByAnnotation()
157-
?: run {
158-
when {
159-
nullToEmptyCollection && type.isCollectionLikeType -> false
160-
nullToEmptyMap && type.isMapLikeType -> false
161-
else -> cache.findKotlinParameter(this)?.isRequired()
162-
}
163-
}
164-
16559
private fun AnnotatedMethod.findValueClassReturnType() = cache.findValueClassReturnType(this)
166-
167-
private fun KParameter.isRequired(): Boolean {
168-
val paramType = type
169-
val isPrimitive = when (val javaType = paramType.javaType) {
170-
is Class<*> -> javaType.isPrimitive
171-
else -> false
172-
}
173-
174-
return !paramType.isMarkedNullable && !isOptional && !isVararg &&
175-
!(isPrimitive && !context.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES))
176-
}
177-
178-
private fun KType.isRequired(): Boolean = !isMarkedNullable
179-
180-
companion object {
181-
val UNIT_TYPE: KType by lazy { Unit::class.createType() }
182-
}
18360
}

src/main/kotlin/tools/jackson/module/kotlin/KotlinModule.kt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,17 @@ class KotlinModule private constructor(
9292
context.addDeserializerModifier(KotlinValueDeserializerModifier)
9393
}
9494

95-
context.insertAnnotationIntrospector(KotlinAnnotationIntrospector(
96-
context,
97-
cache,
98-
nullToEmptyCollection,
99-
nullToEmptyMap,
100-
nullIsSameAsDefault,
101-
useJavaDurationConversion
102-
))
95+
context.insertAnnotationIntrospector(KotlinAnnotationIntrospector(cache, useJavaDurationConversion))
10396
context.appendAnnotationIntrospector(
104-
KotlinNamesAnnotationIntrospector(cache, newStrictNullChecks, kotlinPropertyNameAsImplicitName)
97+
KotlinNamesAnnotationIntrospector(
98+
context = context,
99+
cache = cache,
100+
nullToEmptyCollection = nullToEmptyCollection,
101+
nullToEmptyMap = nullToEmptyMap,
102+
nullIsSameAsDefault = nullIsSameAsDefault,
103+
strictNullChecks = newStrictNullChecks,
104+
kotlinPropertyNameAsImplicitName = kotlinPropertyNameAsImplicitName
105+
)
105106
)
106107

107108
context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion))

src/main/kotlin/tools/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,127 @@ package tools.jackson.module.kotlin
33
import com.fasterxml.jackson.annotation.JsonProperty
44
import com.fasterxml.jackson.annotation.JsonSetter
55
import com.fasterxml.jackson.annotation.Nulls
6+
import tools.jackson.databind.DeserializationFeature
7+
import tools.jackson.databind.JacksonModule
68
import tools.jackson.databind.JavaType
79
import tools.jackson.databind.cfg.MapperConfig
810
import tools.jackson.databind.introspect.Annotated
911
import tools.jackson.databind.introspect.AnnotatedClass
12+
import tools.jackson.databind.introspect.AnnotatedField
1013
import tools.jackson.databind.introspect.AnnotatedMember
1114
import tools.jackson.databind.introspect.AnnotatedMethod
1215
import tools.jackson.databind.introspect.AnnotatedParameter
1316
import tools.jackson.databind.introspect.NopAnnotationIntrospector
1417
import tools.jackson.databind.introspect.PotentialCreator
1518
import java.lang.reflect.Constructor
19+
import java.lang.reflect.Field
20+
import java.lang.reflect.Method
1621
import java.util.Locale
1722
import kotlin.collections.getOrNull
1823
import kotlin.reflect.KClass
1924
import kotlin.reflect.KFunction
25+
import kotlin.reflect.KMutableProperty1
2026
import kotlin.reflect.KParameter
27+
import kotlin.reflect.KProperty1
28+
import kotlin.reflect.KType
29+
import kotlin.reflect.full.createType
30+
import kotlin.reflect.full.declaredMemberProperties
2131
import kotlin.reflect.full.hasAnnotation
2232
import kotlin.reflect.full.memberProperties
2333
import kotlin.reflect.full.primaryConstructor
34+
import kotlin.reflect.full.valueParameters
2435
import kotlin.reflect.jvm.javaGetter
36+
import kotlin.reflect.jvm.javaSetter
2537
import kotlin.reflect.jvm.javaType
38+
import kotlin.reflect.jvm.kotlinProperty
2639

2740
internal class KotlinNamesAnnotationIntrospector(
41+
private val context: JacksonModule.SetupContext,
2842
private val cache: ReflectionCache,
43+
private val nullToEmptyCollection: Boolean,
44+
private val nullToEmptyMap: Boolean,
45+
private val nullIsSameAsDefault: Boolean,
2946
private val strictNullChecks: Boolean,
3047
private val kotlinPropertyNameAsImplicitName: Boolean
3148
) : NopAnnotationIntrospector() {
49+
private fun KType.isRequired(): Boolean = !isMarkedNullable
50+
51+
// Since Kotlin's property has the same Type for each field, getter, and setter,
52+
// nullability can be determined from the returnType of KProperty.
53+
private fun KProperty1<*, *>.isRequiredByNullability() = returnType.isRequired()
54+
55+
private fun KParameter.isRequired(): Boolean {
56+
val paramType = type
57+
val isPrimitive = when (val javaType = paramType.javaType) {
58+
is Class<*> -> javaType.isPrimitive
59+
else -> false
60+
}
61+
62+
return !paramType.isMarkedNullable && !isOptional && !isVararg &&
63+
!(isPrimitive && !context.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES))
64+
}
65+
66+
private fun AnnotatedField.hasRequiredMarker(): Boolean? {
67+
val field = member as Field
68+
return field.kotlinProperty?.returnType?.isRequired()
69+
}
70+
71+
private fun KFunction<*>.isGetterLike(): Boolean = parameters.size == 1
72+
private fun KFunction<*>.isSetterLike(): Boolean = parameters.size == 2 && returnType == UNIT_TYPE
73+
74+
private fun AnnotatedMethod.getRequiredMarkerFromCorrespondingAccessor(): Boolean? {
75+
member.declaringClass.kotlin.declaredMemberProperties.forEach { kProperty ->
76+
if (kProperty.javaGetter == this.member || (kProperty as? KMutableProperty1)?.javaSetter == this.member) {
77+
return kProperty.isRequiredByNullability()
78+
}
79+
}
80+
return null
81+
}
82+
83+
// Is the member method a regular method of the data class or
84+
private fun Method.getRequiredMarkerFromAccessorLikeMethod(): Boolean? = cache.kotlinFromJava(this)?.let { func ->
85+
when {
86+
func.isGetterLike() -> func.returnType.isRequired()
87+
// If nullToEmpty could be supported for setters,
88+
// a branch similar to AnnotatedParameter.hasRequiredMarker should be added.
89+
func.isSetterLike() -> func.valueParameters[0].isRequired()
90+
else -> null
91+
}
92+
}
93+
94+
// This could be a setter or a getter of a class property or
95+
// a setter-like/getter-like method.
96+
private fun AnnotatedMethod.hasRequiredMarker(): Boolean? = this.getRequiredMarkerFromCorrespondingAccessor()
97+
?: this.member.getRequiredMarkerFromAccessorLikeMethod()
98+
99+
// TODO: implement nullIsSameAsDefault flag, which represents when TRUE that if something has a default value, it can be passed a null to default it
100+
// this likely impacts this class to be accurate about what COULD be considered required
101+
private fun AnnotatedParameter.hasRequiredMarker(): Boolean? = when {
102+
nullToEmptyCollection && type.isCollectionLikeType -> false
103+
nullToEmptyMap && type.isMapLikeType -> false
104+
else -> cache.findKotlinParameter(this)?.isRequired()
105+
}
106+
107+
override fun hasRequiredMarker(
108+
cfg: MapperConfig<*>,
109+
m: AnnotatedMember
110+
): Boolean? = m.takeIf { it.member.declaringClass.isKotlinClass() }?.let { _ ->
111+
println(m)
112+
113+
cache.javaMemberIsRequired(m) {
114+
try {
115+
when (m) {
116+
is AnnotatedField -> m.hasRequiredMarker()
117+
is AnnotatedMethod -> m.hasRequiredMarker()
118+
is AnnotatedParameter -> m.hasRequiredMarker()
119+
else -> null
120+
}
121+
} catch (_: UnsupportedOperationException) {
122+
null
123+
}
124+
}
125+
}
126+
32127
private fun getterNameFromJava(member: AnnotatedMethod): String? {
33128
val name = member.name
34129

@@ -123,6 +218,10 @@ internal class KotlinNamesAnnotationIntrospector(
123218

124219
private fun findKotlinParameter(param: Annotated) = (param as? AnnotatedParameter)
125220
?.let { cache.findKotlinParameter(it) }
221+
222+
companion object {
223+
val UNIT_TYPE: KType by lazy { Unit::class.createType() }
224+
}
126225
}
127226

128227
private fun KParameter.markedNonNullAt(index: Int) = type.arguments.getOrNull(index)?.type?.isMarkedNullable == false

src/test/kotlin/tools/jackson/module/kotlin/test/PropertyRequirednessTests.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ class TestPropertyRequiredness {
125125
"i".isOptionalForDeserializationOf(testClass, mapper)
126126

127127
"x".isRequiredForDeserializationOf(testClass, mapper)
128-
"x".isOptionalForSerializationOf(testClass, mapper)
128+
"x".isRequiredForSerializationOf(testClass, mapper)
129129

130130
"z".isRequiredForDeserializationOf(testClass, mapper)
131131
"z".isRequiredForSerializationOf(testClass, mapper)
@@ -161,7 +161,7 @@ class TestPropertyRequiredness {
161161
"h".isOptionalForDeserializationOf(testClass, mapper)
162162

163163
"x".isRequiredForDeserializationOf(testClass, mapper)
164-
"x".isOptionalForSerializationOf(testClass, mapper)
164+
"x".isRequiredForSerializationOf(testClass, mapper)
165165

166166
"z".isRequiredForDeserializationOf(testClass, mapper)
167167
"z".isRequiredForSerializationOf(testClass, mapper)

0 commit comments

Comments
 (0)