Skip to content
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

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

Closed
3 tasks done
alturkovic opened this issue Aug 29, 2024 · 2 comments
Closed
3 tasks done
Labels

Comments

@alturkovic
Copy link

alturkovic commented Aug 29, 2024

Search before asking

  • I searched in the issues and found nothing similar.
  • I searched in the issues of databind and other modules used and found nothing similar.
  • 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 alturkovic added the bug label Aug 29, 2024
@k163377
Copy link
Contributor

k163377 commented Sep 14, 2024

It is closed as a duplicate of #651.
I tried it locally and it seemed to work by doing the following

data class TimeoutFilter(val timeout: Duration, @field:JsonTypeInfo(use = CLASS) val filter: Filter) : Filter {
    override suspend fun accept(): Boolean {
        TODO("Not yet implemented")
    }
}

@k163377 k163377 closed this as completed Sep 14, 2024
@alturkovic
Copy link
Author

Thanks for the clarification!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants