【问题标题】:Kotlin and Jackson - Missing type id when trying to resolve subtype of simple typeKotlin 和 Jackson - 尝试解析简单类型的子类型时缺少类型 ID
【发布时间】:2021-02-19 17:57:51
【问题描述】:

我有一个 Kotlin 密封类 - Pet 和两个子类 - DogCat。我的应用程序需要传输一组以 JSON 序列化的宠物。为了区分子类,我使用 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 在序列化过程中吞下了 type 属性,因此 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

【问题讨论】:

    标签: java json kotlin jackson


    【解决方案1】:

    你应该做出的改变
    首先,在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>
    

    【讨论】:

      【解决方案2】:

      这实际上不是一个错误,而是一个特性。对于具有泛型的集合,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)
          }
      }
      

      您也可以使用这种方法Workaround without custom serializers/deserializers

      您的代码如下所示:

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

      【讨论】:

      • 我认为说它是一项功能只是懒惰。序列化器可以轻松地使用父 ObjectMapper 来解析编解码器...实际上应该更容易实现,甚至更高效。
      猜你喜欢
      • 1970-01-01
      • 2020-04-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-12-23
      • 1970-01-01
      • 2021-12-30
      • 1970-01-01
      相关资源
      最近更新 更多