Jackson:将JSON反序列化为Scala ADT

问题描述 投票:0回答:1

假设您有一个如下所示的JSON:

[{"foo": 1, "bar": 2}, {"foo": 3, "bar": {"baz": 4}}]

尝试使用Scala sum类型来表示它似乎很自然:

sealed trait Item
case class IntItem(foo: Int, bar: Int) extends Item
case class Baz(baz: Int)
case class BazItem(foo: Int, bar: Baz) extends Item

我的问题是:是否可以使用Jackson的Scala模块将上面的JSON序列化为List[Item]

我的尝试:

val string = "[{\"foo\": 1, \"bar\": 2}, {\"foo\": 3, \"bar\": {\"baz\": 4}}]"
val mapper = new ObjectMapper() with ScalaObjectMapper
mapper.registerModule(DefaultScalaModule)
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
mapper.readValue[List[Item]](string)

例外:

线程“main”中的异常com.fasterxml.jackson.databind.JsonMappingException:无法构造...的实例,问题:抽象类型需要映射到具体类型,具有自定义反序列化器,或者使用其他类型信息进行实例化在[来源:[{“foo”:1,“bar”:{“baz”:2}},{“foo”:3,“bar”:{“baz”:4}}]; line:1,column:2](通过引用链:com.fasterxml.jackson.module.scala.deser.BuilderWrapper [0])

这很清楚问题是什么,但我不确定如何最好地解决它。

scala jackson
1个回答
0
投票

正如@Dima指出的那样,我认为不存在涵盖所有案例的通用解决方案。此外,我不确定它是否可以存在,因为差异可能隐藏在任意深处,我怀疑有足够聪明的人可以从中创建一个halting problem。然而,许多具体案例都可以解决。

首先,如果您控制双方(序列化和反序列化),您应该考虑将JsonTypeIdResolver注释与一些TypeIdResolver子类一起使用,这些子类将该类型的名称放在JSON本身中。

如果您不能使用JsonTypeIdResolver,可能唯一的解决方案是推出您的自定义JsonDeserializer,如错误所示。您在问题中提供的示例可以通过以下方式处理:

sealed trait Item
case class IntItem(foo: Int, bar: Int) extends Item
case class Baz(baz: Int)
case class BazItem(foo: Int, bar: Baz) extends Item



import com.fasterxml.jackson.core._
import com.fasterxml.jackson.databind._
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.util.TokenBuffer
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.node._
import com.fasterxml.jackson.databind.exc._
import java.io.IOException

class ItemDeserializer() extends StdDeserializer[Item](classOf[Item]) {

  @throws[IOException]
  @throws[JsonProcessingException]
  def deserialize(jp: JsonParser, ctxt: DeserializationContext): Item = {
    // 1) Buffer current state of the JsonParser
    // 2) Use firstParser (from the buffer) to parser whole sub-tree into a generic JsonNode
    // 3) Analyze tree to find out the real type to be parser
    // 3) Using the buffer roll back history and create objectParser to parse the sub-tree as known type
    val tb = new TokenBuffer(jp, ctxt)
    tb.copyCurrentStructure(jp)

    val firstParser = tb.asParser
    firstParser.nextToken
    val curNode = firstParser.getCodec.readTree[JsonNode](firstParser)

    val objectParser = tb.asParser
    objectParser.nextToken()

    val bar = curNode.get("bar")
    if (bar.isInstanceOf[IntNode]) {
      objectParser.readValueAs[IntItem](classOf[IntItem])
    }
    else if (bar.isInstanceOf[ObjectNode]) {
      objectParser.readValueAs[BazItem](classOf[BazItem])
    }
    else {
      throw ctxt.reportBadDefinition[JsonMappingException](classOf[Item], "Unknown subtype of Item") // Jackson 2.9
      //throw InvalidDefinitionException.from(jp, "Unknown subtype of Item", ctxt.constructType(classOf[Item])) // Jackson 2.8
    }
  }
}

然后你可以使用它如下

def test() = {
  import com.fasterxml.jackson.module.scala._
  import com.fasterxml.jackson.module.scala.experimental._

  val mapper = new ObjectMapper() with ScalaObjectMapper
  mapper.registerModule(DefaultScalaModule)

  // add our custom ItemDeserializer
  val module = new SimpleModule
  module.addDeserializer(classOf[Item], new ItemDeserializer)
  mapper.registerModule(module)

  mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)

  val string = "[{\"foo\": 1, \"bar\": 2}, {\"foo\": 3, \"bar\": {\"baz\": 4}}]"

  val list = mapper.readValue[List[Item]](string)
  println(list.mkString(", "))
}

打印

银泰(1,2),巴西(h,基数(h))

ItemDeserializer的主要技巧是使用TokenBuffer来解析JSON两次:第一次分析JSON树并找出应该解析的类型,第二次实际解析已知类型的对象。

© www.soinside.com 2019 - 2024. All rights reserved.