jackson-module-kotlin
jackson-module-kotlin copied to clipboard
@JsonTypeInfo(use = CLASS) does not work if another field is a value class
Search before asking
- [X] I searched in the issues and found nothing similar.
- [X] I searched in the issues of databind and other modules used and found nothing similar.
- [X] I have confirmed that the problem only occurs when using Kotlin.
Describe the bug
When using kotlin, the @JsonTypeInfo(use = CLASS) does nothing if another field is a value class.
To Reproduce
package app.interceptor.core.filter
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.CLASS
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.KotlinFeature
import com.fasterxml.jackson.module.kotlin.kotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import kotlinx.coroutines.delay
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
val mapper: ObjectMapper = ObjectMapper()
.registerModule(kotlinModule {
enable(KotlinFeature.UseJavaDurationConversion)
})
.registerModule(JavaTimeModule())
.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
interface Filter {
suspend fun accept(): Boolean
}
data object AlwaysTrueFilter : Filter {
override suspend fun accept() = true
}
data class AllFilter(@JsonTypeInfo(use = CLASS) val filters: List<Filter>) : Filter {
constructor(vararg filters: Filter) : this(filters.asList())
override suspend fun accept() = filters.all { it.accept() }
}
data class TimeoutFilter(val timeout: Duration, @JsonTypeInfo(use = CLASS) val filter: Filter) : Filter {
override suspend fun accept(): Boolean {
TODO("Not yet implemented")
}
}
data class DelayFilter(val duration: Duration) : Filter {
override suspend fun accept(): Boolean {
delay(duration)
return true
}
}
fun main() {
val allFilter = AllFilter(AlwaysTrueFilter)
val jsonAllFilter = mapper.writeValueAsString(allFilter)
println(jsonAllFilter)
println(mapper.readValue<AllFilter>(jsonAllFilter))
val delayFilter = DelayFilter(1.seconds)
val jsonDelayFilter = mapper.writeValueAsString(delayFilter)
println(jsonDelayFilter)
println(mapper.readValue<DelayFilter>(jsonDelayFilter))
val timeoutFilter = TimeoutFilter(1.seconds, AlwaysTrueFilter)
val jsonTimeoutFilter = mapper.writeValueAsString(timeoutFilter)
println(jsonTimeoutFilter)
println(mapper.readValue<TimeoutFilter>(jsonTimeoutFilter))
}
This produces:
{"filters":[{"@class":"app.interceptor.core.filter.AlwaysTrueFilter"}]}
AllFilter(filters=[AlwaysTrueFilter])
{"duration":"PT1S"}
DelayFilter(duration=1s)
{"timeout":"PT1S","filter":{}}
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `app.interceptor.core.filter.Filter` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 28] (through reference chain: app.interceptor.core.filter.TimeoutFilter["filter"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1887)
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:414)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1375)
at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserialize(AbstractDeserializer.java:274)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:545)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:576)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:446)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1493)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4905)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3848)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3831)
at app.interceptor.core.filter.TestKt.main(Test.kt:59)
at app.interceptor.core.filter.TestKt.main(Test.kt)
Expected behavior
Everything works correctly if @JsonTypeInfo(use = CLASS) is used by itself (AllFilter) or a value class field is used by itself (DelayFilter).
I would expect that this will work when I use both a value class field and a JsonTypeInfo field (TimeoutFilter), but it breaks.
Versions
Kotlin: 2.0.20 Jackson-module-kotlin: 2.17.2 Jackson-databind: 2.17.2
Additional context
No response