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)
    }
}

CLICK HERE to find out more related problems solutions.

Leave a Comment

Your email address will not be published.

Scroll to Top