wake-up-neo.net

Scala/Play: Parsen Sie JSON in Map anstelle von JsObject

Auf der Startseite von Play Framework behaupten sie, "JSON ist ein erstklassiger Bürger". Den Beweis dafür habe ich noch nicht gesehen. 

In meinem Projekt beschäftige ich mich mit ziemlich komplexen JSON-Strukturen. Dies ist nur ein sehr einfaches Beispiel:

{
    "key1": {
        "subkey1": {
            "k1": "value1"
            "k2": [
                "val1",
                "val2"
                "val3"
            ]
        }
    }
    "key2": [
        {
            "j1": "v1",
            "j2": "v2"
        },
        {
            "j1": "x1",
            "j2": "x2"
        }
    ]
}

Jetzt verstehe ich, dass Play Jackson zum Analysieren von JSON verwendet. Ich verwende Jackson in meinen Java-Projekten und würde so etwas ganz einfaches machen:

ObjectMapper mapper = new ObjectMapper();
Map<String, Object> obj = mapper.readValue(jsonString, Map.class);

Dies würde mein JSON in Map-Objekten schön parsen, was ich will - Map von String- und Objektpaaren und würde mir leicht erlauben, ein Array in ArrayList umzuwandeln.

Das gleiche Beispiel in Scala/Play würde so aussehen:

val obj: JsValue = Json.parse(jsonString)

Dies gibt mir stattdessen eine proprietäre JsObjecttype , die ich eigentlich nicht habe.

Meine Frage ist: Kann ich den JSON-String in Scala/Play in Map statt in JsObject genauso einfach analysieren wie in Java?

Nebenfrage: Gibt es einen Grund, warum JsObject anstelle von Map in Scala/Play verwendet wird?

Mein Stack: Play Framework 2.2.1/Scala 2.10.3/Java 8 64bit/Ubuntu 13.10 64bit

UPDATE: Ich kann sehen, dass die Antwort von Travis positiv ist, also denke ich, dass es für jeden sinnvoll ist, aber ich sehe immer noch nicht, wie sich das auf mein Problem anwenden lässt. Sagen wir, wir haben dieses Beispiel (jsonString):

[
    {
        "key1": "v1",
        "key2": "v2"
    },
    {
        "key1": "x1",
        "key2": "x2"
    }
]

Nun, nach allen Anweisungen, sollte ich jetzt all diese Kesselplatten einfügen, die ich sonst nicht verstehe:

case class MyJson(key1: String, key2: String)
implicit val MyJsonReads = Json.reads[MyJson]
val result = Json.parse(jsonString).as[List[MyJson]]

Sieht gut aus, huh? Aber warten Sie eine Minute, da kommt ein anderes Element in das Array, das diesen Ansatz völlig ruiniert:

[
    {
        "key1": "v1",
        "key2": "v2"
    },
    {
        "key1": "x1",
        "key2": "x2"
    },
    {
        "key1": "y1",
        "key2": {
            "subkey1": "subval1",
            "subkey2": "subval2"
        }
    }
]

Das dritte Element passt nicht mehr zu meiner definierten Fallklasse - ich bin wieder bei Platz eins. Ich kann solche und viel kompliziertere JSON-Strukturen in Java jeden Tag verwenden. Schlägt Scala vor, dass ich meine JSONs vereinfachen sollte, damit sie den "typsicheren" Richtlinien entsprechen? Korrigieren Sie mich, wenn ich falsch liege, aber ich denke, dass diese Sprache den Daten dienen sollte und nicht umgekehrt?

UPDATE2: Die Lösung besteht darin, das Jackson-Modul für Scala zu verwenden (Beispiel in meiner Antwort).

23
Caballero

Ich habe mich für das Jackson-Modul für Scala entschieden.

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper

val mapper = new ObjectMapper() with ScalaObjectMapper
mapper.registerModule(DefaultScalaModule)
val obj = mapper.readValue[Map[String, Object]](jsonString)
25
Caballero

Scala rät generell davon ab, Downcasting einzusetzen, und Play Json ist in dieser Hinsicht idiomatisch. Downcasting ist ein Problem, da der Compiler Ihnen nicht dabei helfen kann, die Möglichkeit von ungültigen Eingaben oder anderen Fehlern zu ermitteln. Wenn Sie einen Wert vom Typ Map[String, Any] haben, sind Sie alleine - der Compiler kann Ihnen nicht helfen, den Überblick über die möglichen Any-Werte zu behalten.

Sie haben ein paar Alternativen. Die erste besteht darin, die Pfadoperatoren zu verwenden, um zu einem bestimmten Punkt in der Baumstruktur zu navigieren, an dem Sie den Typ kennen:

