【问题标题】:Scala/Play: parse JSON into Map instead of JsObjectScala/Play:将 JSON 解析为 Map 而不是 JsObject
【发布时间】:2013-11-30 12:26:49
【问题描述】:

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

这反而给了我一个专有的JsObjecttype,这并不是我真正想要的。

我的问题是:我能否像在 Java 中那样轻松地将 Scala/Play 中的 JSON 字符串解析为 Map 而不是 JsObject

附带问题:在 Scala/Play 中使用 JsObject 代替 Map 是否有原因?

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

更新:我可以看到 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"
        }
    }
]

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

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

【问题讨论】:

  • 你真的在 JSON 上学习过游戏教程pages吗?在我看来,它不像。
  • @m09:我实际上认为这是一个非常合理的问题。基于类型类的解码 方法与您在 Jackson 之类的库中找到的方法非常不同(我认为更好,正如我在下面试图论证的那样)。
  • @TravisBrown:看到这个问题并且没有提到读取和 JSON 路径,我只是担心 OP 在要求之前没有花时间实际尝试获得 Play 的做事方式他过去工作流程的精确副本。我确实同意这两种方法是非常不同的,我认为这并不是说 play 的方法更好,而是一个事实;)
  • @m09 这是因为我阅读了我提出这个问题的文档(playframework.com/documentation/2.2.1/ScalaJson)。在我看来,有很多额外的努力和专有方法只是为了使其“类型安全”或任何目标。
  • @Caballero:就我个人而言,我不认为这是“额外的努力”——它更像是将努力提前,以便编译器可以帮助您。当然口味不同,但我偶尔会被Map[String, Object]-plagued 库困住,我觉得此时的体验非常糟糕。

标签: java json scala playframework playframework-2.0


【解决方案1】:

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异常

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

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

这两个 Play 示例都是基于 type classes 的概念构建的,尤其是基于 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)))

是的,这有点冗长,但它是您一次性支付的前期冗长(并且为您提供了一些很好的保证),而不是一堆可能导致令人困惑的运行时错误的强制转换。

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

【讨论】:

  • 我已经用一些在这种情况下让我感到困惑的更多信息编辑了我的问题。
  • 谢谢。我没想到会这么复杂。对我来说,这看起来很荒谬,记住这是一个简单的 JSON 示例,而我正在使用更复杂的 JSON。我什至不明白这种语法。
  • @Caballero:我同意语法有点不透明,但我想重申,无论哪种方式都存在复杂性。这种方法在编译时将其全部放在桌面上,以避免在运行时被它咬住。
【解决方案2】:

我选择使用Jackson module for 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)

【讨论】:

    【解决方案3】:

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

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

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

    jsValueWhichBetterBeAList.as[List[JsValue]]
    

    我希望处理Objects 和JsValues 之间的区别对您来说不是问题(只是因为您抱怨JsValues 是专有的)。显然,这有点像类型语言中的动态编程,这通常不是可行的方法(Travis 的回答通常是可行的方法),但有时我猜这很好。

    【讨论】:

      【解决方案4】:

      您可以简单地提取 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

      【讨论】:

        【解决方案5】:

        建议通读一下模式匹配和递归 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)。完整示例here

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2012-04-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-05-13
          • 1970-01-01
          • 1970-01-01
          • 2016-08-03
          相关资源
          最近更新 更多