Kotlin 和 Jackson - 尝试解析简单类型的子类型时缺少类型 ID

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

我有一个密封的 Kotlin class - Pet 和两个子 classes - DogCat。我的应用程序需要传输在 JSON 中序列化的一组宠物。为了区分 subclasses 我使用 Jackson @JsonTypeInfo@JsonSubTypes 注释。列表如下:

@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()

单个实例正确序列化和反序列化:

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

序列化和反序列化宠物集合时出现问题:

    @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 在序列化期间吞下了类型 属性,因此 objectMapper 无法 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])

有什么办法可以解决这个问题吗?或者解决方法?

杰克逊版本:2.9.0

您应该做出的改变
首先,在 JsonTypeInfo 您需要将 visibility 设置为 true 然后 type 属性 在解串器中可用。

那么你需要实现 PetDeserializer

这里有一个例子:Pet.kt

    import com.fasterxml.jackson.annotation.JsonSubTypes
    import com.fasterxml.jackson.annotation.JsonTypeInfo
    import com.fasterxml.jackson.core.JsonParser
    import com.fasterxml.jackson.core.JsonProcessingException
    import com.fasterxml.jackson.databind.DeserializationContext
    import com.fasterxml.jackson.databind.JsonNode
    import com.fasterxml.jackson.databind.annotation.JsonDeserialize
    import com.fasterxml.jackson.databind.deser.std.StdDeserializer
    import java.io.IOException
    import kotlin.jvm.Throws
    
    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type",visible=true)
    @JsonSubTypes(
        JsonSubTypes.Type(value = Dog::class, name = "dog"),
        JsonSubTypes.Type(value = Cat::class, name = "cat")
    )
    
    
    @JsonDeserialize(using = PetDeserializer::class)
    sealed class Pet {
        abstract val name: String
    }
    
    data class Dog(override val name: String) : Pet()
    data class Cat(override val name: String) : Pet()
    
    class PetDeserializer @JvmOverloads constructor(vc: Class<*>? = Pet::class.java) :
        StdDeserializer<Pet?>(vc) {
        @Throws(IOException::class, JsonProcessingException::class)
        override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): Pet {
            val node = jp.codec
                .readTree<JsonNode>(jp)
            val itemName = node["name"]
                .asText()
            val type = node["type"]
                .asText()
            return when (type) {
                "dog" -> Dog(itemName)
                "cat" -> Cat(itemName)
                else -> throw Error("unknown type")
            }
        }
    
        companion object {
            private const val serialVersionUID = 1883547683050039861L
        }
    }

还有PetTest.kt

import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.Test
import org.skyscreamer.jsonassert.JSONAssert


class HelloTest {

    val objectMapper = ObjectMapper()

    @Test
    fun `serialize dog`() {
        val dog = Dog("Kevin")
        val dogJson = objectMapper.writeValueAsString(dog)
        JSONAssert.assertEquals("""{"type":"dog","name":"Kevin"}""", dogJson, false)
        val dogType = objectMapper.typeFactory.constructType(Dog::class.java)
        if (objectMapper.canDeserialize(dogType)) {
            ObjectMapper().readValue<Dog>(dogJson, Dog::class.java)
        } else {
              throw Error("deserializer not loaded")

        }
    }

}

Maven 依赖项:

        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.9.4</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-joda</artifactId>
            <version>2.9.4</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.9.4</version>
        </dependency>

        <dependency>
            <groupId>org.skyscreamer</groupId>
            <artifactId>jsonassert</artifactId>
            <version>1.5.0</version>
        </dependency>

这实际上不是一个错误,而是一个功能。对于具有泛型的集合,Jackson 将忽略您的子类型注释。这里有一个讨论:

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

以下 2 个“修复”对我有用,并且比上面的答案需要更少的设置(我认为我们可能使用不同的 jackson 版本,但我无法让 jackson 使用非默认构造函数一个子类,所以我用 lateinit)

重写了子类定义

此处提供一种克服此问题的方法

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
}

这些测试通过(同样,JSONAssert 对我来说似乎是不同的方法签名)

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

您也可以使用这种方法

您的代码如下所示:

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

以下测试通过

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