jackson-module-kotlin
jackson-module-kotlin copied to clipboard
JsonTypeInfo + interface = serialization-deserialization problems
Describe the bug When you trying to serialize or deserialize something tha annotated with JsonTypeInfo, you will get a lot of strange variants of behavior. Especially with collections(part1-4), implementations(part2), T(part3), Any(part4). I attach 13 tests: 5 works and the others don't. Tests devided by 4 groups that have own problems.
To Reproduce
package ru.sport24.hub.refresher.message
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
class BadMappingTest {
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
interface SomeInterface {
val v : Long
}
data class SomeClass(
override val v: Long
) : SomeInterface
data class SomeBox(
val items: List<SomeInterface>
)
private val boxWithInterface = SomeBox(listOf(SomeClass(1)))
/*
PART 1: Interface
why om doesnt print @class when write list of interfaces?
*/
/**
* {"items":[{"@class":"ru.sport24.hub.refresher.message.BadMappingTest$SomeClazz","v":1}]}"
* will work
*/
@Test
fun `boxWithInterface works nice`() {
val objectMapper = objectMapper()
val serialized = objectMapper.writeValueAsString(boxWithInterface)
val deserialized = objectMapper.readValue<SomeBox>(serialized)
println(serialized)
Assertions.assertEquals(boxWithInterface, deserialized)
}
/**
* [{"v":1}]
* will throw: Missing type id when trying to resolve subtype of [simple type, class ru.sport24.hub.refresher.message.BadMappingTest$SomeInterface]: missing type id property '@class'
at [Source: (String)"[{"v":1}]"; line: 1, column: 8] (through reference chain: java.util.ArrayList[0])
*/
@Test
fun `boxWithInterface-list doesnt work`() {
val objectMapper = objectMapper()
val serialized = objectMapper.writeValueAsString(boxWithInterface.items)
val deserialized = objectMapper.readValue<List<SomeInterface>>(serialized)
println(serialized)
Assertions.assertEquals(boxWithInterface.items, deserialized)
}
/**
* {"@class":"ru.sport24.hub.refresher.message.BadMappingTest$SomeClazz","v":1}
* will work
*/
@Test
fun `boxWithInterface-list-item works nice`() {
val objectMapper = objectMapper()
val serialized = objectMapper.writeValueAsString(boxWithInterface.items.first())
val deserialized = objectMapper.readValue<SomeInterface>(serialized)
println(serialized)
Assertions.assertEquals(boxWithInterface.items.first(), deserialized)
}
/*
PART 2: Class
why even om need @class when you deserialize implementation?
or
why om doesnt print @class when write list of implementations?
*/
data class OtherBox(
val items: List<SomeClass>
)
private val boxWithClass = OtherBox(listOf(SomeClass(1)))
/**
* {"items":[{"@class":"ru.sport24.hub.refresher.message.BadMappingTest$SomeClazz","v":1}]}"
* will work
*/
@Test
fun `boxWithClass works nice`() {
val objectMapper = objectMapper()
val serialized = objectMapper.writeValueAsString(boxWithClass)
val deserialized = objectMapper.readValue<OtherBox>(serialized)
println(serialized)
Assertions.assertEquals(boxWithClass, deserialized)
}
/**
* [{"v":1}]
* will throw: Missing type id when trying to resolve subtype of [simple type, class ru.sport24.hub.refresher.message.BadMappingTest$SomeInterface]: missing type id property '@class'
at [Source: (String)"[{"v":1}]"; line: 1, column: 8] (through reference chain: java.util.ArrayList[0])
*/
@Test
fun `boxWithClass-list doesnt work`() {
val objectMapper = objectMapper()
val serialized = objectMapper.writeValueAsString(boxWithClass.items)
val deserialized = objectMapper.readValue<List<SomeClass>>(serialized)
println(serialized)
Assertions.assertEquals(boxWithClass.items, deserialized)
}
/**
* {"@class":"ru.sport24.hub.refresher.message.BadMappingTest$SomeClazz","v":1}
* will work
*/
@Test
fun `boxWithClass-list-item works nice`() {
val objectMapper = objectMapper()
val serialized = objectMapper.writeValueAsString(boxWithClass.items.first())
val deserialized = objectMapper.readValue<SomeClass>(serialized)
println(serialized)
Assertions.assertEquals(boxWithClass.items.first(), deserialized)
}
/**
* {"@class":"ru.sport24.hub.refresher.message.BadMappingTest$SomeClazz","v":1}
* will throw Missing type id when trying to resolve subtype of [simple type, class ru.sport24.hub.refresher.message.BadMappingTest$SomeClass]: missing type id property '@class'
at [Source: (String)"{"v":1}"; line: 1, column: 7]
*/
@Test
fun `SomeClass without class doesnt work`() {
val objectMapper = objectMapper()
val deserialized = objectMapper.readValue<SomeClass>("{\"v\":1}")
Assertions.assertEquals(boxWithClass.items.first(), deserialized)
}
/*
PART 3: T
why om dont print @class here? even in "box-tests" like om did in part 1,2 and even(!) 4?
or
why SomeClass deeds @class?
*/
data class AlmostOtherBox<T>(
val items: List<T>
)
private val boxWithT = AlmostOtherBox<SomeClass>(listOf(SomeClass(1)))
/**
* {"items":[{"v":1}]}"
* will throw: Missing type id when trying to resolve subtype of [simple type, class ru.sport24.hub.refresher.message.BadMappingTest$SomeClass]: missing type id property '@class' (for POJO property 'items')
at [Source: (String)"{"items":[{"v":1}]}"; line: 1, column: 17] (through reference chain: ru.sport24.hub.refresher.message.BadMappingTest$AlmostOtherBox["items"]->java.util.ArrayList[0])
*/
@Test
fun `boxWithT doesnt work`() {
val objectMapper = objectMapper()
val serialized = objectMapper.writeValueAsString(boxWithT)
val deserialized = objectMapper.readValue<AlmostOtherBox<SomeClass>>(serialized)
println(serialized)
Assertions.assertEquals(boxWithT, deserialized)
}
/**
* [{"v":1}]
* will throw: Missing type id when trying to resolve subtype of [simple type, class ru.sport24.hub.refresher.message.BadMappingTest$SomeClass]: missing type id property '@class'
at [Source: (String)"[{"v":1}]"; line: 1, column: 8] (through reference chain: java.util.ArrayList[0])
*/
@Test
fun `boxWithT-list doesnt work`() {
val objectMapper = objectMapper()
val serialized = objectMapper.writeValueAsString(boxWithT.items)
val deserialized = objectMapper.readValue<List<SomeClass>>(serialized)
println(serialized)
Assertions.assertEquals(boxWithT.items, deserialized)
}
/**
* {"@class":"ru.sport24.hub.refresher.message.BadMappingTest$SomeClazz","v":1}
* will work
*/
@Test
fun `boxWithT-list-item works nice`() {
val objectMapper = objectMapper()
val serialized = objectMapper.writeValueAsString(boxWithT.items.first())
val deserialized = objectMapper.readValue<SomeClass>(serialized)
println(serialized)
Assertions.assertEquals(boxWithT.items.first(), deserialized)
}
/*
PART 4: Any
why om print @class for Any?
or
why om doest use @class if om already printed it?
*/
data class ThirdBox(
val items: List<Any>
)
private val boxWithAny = OtherBox(listOf(SomeClass(1)))
/**
* {"items":[{"@class":"ru.sport24.hub.refresher.message.BadMappingTest$SomeClass","v":1}]}
* will throw: : expected: <OtherBox(items=[SomeClass(v=1)])> but was: <ThirdBox(items=[{@class=ru.sport24.hub.refresher.message.BadMappingTest$SomeClass, v=1}])>
*/
@Test
fun `boxWithAny doesnt work`() {
val objectMapper = objectMapper()
val serialized = objectMapper.writeValueAsString(boxWithAny)
val deserialized = objectMapper.readValue<ThirdBox>(serialized)
println(serialized)
Assertions.assertEquals(boxWithAny, deserialized)
}
/**
* [{"v":1}]
* will throw: expected: <[SomeClass(v=1)]> but was: <[{v=1}]>
*/
@Test
fun `boxWithAny-list doesnt work`() {
val objectMapper = objectMapper()
val serialized = objectMapper.writeValueAsString(boxWithAny.items)
val deserialized = objectMapper.readValue<List<Any>>(serialized)
println(serialized)
Assertions.assertEquals(boxWithAny.items, deserialized)
}
/**
* {"@class":"ru.sport24.hub.refresher.message.BadMappingTest$SomeClazz","v":1}
* will throw: expected: <SomeClass(v=1)> but was: <{@class=ru.sport24.hub.refresher.message.BadMappingTest$SomeClass, v=1}>
*/
@Test
fun `boxWithAny-list-item doesnt work`() {
val objectMapper = objectMapper()
val serialized = objectMapper.writeValueAsString(boxWithAny.items.first())
val deserialized = objectMapper.readValue<Any>(serialized)
println(serialized)
Assertions.assertEquals(boxWithAny.items.first(), deserialized)
}
private fun objectMapper(): ObjectMapper {
val objectMapper = ObjectMapper()
objectMapper.registerModule(KotlinModule())
return objectMapper
}
}
Expected behavior
- serialized interface in any part should have @class
- deserialization of implementation (data class as minimal) shoulnt need @class
- if @class present - jackson should use it
Versions Kotlin: 1.4.0 Jackson-module-kotlin: 2.9.8 Jackson-databind: 2.9.8
Quick note: 2.9.8 is pretty old version so probably best to reproduce with 2.11.3.
there was a few mistakes in original message: some of bad tests were named "work nice" and ive wrote that 4 of them are good, but in fact there were 5 good. i fixed it and rewrited original message.
i had updated version to 2.11.3 and nothing was changed.
and as temporal fix of
- serialized interface in any part should have @Class
you sholdnt serialize
Collection<YourClass>
- useSomeWrapper(val yourCollection:Collection<YourClass>)
. important:TWrapper<T>(val yourCollection:Collection<T>)
will not fix this problem - deserialization of implementation (data class as minimal) shoulnt need @Class
your should annotate every your class that implementing anything with
@JsonTypeInfo( use = JsonTypeInfo.Id.CLASS, defaultImpl = SelfNameOfThisClass::class )
Is this fixed already?