Jackson:将JSON反序列化为Scala ADT

时间:2017-12-23 19:59:34

标签: scala jackson

假设您有一个如下所示的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])

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

1 个答案:

答案 0 :(得分:0)

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

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

如果您无法使用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(", "))
}

打印

  

IntItem(1,2),BazItem(3,Baz(4))

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