2017-12-23 5 views
0

Angenommen, Sie eine JSON haben, die wie folgt aussieht:Jackson: deserialisiert JSON Scala ADT

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

Es ist natürlich zu versuchen, scheint dies mit einer Scala Summe Typ darstellen:

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 

Meine Frage ist: Ist es möglich, Jacksons Scala-Modul zu verwenden, um den obigen JSON in eine List[Item] zu serialisieren?

Mein Versuch:

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) 

Die Ausnahme:

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: konstruieren kann nicht Instanz ... Artikel, Problem : abstrakte Typen müssen entweder zu konkreten Typen gemappt werden, einen benutzerdefinierten Deserializer haben oder mit zusätzlichen Typinformationen um [Source: [{"foo": 1, "bar": {"baz": 2}}, { "foo": 3, "bar": {"baz": 4}}]; Zeile: 1, Spalte: 2] (durch Referenzkette: com.fasterxml.jackson.module.scala.deser.BuilderWrapper [0])

, dass es ziemlich klar, was das Problem macht, ist, aber ich bin Ich bin mir nicht sicher, wie ich es am besten beheben kann.

+1

Ich füge nur hinzu, wenn Sie '" bar ": 2' mit' "bar": {"baz": 2} 'und' List [Element] 'mit' List [BazItem] 'dann alles funktioniert Gut, es ist wirklich ein Problem mit ADT's. –

+2

Ich frage mich nur ... wenn, sagen wir mal, du würdest Jackson implementieren, wie würdest du darüber raten, in welchen Typ jedes der Listenelemente geparst werden soll. – Dima

Antwort

0

Wie @Dima darauf hingewiesen hat, glaube ich nicht, dass es eine generische Lösung gibt, die alle Fälle abdeckt. Außerdem bin ich nicht sicher, ob es überhaupt existieren kann, weil der Unterschied beliebig tief verborgen sein kann und ich vermute, dass jemand, der schlau genug ist, daraus einen halting problem erstellen kann. Es können jedoch viele spezifische Fälle gelöst werden.

Vor allem, wenn Sie beiden Seiten (Serialisierung und Deserialisierung) steuern, sollten Sie die Verwendung JsonTypeIdResolver Anmerkung mit einigen TypeIdResolver Subklassen berücksichtigen, die Namen des Typs im JSON selbst setzen.

Wenn Sie nicht verwenden können, ist wahrscheinlich die einzige Lösung, Ihre benutzerdefinierte auszurollen, wie der Fehler vorschlägt. Das Beispiel, das Sie in Ihrer Frage bereitgestellt wird, kann durch so etwas wie diese behandelt werden:

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

und dann können Sie es wie folgt

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(", ")) 
} 

die

intitem druckt (1,2), BazItem (3, Baz (4))

Der Haupttrick in der ItemDeserializer soll TokenBuffer verwenden, um JSON zweimal zu analysieren: erstens, um den JSON-Baum zu analysieren und herauszufinden, welcher Typ analysiert werden soll, zum zweiten Mal, um das Objekt eines bekannten Typs tatsächlich zu analysieren.