FasterXML / FasterXML/jackson-module-kotlin

JsonTypeInfo + interface = serialization-deserialization problems

Open
#395 4 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
Kotlin
Stars
1.2k
Forks
187
Avg merge
9h 48m
Merged PRs (30d)
16

Description

**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**
```kotlin
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
)
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(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>(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(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
)
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(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>(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(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("{\"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(
val items: List
)
private val boxWithT = AlmostOtherBox(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>(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>(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(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
)
private val boxWithAny = OtherBox(listOf(SomeClass(1)))

/**
* {"items":[{"@class":"ru.sport24.hub.refresher.message.BadMappingTest$SomeClass","v":1}]}
* will throw: : expected: but was:
*/
@Test
fun `boxWithAny doesnt work`() {
val objectMapper = objectMapper()
val serialized = objectMapper.writeValueAsString(boxWithAny)
val deserialized = objectMapper.readValue(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>(serialized)
println(serialized)
Assertions.assertEquals(boxWithAny.items, deserialized)
}

/**
* {"@class":"ru.sport24.hub.refresher.message.BadMappingTest$SomeClazz","v":1}
* will throw: expected: 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(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

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.