如何在 Scala Play 中使用可变键解析 JSON?

How to parse JSON with variable keys in Scala Play?

首先祝大家新年快乐!

我在 Play 中解析 JSON 时遇到一些问题,我正在处理的格式如下:

JSON Response:

 ...
"image":{  
    "large":{  
      "path":"http://url.jpg",
      "width":300,
      "height":200
    },
    "medium":{  
      "path":"http://url.jpg",
      "width":200,
      "height":133
    },
    ...
 }
...

我被尺码问题困住了。它们显然是变量,我不确定如何为此编写格式化程序? JSON 来自外部服务。

到目前为止我有

final case class Foo(
  ..
  ..
  image: Option[Image])


final case class Image(size: List[Size])

final case class Size(path: String, width: Int, height: Int)

对于格式,我刚刚为所有课程做了 Json.reads[x]。但是我很确定大小变量正在丢弃格式,因为它无法从传入的 JSON 创建 Image 对象。

假设您要反序列化为以下情况 类:

case class Size(name: String, path: String, width: Int, height: Int)
case class Image(sizes: List[Size])
case class Foo(..., image: Option[Image])

有很多方法可以通过自定义 Reads 实现来完成这项工作。我将为 Size:

使用 reads
implicit val sizeReads = Json.reads[Size]

然后,由于大小不是 image 对象中的实际数组,我只是将它们合并为一个以利用我已有的 Reads[Size]。我可以将正在验证为 Image 的给定 JsValue 转换为 JsObject。然后我可以从 JsObject 中获取 fields,这将是 Seq[(String, JsValue)]。在这种情况下,String 是大小描述符,JsValue 是包含该大小的所有值的对象。我将把它们合并成一个对象,并从 Seq.

中创建一个 JsArray

从那里开始,我需要做的就是将 JsArray 验证为 List[Size],并将 map 验证为 Image.

implicit val imageReads = new Reads[Image] {
    def reads(js: JsValue): JsResult[Image] = {
        val fields: Seq[JsValue] = js.as[JsObject].fields.map { case (name, values) =>
            Json.obj("name" -> name) ++ values.as[JsObject]
        }

        JsArray(fields).validate[List[Size]].map(Image(_))
    }
}

那么Foo也可以使用reads

implicit val fooReads = Json.reads[Foo]

示例:

case class Foo(something: String, image: Option[Image])

val json = Json.parse("""{
    "something":"test",
    "image":{  
        "large":{  
            "path":"http://url.jpg",
            "width":300,
            "height":200
        },
        "medium":{  
            "path":"http://url.jpg",
            "width":200,
            "height":133
        }
    }
}""")

