Scala/Play:将JSON解析为Map而不是JsObject



在Play Framework的主页上,他们声称"JSON是一等公民"。我还没有看到证据。

在我的项目中,我正在处理一些非常复杂的JSON结构。这只是一个非常简单的例子:

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

现在我明白了Play正在使用Jackson来解析JSON。我在Java项目中使用Jackson,我会做一些简单的事情,这样:

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

这将很好地将我的 JSON 解析为我想要的 Map 对象 - 字符串和对象对的映射,并允许我轻松地将数组转换为ArrayList

Scala/Play 中的相同示例如下所示:

val obj: JsValue = Json.parse(jsonString)

相反,这给了我一个专有的JsObject类型,这不是我真正想要的。

我的问题是:我可以在 Scala/Play 中解析 JSON 字符串以Map而不是像在 Java 中那样轻松地JsObject吗?

附带问题:在 Scala/Play 中使用 JsObject 而不是Map是否有理由?

我的堆栈: Play Framework 2.2.1/Scala 2.10.3/Java 8 64bit/Ubuntu 13.10 64bit

更新:我可以看到特拉维斯的答案是赞成的,所以我想这对每个人都有意义,但我仍然看不出如何将其应用于解决我的问题。假设我们有这个例子(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"
        }
    }
]

第三个元素不再与我定义的案例类匹配 - 我又回到了原点。我每天都能在 Java 中使用这样更复杂的 JSON 结构,Scala 是否建议我应该简化我的 JSON 以适应它的"类型安全"策略?如果我错了,请纠正我,但我认为该语言应该为数据服务,而不是相反?

UPDATE2:解决方案是使用 Jackson 模块进行 scala(我回答中的示例(。

Scala 通常不鼓励使用向下转换,而 Play Json 在这方面是惯用的。向下转换是一个问题,因为它使编译器无法帮助您跟踪无效输入或其他错误的可能性。获得类型 Map[String, Any] 的值后,您只能靠自己 - 编译器无法帮助您跟踪这些Any值可能是什么。

您有几个选择。第一种是使用路径运算符导航到树中您知道类型的特定点:

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

但前一种方法的优点是以更容易推理的方式失败。而不是一个可能难以解释的ClassCastException例外,我们只会得到一个不错的JsError

请注意,如果我们知道我们期望什么样的结构,我们可以在树中的更高点进行验证:

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

这两个 Play 示例都基于类型类的概念构建,特别是基于 Play 提供的 Read 类型类的实例。还可以为自己定义的类型提供自己的类型类实例。这将允许您执行以下操作:

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

是的,这有点冗长,但这是预先冗长,您只需支付一次费用(这为您提供了一些很好的保证(,而不是一堆可能导致令人困惑的运行时错误的转换。

它并不适合所有人,也可能不符合您的口味——这完全没问题。您可以使用路径运算符来处理这样的情况,甚至是普通的旧杰克逊。不过,我鼓励你给类型类方法一个机会——有一个陡峭的学习曲线,但很多人(包括我自己(非常喜欢它。

我选择使用Jackson模块进行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)

为了进一步参考并本着简单的精神,您可以随时选择:

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

但是,这将为与格式不对应的 JSON 字符串引发异常(但我认为这也适用于杰克逊方法(。现在可以进一步处理JsValue,如下所示:

jsValueWhichBetterBeAList.as[List[JsValue]]

我希望处理Object s和JsValue s之间的区别对你来说不是问题(只是因为你抱怨JsValue是专有的(。显然,这有点像类型语言的动态编程,这通常不是要走的路(Travis的答案通常是要走的路(,但有时我猜这很好。

你可以简单地提取出一个 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

建议阅读模式匹配和递归ADT,以更好地理解为什么Play Json将JSON视为"一等公民"。

话虽如此,许多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(。完整示例在这里。

最新更新