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

JsonTypeInfo + interface = serialization-deserialization problems

Open graf8787 opened this issue 4 years ago • 4 comments

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

  1. serialized interface in any part should have @class
  2. deserialization of implementation (data class as minimal) shoulnt need @class
  3. if @class present - jackson should use it

Versions Kotlin: 1.4.0 Jackson-module-kotlin: 2.9.8 Jackson-databind: 2.9.8

graf8787 avatar Nov 11 '20 15:11 graf8787

Quick note: 2.9.8 is pretty old version so probably best to reproduce with 2.11.3.

cowtowncoder avatar Nov 11 '20 17:11 cowtowncoder

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.

graf8787 avatar Nov 12 '20 05:11 graf8787

and as temporal fix of

  1. serialized interface in any part should have @Class you sholdnt serialize Collection<YourClass> - use SomeWrapper(val yourCollection:Collection<YourClass>). important: TWrapper<T>(val yourCollection:Collection<T>) will not fix this problem
  2. 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 )

graf8787 avatar Nov 12 '20 05:11 graf8787

Is this fixed already?

xiaoyunwu avatar Feb 20 '22 02:02 xiaoyunwu