Kotlin and Jackson - Missing type id when trying to resolve subtype of simple type

yanefedor

I have a Kotlin sealed class - Pet and two subclasses - Dog and Cat. My application requires to transfer a collection of pets serialized in JSON. In order to differentiate subclasses I use Jackson @JsonTypeInfo and @JsonSubTypes annotations. The listing below:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes(
    JsonSubTypes.Type(value = Dog::class, name = "dog"),
    JsonSubTypes.Type(value = Cat::class, name = "cat")
)
sealed class Pet { abstract val name: String }

data class Dog(override val name: String): Pet()
data class Cat(override val name: String): Pet()

Single instances are serialized and deserialized properly:

    @Test
    fun `serialize dog`() {
        val dog = Dog("Kevin")
        val dogJson = objectMapper.writeValueAsString(dog)

        JsonAssert.assertEquals(dogJson, """{"type":"dog","name":"Kevin"}""")
        val newDog = objectMapper.readValue<Dog>(dogJson)
    }

The problem comes up when a collection of pets is being serialized and deserialized:

    @Test
    fun `serialize dog and cat`() {
        val pets: Set<Pet> = setOf(Dog("Kevin"), Cat("Marta"))
        val petsJson = objectMapper.writeValueAsString(pets)

        JsonAssert.assertEquals(petsJson, """[{"name":"Kevin"},{"name":"Marta"}]""")
        val newPets = objectMapper.readValue<Set<Pet>>(petsJson)
    }

Jackson swallows the type property during serialization and because of that objectMapper is not able to readValue:

com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class s3.moria.workflows.common.model.Pet]: missing type id property 'type'
 at [Source: (String)"[{"name":"Kevin"},{"name":"Marta"}]"; line: 1, column: 17] (through reference chain: java.util.HashSet[0])

Any ideas how tackle this problem? Or workarounds?

Jackson version: 2.9.0

Vlad L

This is actually not a bug, but a feature. For collections with generics, Jackson will ignore your subtypes annotations. There is a discussion here about it:

https://github.com/FasterXML/jackson-databind/issues/1816

The following 2 "fixes" work for me, and require less setup than the answer above (I think we are using different jackson versions perhaps, but I couldn't get jackson to work with a non-default constructor for a subclass, so I rewrote the subclass definition with lateinit)

One approach to overcome this is here:

Create your own set writer

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes(
    JsonSubTypes.Type(value = Dog1::class, name = "dog"),
    JsonSubTypes.Type(value = Cat1::class, name = "cat")
)
sealed class Pet1 {
    abstract val name: String
}

class Dog1 : Pet1() {
    override lateinit var name: String
}

class Cat1 : Pet1() {
    override lateinit var name: String
}

These tests pass (again JSONAssert seems to be of a different method signature for me)

package com.example.demo

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.junit.jupiter.api.Test
import org.skyscreamer.jsonassert.JSONAssert

internal class PetTest1 {

    private var objectMapper = ObjectMapper()

    @Test
    fun `serialize dog`() {
        val dog = Dog1()
        dog.name = "Kevin"
        val dogJson = objectMapper.writeValueAsString(dog)

        JSONAssert.assertEquals(dogJson, """{"type":"dog","name":"Kevin"}""", true)
        val newDog = objectMapper.readValue<Dog1>(dogJson)
    }

    @Test
    fun `serialize dog and cat with mapper`() {
        val dog = Dog1()
        dog.name = "Kevin"
        val cat = Cat1()
        cat.name = "Marta"
        val pets: Set<Pet1> = setOf(dog, cat)
        val petCollectionType = objectMapper.typeFactory
            .constructCollectionType(Set::class.java, Pet1::class.java)

        val petsJson = objectMapper.writer().forType(petCollectionType).writeValueAsString(pets)

        JSONAssert.assertEquals(
            petsJson, """[{"type":"dog","name":"Kevin"},{"type":"cat","name":"Marta"}]""", true
        )
        val newPets = objectMapper.readValue<Set<Pet1>>(petsJson)
    }
}

You can also use this approach: Workaround without custom serializers/deserializers

Your code would look like:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY)
@JsonSubTypes(
    JsonSubTypes.Type(value = Dog::class, name = "dog"),
    JsonSubTypes.Type(value = Cat::class, name = "cat")
)
sealed class Pet {
    abstract val jacksonMarker: String
        @JsonProperty("@type")
        get
    abstract val name: String
}

