-
Notifications
You must be signed in to change notification settings - Fork 175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Duration support #689
Duration support #689
Changes from 7 commits
cf5281c
7a61b3f
a1ba894
efeb17f
58ca164
bc2a0b3
79d8e4e
b56e139
9709b83
7161b2b
a0151d4
bdc58b4
4fba300
e650766
f2e51b4
73bf682
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,6 @@ 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 com.fasterxml.jackson.databind.util.Converter | ||
import java.lang.reflect.AccessibleObject | ||
import java.lang.reflect.Constructor | ||
|
@@ -23,13 +22,17 @@ import kotlin.reflect.full.createType | |
import kotlin.reflect.full.declaredMemberProperties | ||
import kotlin.reflect.full.memberProperties | ||
import kotlin.reflect.jvm.* | ||
import kotlin.time.Duration | ||
|
||
|
||
internal class KotlinAnnotationIntrospector(private val context: Module.SetupContext, | ||
private val cache: ReflectionCache, | ||
private val nullToEmptyCollection: Boolean, | ||
private val nullToEmptyMap: Boolean, | ||
private val nullIsSameAsDefault: Boolean) : NopAnnotationIntrospector() { | ||
internal class KotlinAnnotationIntrospector( | ||
private val context: Module.SetupContext, | ||
private val cache: ReflectionCache, | ||
private val nullToEmptyCollection: Boolean, | ||
private val nullToEmptyMap: Boolean, | ||
private val nullIsSameAsDefault: Boolean, | ||
private val useJavaDurationConversion: Boolean, | ||
) : NopAnnotationIntrospector() { | ||
|
||
// TODO: implement nullIsSameAsDefault flag, which represents when TRUE that if something has a default value, it can be passed a null to default it | ||
// this likely impacts this class to be accurate about what COULD be considered required | ||
|
@@ -66,11 +69,14 @@ internal class KotlinAnnotationIntrospector(private val context: Module.SetupCon | |
|
||
override fun findSerializationConverter(a: Annotated): Converter<*, *>? = when (a) { | ||
// Find a converter to handle the case where the getter returns an unboxed value from the value class. | ||
is AnnotatedMethod -> cache.findValueClassReturnType(a) | ||
?.let { cache.getValueClassBoxConverter(a.rawReturnType, it) } | ||
is AnnotatedClass -> a | ||
.takeIf { Sequence::class.java.isAssignableFrom(it.rawType) } | ||
?.let { SequenceToIteratorConverter(it.type) } | ||
is AnnotatedMethod -> a.ktClass()?.let { cache.getValueClassBoxConverter(a.rawReturnType, it) } | ||
is AnnotatedClass -> lookupKotlinTypeConverter(a) | ||
else -> null | ||
} | ||
|
||
private fun lookupKotlinTypeConverter(a: AnnotatedClass) = when { | ||
Sequence::class.java.isAssignableFrom(a.rawType) -> SequenceToIteratorConverter(a.type) | ||
Duration::class.java.isAssignableFrom(a.rawType) -> KotlinToJavaDurationConverter.takeIf { useJavaDurationConversion } | ||
k163377 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
else -> null | ||
} | ||
|
||
|
@@ -81,11 +87,15 @@ internal class KotlinAnnotationIntrospector(private val context: Module.SetupCon | |
|
||
// Perform proper serialization even if the value wrapped by the value class is null. | ||
// If value is a non-null object type, it must not be reboxing. | ||
override fun findNullSerializer(am: Annotated): JsonSerializer<*>? = (am as? AnnotatedMethod)?.let { _ -> | ||
cache.findValueClassReturnType(am) | ||
?.takeIf { it.requireRebox() } | ||
?.let { cache.getValueClassBoxConverter(am.rawReturnType, it).delegatingSerializer } | ||
} | ||
override fun findNullSerializer(am: Annotated): JsonSerializer<*>? = (am as? AnnotatedMethod) | ||
?.ktClass() | ||
?.takeIf { it.requireRebox() } | ||
?.let { cache.getValueClassBoxConverter(am.rawReturnType, it).delegatingSerializer } | ||
|
||
override fun findSerializer(am: Annotated): Any? = when ((am as? AnnotatedMethod)?.ktClass()) { | ||
Duration::class -> KotlinToJavaDurationConverter.delegatingSerializer.takeIf { useJavaDurationConversion } | ||
else -> null | ||
} ?: super.findSerializer(am) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This appears unnecessary since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is solution to nullable fields you mentioned here:
If I comment line 96 then
Though I can remove There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, I guess I misunderstood the function. However, let me make it a policy to use only The policy of using only a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, cherry-picked this commit. |
||
|
||
/** | ||
* Subclasses can be detected automatically for sealed classes, since all possible subclasses are known | ||
|
@@ -102,7 +112,7 @@ internal class KotlinAnnotationIntrospector(private val context: Module.SetupCon | |
|
||
private fun AnnotatedField.hasRequiredMarker(): Boolean? { | ||
val byAnnotation = (member as Field).isRequiredByAnnotation() | ||
val byNullability = (member as Field).kotlinProperty?.returnType?.isRequired() | ||
val byNullability = (member as Field).kotlinProperty?.returnType?.isRequired() | ||
|
||
return requiredAnnotationOrNullability(byAnnotation, byNullability) | ||
} | ||
|
@@ -122,7 +132,7 @@ internal class KotlinAnnotationIntrospector(private val context: Module.SetupCon | |
} | ||
|
||
private fun Method.isRequiredByAnnotation(): Boolean? { | ||
return (this.annotations.firstOrNull { it.annotationClass.java == JsonProperty::class.java } as? JsonProperty)?.required | ||
return (this.annotations.firstOrNull { it.annotationClass.java == JsonProperty::class.java } as? JsonProperty)?.required | ||
} | ||
|
||
// Since Kotlin's property has the same Type for each field, getter, and setter, | ||
|
@@ -171,12 +181,14 @@ internal class KotlinAnnotationIntrospector(private val context: Module.SetupCon | |
return requiredAnnotationOrNullability(byAnnotation, byNullability) | ||
} | ||
|
||
private fun AnnotatedMethod.ktClass() = cache.findValueClassReturnType(this) | ||
k163377 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
private fun KFunction<*>.isConstructorParameterRequired(index: Int): Boolean { | ||
return isParameterRequired(index) | ||
} | ||
|
||
private fun KFunction<*>.isMethodParameterRequired(index: Int): Boolean { | ||
return isParameterRequired(index+1) | ||
return isParameterRequired(index + 1) | ||
} | ||
|
||
private fun KFunction<*>.isParameterRequired(index: Int): Boolean { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to implement a
findDeserializationConverter
that returns aConverter
for anAnnotatedParameter
?I think it can be implemented by just adding
Converter
.https://github.com/ProjectMapK/jackson-module-kogera/blob/ca6bfe5a1689ebef4dff795ae21ccbe7ca0248df/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotation_introspector/KotlinFallbackAnnotationIntrospector.kt#L92
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately we can't do that without changes in Jackson core libs.
tl;dr; Jackson extracts parameter of type
long
and@JsonDeserialization
annotation is only source of its real type (in Java-reflection land).Why it happens
If we check ctors of tested class:
We will got:
Jackson looks only for Java ctors and removed synthetic ctors from lookup in FasterXML/jackson-databind#1005 hence we will see only:
How we can restore original type
Theoretically, we can restore original types via Kotlin reflection but this will work only for synthetic ctor, following code:
will return:
If we could collect that synthetic constructors (or just check for one with
kotlin.jvm.internal.DefaultConstructorMarker
param) then we can remove need of explicit@JsonCreator
and@JsonDeserializer
.That being said, with current Jackson code we can't customize collection of constructors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since annotations are applied to the type after conversion in serialization, I mistakenly thought that this was also true for deserialization.
If this is the case, I think it is sufficient to implement only the deserializer, so I am very sorry, but please remove
JavaToKotlinDurationConverter
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean this?
https://github.com/FasterXML/jackson-module-kotlin/pull/689/files#diff-0fe95cd6b35f4ae8bd509a735e78af8cd195263fdeeb9de7fdb30e1be802bb21R107
This won't cut the bill (at least in current code) because type we see from a ctor definition is a
long
hence we fall in wrongwhen
-branch. Order inwhen
clause doesn't matter.If I remove converter I need to remove it from POJO in
"should deserialize Kotlin duration inside data class"
and test will fail:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I seem to be confused myself.
I have compiled a list of proposed fixes for deserialization, could you please check it out?
The following points have been improved
KotlinDuration
argument can now be deserialized without annotationDelegatingDeserializer
Java
->Kotlin
conversion with stdlib functionhttps://github.com/k163377/jackson-module-kotlin/compare/9709b83ebbc08ebbe39ad0931033b63de91bbe66..a0151d422267109a246c95c18242633b235c3ae7
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, thank you!
Now I understand what you mean before, you wanted me to replace (remove) deserializer with converter like in case of serialization.
Ah, missed that one. Good catch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One minor thing, personally I would add guard condition to avoid
return@let
and usewhen
at the end but I leave decision up to you.