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 obj = mapper.readValue(jsonString, Map.class); 

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

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

 val obj: JsValue = Json.parse(jsonString) 

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

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

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

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

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

 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。 我鼓励你给类型类方法一个机会,虽然有一个陡峭的学习曲线,但很多人(包括我自己)非常喜欢它。

我选择使用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字符串抛出exception(但我认为这也适用于Jackson方法)。 现在可以进一步处理JsValue如:

 jsValueWhichBetterBeAList.as[List[JsValue]] 

我希望处理Object s和JsValue之间的区别不是你的问题(只是因为你抱怨JsValue是专有的)。 显然,这有点像类型语言中的动态编程,通常不是那种方式(特拉维斯的答案通常是要走的路),但有时我觉得这很好。

您可以简单地提取出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(如谷歌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)。 完整的例子。