class Dog : Pet() {
    override val jacksonMarker: String
        get() = "dog"
    override lateinit var name: String
}

class Cat : Pet() {
    override val jacksonMarker: String
        get() =  "cat"
    override lateinit var name: String
}

The following tests pass

internal class PetTest {

    private var objectMapper = ObjectMapper()

    @Test
    fun `serialize dog`() {
        val dog = Dog()
        dog.name = "Kevin"
        val dogJson = objectMapper.writeValueAsString(dog)

        JSONAssert.assertEquals(dogJson, """{"@type":"dog","name":"Kevin"}""", true)
        val newDog = objectMapper.readValue<Dog>(dogJson)
    }

    @Test
    fun `serialize dog and cat`() {
        val dog = Dog()
        dog.name = "Kevin"
        val cat = Cat()
        cat.name = "Marta"
        val pets: Set<Pet> = setOf(dog, cat)
        val petsJson = objectMapper.writeValueAsString(pets)

        JSONAssert.assertEquals(
            petsJson, """[{"@type":"dog","name":"Kevin"},{"@type":"cat","name":"Marta"}]""", true)
        val newPets = objectMapper.readValue<Set<Pet>>(petsJson)
    }
}

Collected from the Internet

Please contact javaer1[email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related

@JsonTypeName not working and return Missing type id when trying to resolve subtype

How to identify the missing type id in Jackson error?

Enable basic security with error could not resolve type id 'basic' into a subtype

Missing type id when trying to resolve subtype of simple type, missing type id property type

Generically subtype a type parameter of a generic method when used by a subtype

ActiveMq throws MessageConversionException: Failed to resolve type id when sending an object

How to resolve 'missing type' errors in XAML?

Jackson 3.1.1 Missing Type ID of Subtype Exception for Polymorphic Type of List?

'List<dynamic>' is not a subtype of type 'List<Widget>' when trying to display a list of items

Kotlin - Type of `if` and `when` Expressions

How do I resolve type 'Timestamp' is not a subtype of type 'String' in type cast

type 'FirebaseFirestore' is not a subtype of type 'Firestore'

How do you resolve error "type 'Session' is not a subtype of type 'Map<String, dynamic>' when updating a Sqlite db with an object?

Android | Kotlin : Return type of 'onCreateDialog' is not a subtype of the return type of the overridden member

How to create a class with a method that returns a subtype with a type parameter in Kotlin?

type '_Type' is not a subtype of type 'Widget?'

A missing or empty content type header was found when trying to read a message. The content type header is required

Android Kotlin error : "return type is 'unit' which is not a subtype of overridden"

How to resolve "Unhandled Exception: type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'List<DocumentSnapshot>' in type cast"?

"com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id" when trying to deserialise subclass

When i am trying to fetch api this error : "type 'List<dynamic>' is not a subtype of type 'Map<String, dynamic>" shows up

type 'text' is not a subtype of type 'string'

Kotlin Jackson "missing type id property 'type' (for POJO property 'data');"

Flutter: Unhandled Exception: type 'Null' is not a subtype of type 'String' when trying to run app

type 'Null' is not a subtype of type 'String' this error pops up when trying to access data from map

How to resolve type in Kotlin when value is Any?

type 'Null' is not a subtype of type 'String' when get data

I got an error when trying to get data from the API error type 'null' is not a subtype of type 'string'

Getting type 'Null' is not a subtype of type 'String' error when using BlocBuilder