jackson-module-kotlin icon indicating copy to clipboard operation
jackson-module-kotlin copied to clipboard

@JsonTypeInfo(use = CLASS) does not work if another field is a value class

Open alturkovic opened this issue 1 year ago • 0 comments

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

alturkovic avatar Aug 29 '24 11:08 alturkovic