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

Cab*_*ero 23 java json scala playframework playframework-2.0

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

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

{
    "key1": {
        "subkey1": {
            "k1": "value1"
            "k2": [
                "val1",
                "val2"
                "val3"
            ]
        }
    }
    "key2": [
        {
            "j1": "v1",
            "j2": "v2"
        },
        {
            "j1": "x1",
            "j2": "x2"
        }
    ]
}
Run Code Online (Sandbox Code Playgroud)

现在我知道Play正在使用Jackson来解析JSON.我在我的Java项目中使用Jackson,我会做这样简单的事情:

ObjectMapper mapper = new ObjectMapper();
Map<String, Object> obj = mapper.readValue(jsonString, Map.class);
Run Code Online (Sandbox Code Playgroud)

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

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

val obj: JsValue = Json.parse(jsonString)
Run Code Online (Sandbox Code Playgroud)

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

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

附带问题:是否有JsObject使用MapScala/Play 而不是Scala/Play的原因?

我的堆栈: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"
    }
]
Run Code Online (Sandbox Code Playgroud)

好吧,根据所有指示,我现在应该放入所有那些我不理解其目的的样板:

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

看起来不错,是吧?但是等一下,数组中出现了另一个元素,完全破坏了这种方法:

[
    {
        "key1": "v1",
        "key2": "v2"
    },
    {
        "key1": "x1",
        "key2": "x2"
    },
    {
        "key1": "y1",
        "key2": {
            "subkey1": "subval1",
            "subkey2": "subval2"
        }
    }
]
Run Code Online (Sandbox Code Playgroud)

第三个元素不再匹配我定义的案例类 - 我再次在第一个方面.我每天都能在Java中使用这种更复杂的JSON结构,Scala是否建议我应该简化我的JSON以适应它的"类型安全"策略?如果我错了,请纠正我,但我认为该语言应该提供数据,而不是相反?

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

Tra*_*own 31

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,)
Run Code Online (Sandbox Code Playgroud)

这类似于以下内容:

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

val k1Value = json("key1")
  .asInstanceOf[Map[String, Any]]("subkey1")
  .asInstanceOf[Map[String, String]]("k1")
Run Code Online (Sandbox Code Playgroud)

但前一种方法的优点是失败的方式更容易推理.我们只是获得了一个很好的价值,而不是一个可能难以解释的ClassCastException 异常.JsError

请注意,如果我们知道我们期望的结构类型,我们可以在树中更高的位置进行验证:

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

这两个Play示例都建立在类型类的概念之上- 特别是在ReadPlay提供的类型类的实例上.您还可以为自己定义的类型提供自己的类型类实例.这将允许您执行以下操作:

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

val something = myObj.key1.subkey1.k2(2)
Run Code Online (Sandbox Code Playgroud)

管他呢.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(_, _))
}
Run Code Online (Sandbox Code Playgroud)

其工作方式如下:

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)))
Run Code Online (Sandbox Code Playgroud)

是的,这是一个更冗长,但它是你支付一次的前期冗长(这提供了一些很好的保证),而不是一堆可能导致混乱的运行时错误的强制转换.

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

  • @Caballero:我同意语法有点不透明,但我想重申一下复杂性.这种方法在编译时将它全部放在桌面上,以避免它在运行时咬你. (2认同)

Cab*_*ero 26

我选择使用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)
Run Code Online (Sandbox Code Playgroud)


Dom*_*her 11

为了进一步参考和简洁的精神,您可以随时寻求:

Json.parse(jsonString).as[Map[String, JsValue]]
Run Code Online (Sandbox Code Playgroud)

但是,这将为不符合格式的JSON字符串抛出异常(但我认为这也适用于Jackson方法).在JsValue现在能够被处理进一步,如:

jsValueWhichBetterBeAList.as[List[JsValue]]
Run Code Online (Sandbox Code Playgroud)

我希望处理Objects和JsValues 之间的区别对你来说不是一个问题(只是因为你抱怨JsValue是专有的).显然,这有点像类型语言中的动态编程,这通常不是可行的方法(特拉维斯的回答通常是要走的路),但有时我觉得这很好.