web-dev-qa-db-ja.com

Scala / Play:JSONをJsObjectではなくMapに解析します

Play Frameworkのホームページでは、「JSONは一流の市民です」と主張しています。その証拠はまだ見ていません。

私のプロジェクトでは、かなり複雑なJSON構造を扱っています。これは非常に単純な例です。

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

PlayがJSONの解析にJacksonを使用していることを理解しました。私はJavaプロジェクトでJacksonを使用しており、次のような簡単なことを行います。

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

これは、JSONを適切にマップオブジェクトに解析します。これは、文字列とオブジェクトのペアのマップであり、配列をArrayListに簡単にキャストできます。

Scala/Playの同じ例は次のようになります。

val obj: JsValue = Json.parse(jsonString)

代わりに、独自のJsObjecttype が得られますが、これは本当に私が望んでいるものではありません。

私の質問は、Scala/PlayでJSON文字列をMapの代わりにJsObjectではなくJavaで行うのと同じくらい簡単に解析できますか?

副次的な質問:Scala/PlayでJsObjectの代わりにMapが使用される理由はありますか?

私のスタック:Play Framework 2.2.1/Scala 2.10.3/Java 8 64bit/Ubuntu 13.10 64bit

UPDATE:Travisの答えは賛成であることがわかりますので、誰にとっても理にかなっていると思いますが、それがどのように適用できるかまだわかりません私の問題を解決するために。この例(jsonString)があるとします:

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

さて、すべての指示に従って、私は今では私がそうでなければ目的を理解していないすべてのその定型句を入れる必要があります:

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

良さそうですね。しかし、ちょっと待ってください。このアプローチを完全に台無しにする別の要素が配列にあります。

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

3番目の要素は、定義済みのケースクラスと一致しなくなりました-再び正方形になりました。 Java daily、does Scalaは、JSONを "typeに合わせて単純化することを提案します。私は間違っている場合は修正しますが、その言語はデータを提供すべきですが、その逆ではありませんか?

UPDATE2:解決策は、scala(私の答えの例)にJacksonモジュールを使用することです。

23
Caballero

scalaのJacksonモジュール を使用することにしました。

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

一般的にScalaはダウンキャストの使用を推奨しません。PlayJsonはこの点で慣用的です。ダウンキャストは問題です。コンパイラが無効な入力やその他のエラーの可能性を追跡するのを支援できないためです。タイプMap[String, Any]の値を取得したら、自分で実行します。コンパイラは、これらのAny値が何であるかを追跡することはできません。

いくつかの選択肢があります。 1つ目は、パス演算子を使用して、タイプがわかっているツリー内の特定のポイントに移動することです。

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

これは、次のようなものに似ています。

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

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

しかし、前者のアプローチには、推論しやすい方法で失敗するという利点があります。解釈が難しい可能性のあるClassCastExceptionexceptionの代わりに、Nice JsErrorvalue

予想される構造の種類がわかっている場合は、ツリーの上位で検証できることに注意してください。

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

これらのPlayの例は両方とも type classes —の概念に基づいて構築されており、特にRead Playが提供する型クラス。また、自分で定義した型の独自の型クラスインスタンスを提供することもできます。これにより、次のようなことができます。

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

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

または何でも。 Playのドキュメント(上記にリンク)は、これを実行する方法の良い紹介を提供します。問題が発生した場合は、ここでいつでもフォローアップの質問をすることができます。


質問の更新に対処するために、key2のさまざまな可能性に対応するようにモデルを変更し、独自の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(_, _))
}

これは次のように機能します:

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

はい、これはもう少し冗長ですが、実行時エラーを混乱させる可能性のあるキャストの代わりに、一度支払うだけで(そしてニースの保証を提供します)事前の冗​​長性です。

それは皆のためではなく、あなたの好みに合わないかもしれません-それは完全に大丈夫です。パス演算子を使用して、このようなケースを処理したり、単純な古いジャクソンを処理することもできます。ただし、型クラスのアプローチにチャンスを与えることをお勧めします。急な学習曲線がありますが、多くの人々(私を含む)が非常に強くそれを好んでいます。

31
Travis Brown

さらに参照し、シンプルさの精神で、あなたはいつでも行くことができます:

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

ただし、これは、形式に対応しないJSON文字列に対して例外をスローします(ただし、Jacksonアプローチにも当てはまると思います)。 JsValueは、次のようにさらに処理できるようになりました。

jsValueWhichBetterBeAList.as[List[JsValue]]

ObjectsとJsValuesの処理の違いが問題にならないことを願っています(JsValuesがプロプライエタリであると不平を言っていたからです)。明らかに、これは型付き言語の動的プログラミングに少し似ており、通常は進むべき道ではありません(Travisの答えは通常行くべき道です)が、時にはそれがいいと思います。

11
Dominik Bucher

Jsonの値を抽出するだけで、scalaは対応するマップを提供します。例:

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

上記のタイプのJsonがあるとします。マップに変換するには、次のようにします。

var mapFromJson = myJson.value

これにより、タイプのマップが得られます。scala.collection.immutable.HashMap $ HashTrieMap

0

Play JsonがJSONを「ファーストクラスの市民」として扱う理由をよりよく理解するために、一般にパターンマッチングと再帰ADTを読むことをお勧めします。

そうは言っても、多くのJavaファーストAPI(Google Javaライブラリなど)は、JSONがMap[String, Object]。パターンマッチングを使用してこのオブジェクトを再帰的に生成する独自の関数を非常に簡単に作成できますが、おそらく最も簡単な解決策は次の既存のパターンを使用することです。

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を使用します(順序が重要でない場合は、HashMapを使用できます)。完全な例 こちら

0
Matt