web-dev-qa-db-fra.com

Scala/Play: analyser JSON dans Map au lieu de JsObject

Sur la page d'accueil de Play Framework, ils affirment que "JSON est un citoyen de première classe". Je n'ai pas encore vu la preuve de cela. 

Dans mon projet, je traite de structures JSON assez complexes. Ceci est juste un exemple très simple:

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

Je comprends maintenant que Play utilise Jackson pour analyser JSON. J'utilise Jackson dans mes projets Java et je ferais quelque chose de simple comme ceci:

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

Cela analyserait joliment mon JSON dans un objet Map, ce que je veux - un mappage de chaînes et de paires d’objets et me permettrait facilement de convertir un tableau en ArrayList.

Le même exemple dans Scala/Play ressemblerait à ceci:

val obj: JsValue = Json.parse(jsonString)

Cela me donne plutôt une JsObjecttype propriétaire, ce qui n'est pas vraiment ce que je recherche.

Ma question est la suivante: puis-je analyser la chaîne JSON dans Scala/Play avec Map au lieu de JsObject aussi facilement que je le ferais en Java?

Question annexe: existe-t-il une raison pour laquelle JsObject est utilisé à la place de Map dans Scala/Play?

Ma pile: Play Framework 2.2.1/Scala 2.10.3/Java 8 64 bits/Ubuntu 13.10 64 bits

UPDATE: Je peux voir que la réponse de Travis est votée, donc je suppose que cela a du sens pour tout le monde, mais je ne vois toujours pas comment cela peut être appliqué pour résoudre mon problème. Disons que nous avons cet exemple (jsonString):

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

Eh bien, selon toutes les instructions, je devrais maintenant mettre dans tout ce passe-partout que je ne comprends pas par ailleurs, le but de:

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

Ca a l'air d'aller, hein? Mais attendez une minute, il y a un autre élément dans le tableau qui ruine totalement cette approche:

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

Le troisième élément ne correspond plus à ma classe de cas définie - je suis à nouveau à la case départ. Je suis capable d’utiliser tous les jours de telles structures JSON bien plus complexes en Java. Scala me suggère-t-il de simplifier mes JSON afin de s’adapter à sa politique de "sécurité du type"? Corrigez-moi si je me trompe, mais je pense que ce langage devrait servir les données, pas l'inverse?

UPDATE2: La solution consiste à utiliser le module de Jackson pour scala (exemple dans ma réponse).

23
Caballero

J'ai choisi d'utiliser le module Jackson pour scala .

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

En général, Scala déconseille l’utilisation du downcasting et Play Json est idiomatique à cet égard. Le downcasting est un problème car il est impossible pour le compilateur de vous aider à suivre la possibilité d'une entrée invalide ou d'autres erreurs. Une fois que vous avez une valeur de type Map[String, Any], vous êtes autonome: le compilateur ne peut pas vous aider à garder une trace de ce que ces valeurs Any pourraient être.

Vous avez deux alternatives. La première consiste à utiliser les opérateurs de chemin pour naviguer jusqu'à un point particulier de l'arborescence dont vous connaissez le type:

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,)

Cela ressemble à quelque chose comme ce qui suit:

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

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

Mais la première approche a l’avantage d’échouer de manière plus facile à raisonner. Au lieu d'une ClassCastExceptionexception potentiellement difficile à interpréter, nous obtiendrions simplement une Nice JsErrorvaleur.

Notez que nous pouvons valider un point plus haut dans l’arbre si nous savons quel type de structure nous attendons:

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

Ces deux exemples de Play reposent sur le concept de classes de type - et en particulier sur des instances de la classe de type Read fournie par Play. Vous pouvez également fournir vos propres instances de classe de types pour les types que vous avez définis vous-même. Cela vous permettrait de faire quelque chose comme ce qui suit:

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

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

Ou peu importe. La documentation de Play (lien ci-dessus) constitue une bonne introduction à la procédure à suivre. Vous pouvez toujours poser des questions complémentaires ici si vous rencontrez des problèmes.


Pour traiter la mise à jour de votre question, il est possible de modifier votre modèle afin de l'adapter aux différentes possibilités de key2, puis de définir votre propre instance Reads:

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

Qui fonctionne comme ceci:

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

Oui, c'est un peu plus verbeux, mais c'est une verbosité initiale que vous payez pour une fois (et cela vous donne quelques garanties de Nice), au lieu d'un tas de moulages qui peuvent entraîner des erreurs d'exécution déroutantes.

Ce n'est pas pour tout le monde, et ce peut ne pas être à votre goût - c'est parfaitement bien. Vous pouvez utiliser les opérateurs de chemin pour traiter des cas comme celui-ci, ou même le vieux Jackson pur. Je vous encourage cependant à donner une chance à l’approche des classes de types - il ya une courbe d’apprentissage abrupte, mais beaucoup de gens (y compris moi-même) le préfèrent très fortement.

31
Travis Brown

Pour plus de référence et dans un esprit de simplicité, vous pouvez toujours choisir:

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

Cependant, cela générera une exception pour les chaînes JSON ne correspondant pas au format (mais je suppose que cela vaut également pour l'approche de Jackson). La JsValue peut maintenant être traitée comme:

jsValueWhichBetterBeAList.as[List[JsValue]]

J'espère que la différence entre manipuler Objects et JsValues ne vous pose pas de problème (uniquement parce que vous vous plaigniez du fait que JsValues soit un droit de propriété). Évidemment, cela ressemble un peu à la programmation dynamique dans un langage dactylographié, ce qui n’est généralement pas la bonne solution (la réponse de Travis est généralement la meilleure solution), mais il est parfois agréable de l’avoir.

10
Dominik Bucher

Vous pouvez simplement extraire la valeur d'un Json et scala vous donne la carte correspondante . Exemple:

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

Supposons que vous avez le Json de type ci-dessus . Pour le convertir en carte, faites simplement:

var mapFromJson = myJson.value

Cela vous donne une carte de type: scala.collection.immutable.HashMap $ HashTrieMap

0

Je vous recommanderais de vous renseigner sur la correspondance des modèles et sur les ADT récursifs en général pour mieux comprendre pourquoi Play Json considère JSON comme un "citoyen de première classe". 

Cela étant dit, de nombreuses API de première génération Java (telles que les bibliothèques Java de Google) s’attendent à ce que JSON soit désérialisé en tant que Map[String, Object]. Bien que vous puissiez très simplement créer votre propre fonction générant récursivement cet objet avec une correspondance de modèle, la solution la plus simple consisterait probablement à utiliser le modèle existant suivant:

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)

LinkedHashMap est utilisé si vous souhaitez conserver l'ordre des clés au moment de la désérialisation (un HashMap peut être utilisé si la commande n'a pas d'importance). Exemple complet ici .

0
Matt