scala> json.validate[Foo]
res19: play.api.libs.json.JsResult[Foo] = JsSuccess(Foo(test,Some(Image(List(Size(large,http://url.jpg,300,200), Size(medium,http://url.jpg,200,133))))),)

如果您利用 Json.obj 来模仿您想要的输出 JSON 的结构,那么实施 Writes[Image] 会更容易一些。由于输出 JSON 实际上并不使用数组,我们还需要将尺寸列表合并回单个对象,我们可以使用 foldLeft.

implicit val writes = new Writes[Image] {
    def writes(img: Image): JsValue = {
        img.sizes.foldLeft(new JsObject(Nil)) { case (obj, size) =>
            obj ++ Json.obj(
                size.name -> Json.obj(
                    "path" -> size.path,
                    "width" -> size.width,
                    "height" -> size.height
                )
            )
        }
    }
}

更新2016-07-28

下面描述的解决方案由于使用了 return 关键字而破坏了 Referential Transparency,这不是我今天推荐的解决方案。尽管如此,由于历史原因,我不会保留它。

简介

这里的问题是您需要找到某个地方来保存 Image 对象中每个 Size 对象的密钥。有两种方法可以做到这一点,一种是将其保存在 Size 对象本身中。这是有道理的,因为名称与 Size 对象密切相关,并且将其存储在那里很方便。因此,让我们先探索该解决方案。

关于对称性的快速说明

在我们深入探讨任何解决方案之前,让我先介绍一下对称的概念。这个想法是,当您读取任何 Json 值时,您可以使用 Scala 模型表示返回到 完全 相同的 Json 值。

处理编组数据时的对称性并不是严格要求的,事实上有时它要么是不可能的,要么强制执行它的成本太高而没有任何实际收益。但是 通常 它很容易实现,它使序列化实现的工作变得更好。在许多情况下,它也是必需的。

name 保存到 Size

import play.api.libs.json.Format
import play.api.libs.json.JsPath
import play.api.libs.json.Reads
import play.api.libs.json.JsValue
import play.api.libs.json.JsResult
import play.api.libs.json.JsSuccess
import play.api.libs.json.JsError
import play.api.libs.json.JsObject
import play.api.libs.json.Json

final case class Foo(images: Option[Image])

object Foo {
  implicit val fooFormat: Format[Foo] = Json.format[Foo]
}

final case class Image(sizes: Seq[Size])

object Image {

  implicit val imagesFormat: Format[Image] =
    new Format[Image] {

      /** @inheritdoc */
      override def reads(json: JsValue): JsResult[Image] = json match {
        case j: JsObject => {
          JsSuccess(Image(j.fields.map{
            case (name, size: JsObject) =>
              if(size.keys.size == 3){
                val valueMap = size.value
                valueMap.get("path").flatMap(_.asOpt[String]).flatMap(
                  p=> valueMap.get("height").flatMap(_.asOpt[Int]).flatMap(
                    h => valueMap.get("width").flatMap(_.asOpt[Int]).flatMap(
                      w => Some(Size(name, p, h, w))
                    ))) match {
                  case Some(value) => value
                  case None => return JsError("Invalid input")
                }
              } else {
                  return JsError("Invalid keys on object")
              }
            case _ =>
              return JsError("Invalid JSON Type")
          }))
        }
        case _ => JsError("Invalid Image")
      }

      /** @inheritdoc */
      override def writes(o: Image): JsValue = {
        JsObject(o.sizes.map((s: Size) =>
          (s.name ->
            Json.obj(
              ("path" -> s.path),
              ("height" -> s.height),
              ("width" -> s.width)))))
      }
    }

}

final case class Size(name: String, path: String, height: Int, width: Int)

在此解决方案中,Size 没有任何直接的 Json 序列化或反序列化,而是作为 Image 对象的产物。这是因为,为了对 Image 对象进行对称序列化,您不仅需要保留 Size 对象的参数、路径、高度和宽度,还需要保留 name Size 的指定为 Image 对象上的键。如果你不存储这个你就不能自由地来回。

正如我们在下面看到的那样,这是有效的,

scala> import play.api.libs.json.Json
import play.api.libs.json.Json

scala> Json.parse("""
     | {  
     |     "large":{  
     |       "path":"http://url.jpg",
     |       "width":300,
     |       "height":200
     |     },
     |     "medium":{  
     |       "path":"http://url.jpg",
     |       "width":200,
     |       "height":133
     |     }
     | }""")
res0: play.api.libs.json.JsValue = {"large":{"path":"http://url.jpg","width":300,"height":200},"medium":{"path":"http://url.jpg","width":200,"height":133}}

scala> res0.validate[Image]
res1: play.api.libs.json.JsResult[Image] = JsSuccess(Image(ListBuffer(Size(large,http://url.jpg,200,300), Size(medium,http://url.jpg,133,200))),)

scala> 

而且非常重要的是,它既 安全 对称

scala> Json.toJson(res0.validate[Image].get)
res4: play.api.libs.json.JsValue = {"large":{"path":"http://url.jpg","height":200,"width":300},"medium":{"path":"http://url.jpg","height":133,"width":200}}

scala> 

安全须知

在生产代码中,您永远、永远、永远不想在 JsValue 上使用 .as[T] 方法。这是因为如果数据不是您所期望的,它会在没有任何有意义的错误处理的情况下爆炸。如果必须,请使用 .asOpt[T],但通常更好的选择是 .validate[T],因为这会在失败时产生某种形式的错误,您可以记录该错误,然后向用户报告。

可能是更好的解决方案

现在,完成此操作的更好方法可能是将 Image 案例 class 声明更改为以下

final case class Image(s: Seq[(String, Size)])

然后保持原样 Size

final case class Size(path: String, height: Int, width: Int)

那么为了安全和对称,您只需要执行以下操作即可。

如果我们这样做,那么实现会变得更好,同时仍然是安全和对称的。

import play.api.libs.json.Format
import play.api.libs.json.JsPath
import play.api.libs.json.Reads
import play.api.libs.json.JsValue
import play.api.libs.json.JsResult
import play.api.libs.json.JsSuccess
import play.api.libs.json.JsError
import play.api.libs.json.JsObject
import play.api.libs.json.Json

final case class Foo(images: Option[Image])

object Foo {
  implicit val fooFormat: Format[Foo] = Json.format[Foo]
}

final case class Image(sizes: Seq[(String, Size)])

object Image {

  implicit val imagesFormat: Format[Image] =
    new Format[Image] {

      /** @inheritdoc */
      override def reads(json: JsValue): JsResult[Image] = json match {
        case j: JsObject =>
          JsSuccess(Image(j.fields.map{
            case (name, size) =>
              size.validate[Size] match {
                case JsSuccess(validSize, _) => (name, validSize)
                case e: JsError => return e
              }
          }))
        case _ =>
          JsError("Invalid JSON type")
      }

      /** @inheritdoc */
      override def writes(o: Image): JsValue = Json.toJson(o.sizes.toMap)
    }
}

final case class Size(path: String, height: Int, width: Int)

object Size {
  implicit val sizeFormat: Format[Size] = Json.format[Size]
}

仍然像以前一样工作

scala> Json.parse("""
     | {
     | "large":{  
     |       "path":"http://url.jpg",
     |       "width":300,
     |       "height":200
     |     },
     |     "medium":{  
     |       "path":"http://url.jpg",
     |       "width":200,
     |       "height":133}}""")
res1: play.api.libs.json.JsValue = {"large":{"path":"http://url.jpg","width":300,"height":200},"medium":{"path":"http://url.jpg","width":200,"height":133}}

scala> res1.validate[Image]
res2: play.api.libs.json.JsResult[Image] = JsSuccess(Image(ListBuffer((large,Size(http://url.jpg,200,300)), (medium,Size(http://url.jpg,133,200)))),)

scala> Json.toJson(res1.validate[Image].get)
res3: play.api.libs.json.JsValue = {"large":{"path":"http://url.jpg","height":200,"width":300},"medium":{"path":"http://url.jpg","height":133,"width":200}}

但好处是 Size 现在反映了真实的 Json,即您可以仅对 Size 值进行序列化和反序列化。这使得它更容易使用和思考。

TL;DR 第一个例子reads的评论

虽然我认为第一个解决方案在某种程度上不如第二个解决方案,但我们确实在 reads 的第一个实现中使用了一些有趣的习语,这些习语在更一般的意义上非常有用,但通常不太了解。所以我想花时间为那些感兴趣的人更详细地研究它们。如果您已经了解正在使用的成语,或者您只是不在乎,请随时跳过此讨论。

flatMap 链接

当我们试图从 valueMap 中获取我们需要的值时,任何一步都可能出错。我们希望在不引发灾难性异常的情况下合理地处理这些情况。

为了实现这一点,我们使用 Option 值和常用的 flatMap 函数来链接我们的计算。我们为每个期望值执行了两个步骤,从 valueMap 中获取值,然后使用 asOpt[T] 函数将其强制为正确的类型。现在好事是 valueMap.get(s: String)jsValue.asOpt[T] 都是 return Option 值。这意味着我们可以使用 flatMap 来构建我们的最终结果。 flatMap 有很好的 属性 如果 flatMap 链中的任何步骤失败,即 return None,那么所有其他步骤都不会 运行,最后的结果是 returned as None.

这个习语是一般 Monadic 编程的一部分,它在函数式语言中很常见,尤其是 Haskell 和 Scala。在 Scala 中,它通常不被称为 Monadic,因为当这个概念在 Haskell 中被引入时,它经常被解释得不好,导致很多人不喜欢它,尽管它实际上非常有用。因此,人们常常害怕使用 Scala 的 "M word"。

功能性短路

在两个版本中 reads 中使用的另一个习语是通过在 scala 中使用 return 关键字来短路函数调用。

您可能知道,在 Scala 中通常不鼓励使用 return 关键字,因为任何函数的最终值都会自动成为该函数的 return 值。然而,有一个非常有用的时间使用 return 关键字,那就是当您调用一个函数时,该函数表示对某物的重复调用,例如 map 函数。如果您在其中一个输入上遇到了一些终止条件,您可以使用 return 关键字来停止对其余元素执行 map 调用。它有点类似于在 for 循环中使用 break 语言,如 Java.

在我们的例子中,我们想确保 Json 中元素的某些事情,比如它有正确的键和类型,如果在任何时候我们的假设不正确,我们想要到 return 正确的错误信息。现在我们可以 map 遍历 Json 中的字段,然后在 map 操作完成后检查结果,但考虑是否有人向我们发送了 非常大的数据 Json 有成千上万的键没有我们想要的结构。我们将不得不将我们的函数应用于 所有 值,即使我们知道仅在第一次应用后出现错误。使用 return 我们可以在知道错误后立即结束 map 应用程序,而不必在结果已知时花时间在其余元素上应用 map 应用程序。

总之,我希望一点迂腐的解释对您有所帮助!

也许基本类型更正常。只有我们定义了两个类:

final case class Size(path: String, width: Int, height: Int)
final case class Image(image: Map[String, Size])

implicit val sizeFormat: Format[Size] = Json.format[Size]
implicit val imageFormat: Format[Image] = Json.format[Image]

然后,运行一个例子:

val json: JsValue = Json.parse("""
{
  "image":{  
    "large":{  
      "path":"http://url.jpg",
      "width":300,
      "height":200
    },
    "medium":{  
      "path":"http://url.jpg",
      "width":200,
      "height":133
    }
  }
}
""")

json.validate[Image]

你可以获得

scala> json.validate[Image]
res13: play.api.libs.json.JsResult[Image] = JsSuccess(Image(Map(large -> Size(http://url.jpg,300,200), medium -> Size(http://url.jpg,200,133))),)

scala> json.validate[Image].get.image
res14: Map[String,Size] = Map(large -> Size(http://url.jpg,300,200), medium -> Size(http://url.jpg,200,133))

scala> json.validate[Image].get.image("large")
res15: Size = Size(http://url.jpg,300,200)

scala> json.validate[Image].get.image("large").path
res16: String = http://url.jpg

你也可以这样写:

scala> json.validate[Image].get
res18: Image = Image(Map(large -> Size(http://url.jpg,300,200), medium -> Size(http://url.jpg,200,133)))

scala> Json.toJson(json.validate[Image].get)
res19: play.api.libs.json.JsValue = {"image":{"large":{"path":"http://url.jpg","width":300,"height":200},"medium":{"path":"http://url.jpg","width":200,"height":133}}}