scala> val json = Json.parse(jsonString)
json: play.api.libs.json.JsValue = {"key1": ...

scala> val k1Value = (json \ "key1" \ "subkey1" \ "k1").validate[String]
k1Value: play.api.libs.json.JsResult[String] = JsSuccess(value1,)

Dies ähnelt etwa dem Folgenden:

val json: Map[String, Any] = ???

val k1Value = json("key1")
  .asInstanceOf[Map[String, Any]]("subkey1")
  .asInstanceOf[Map[String, String]]("k1")

Der erste Ansatz hat jedoch den Vorteil, dass er auf eine Art und Weise versagt, die einfacher zu verstehen ist. Anstelle einer möglicherweise schwer zu interpretierenden ClassCastExceptionexception würden wir nur eine Nice JsErrorvalue erhalten.

Beachten Sie, dass wir an einem höheren Punkt im Baum validieren können, wenn wir wissen, welche Art von Struktur wir erwarten:

scala> println((json \ "key2").validate[List[Map[String, String]]])
JsSuccess(List(Map(j1 -> v1, j2 -> v2), Map(j1 -> x1, j2 -> x2)),)

Diese beiden Play-Beispiele basieren auf dem Konzept der type-Klassen - und insbesondere auf Instanzen der von Play bereitgestellten Read-Klasse. Sie können auch eigene Typklasseninstanzen für von Ihnen selbst definierte Typen bereitstellen. Auf diese Weise können Sie etwa Folgendes tun:

val myObj = json.validate[MyObj].getOrElse(someDefaultValue)

val something = myObj.key1.subkey1.k2(2)

Oder Wasauchimmer. In der Play-Dokumentation (oben verlinkt) finden Sie eine gute Einführung, und Sie können hier immer noch Fragen stellen, wenn Sie Probleme haben.


Um das Update in Ihrer Frage anzusprechen, können Sie Ihr Modell an die verschiedenen Möglichkeiten von key2 anpassen und dann Ihre eigene Reads-Instanz definieren:

case class MyJson(key1: String, key2: Either[String, Map[String, String]])

implicit val MyJsonReads: Reads[MyJson] = {
  val key2Reads: Reads[Either[String, Map[String, String]]] =
    (__ \ "key2").read[String].map(Left(_)) or
    (__ \ "key2").read[Map[String, String]].map(Right(_))

  ((__ \ "key1").read[String] and key2Reads)(MyJson(_, _))
}

Was funktioniert so:

scala> Json.parse(jsonString).as[List[MyJson]].foreach(println)
MyJson(v1,Left(v2))
MyJson(x1,Left(x2))
MyJson(y1,Right(Map(subkey1 -> subval1, subkey2 -> subval2)))

Ja, dies ist ein wenig ausführlicher, aber es ist eine ausführliche Offenheit, für die Sie einmal zahlen (und das gibt Ihnen einige Garantien von Nice), anstatt einer Reihe von Casts, die zu verwirrenden Laufzeitfehlern führen können.

Es ist nicht jedermanns Sache, und es mag nicht Ihrem Geschmack entsprechen - das ist vollkommen in Ordnung. Sie können die Pfadoperatoren verwenden, um Fälle wie diesen oder sogar den alten Jackson zu behandeln. Ich würde Sie jedoch dazu ermutigen, dem Typ-Klassen-Ansatz eine Chance zu geben - es gibt eine steile Lernkurve, aber viele Leute (einschließlich ich selbst) bevorzugen es sehr.

31
Travis Brown

Für weitere Informationen und im Geiste der Einfachheit können Sie sich immer für Folgendes entscheiden:

Json.parse(jsonString).as[Map[String, JsValue]]

Dies führt jedoch zu einer Ausnahme für JSON-Zeichenfolgen, die nicht dem Format entsprechen (aber ich gehe davon aus, dass dies auch für den Jackson-Ansatz gilt). Die JsValue kann nun wie folgt weiter verarbeitet werden:

jsValueWhichBetterBeAList.as[List[JsValue]]

Ich hoffe, der Unterschied zwischen dem Umgang mit Objects und JsValues ist für Sie kein Problem (nur weil Sie beschwert haben, dass JsValues proprietär ist). Offensichtlich ist dies ein bisschen wie dynamisches Programmieren in einer typisierten Sprache, was normalerweise nicht der richtige Weg ist (die Antwort von Travis ist normalerweise der richtige Weg), aber manchmal ist das nett, denke ich.

10
Dominik Bucher

Sie können einfach den Wert eines Json extrahieren, und Scala gibt Ihnen die entsprechende Karte .____ Beispiel:

   var myJson = Json.obj(
          "customerId" -> "xyz",
          "addressId" -> "xyz",
          "firstName" -> "xyz",
          "lastName" -> "xyz",
          "address" -> "xyz"
      )

Angenommen, Sie haben den Json des obigen Typs . Um ihn in eine Map umzuwandeln, führen Sie einfach Folgendes aus:

var mapFromJson = myJson.value

Dies gibt Ihnen eine Karte vom Typ: scala.collection.immutable.HashMap $ HashTrieMap

0

Ich würde empfehlen, sich mit Pattern-Matching und rekursiven ADTs im Allgemeinen vertraut zu machen, um besser zu verstehen, warum Play Json JSON als "erstklassigen Bürger" behandelt. 

Davon abgesehen, erwarten viele Java-First-APIs (wie Google Java-Bibliotheken), dass JSON als Map[String, Object] deserialisiert wird. Sie können zwar ganz einfach eine eigene Funktion erstellen, die dieses Objekt rekursiv mit Mustervergleich erzeugt, die einfachste Lösung wäre jedoch die Verwendung des folgenden vorhandenen Musters:

import com.google.gson.Gson
import Java.util.{Map => JMap, LinkedHashMap}

val gson = new Gson()

def decode(encoded: String): JMap[String, Object] =   
   gson.fromJson(encoded, (new LinkedHashMap[String, Object]()).getClass)

Die LinkedHashMap wird verwendet, wenn Sie die Schlüsselreihenfolge zum Zeitpunkt der Deserialisierung beibehalten möchten (eine HashMap kann verwendet werden, wenn die Reihenfolge keine Rolle spielt). Vollständiges Beispiel hier .

0
Matt