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

时间:2015-01-01 16:33:18

标签: json scala playframework

新年快乐,首先!

我在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对象。

3 个答案:

答案 0 :(得分:8)

更新2016-07-28

由于使用return关键字,下面描述的解决方案会导致Referential Transparency中断,而我今天不推荐这样做。尽管如此,由于历史原因,我不会因此而离开它。

简介

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

关于Symmetry的快速说明

在我们深入研究任何解决方案之前,让我先介绍一下对称性的概念。这个想法是,当您读取任何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> 

关于安全的快速说明

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

可能是更好的解决方案

现在,可能更好的方法是将Image案例类声明更改为以下

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]都返回Option值。这意味着我们可以使用flatMap来构建我们的最终结果。 flatMap具有良好的属性,如果flatMap链中的任何步骤失败,即返回None,则不会运行所有其他步骤,并且最终结果将返回为{{1 }}

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

功能性短路

两个版本中None中使用的另一个习惯用法是在scala中使用reads关键字来短接函数调用。

您可能知道,在Scala中经常不鼓励使用return关键字,因为任何函数的最终值都会自动转换为函数的返回值。然而,有一个非常有用的时间来使用return关键字,即当您调用表示对某些事物重复调用的函数时,例如return函数。如果您在其中一个输入上遇到某个终端条件,则可以使用map关键字停止对其余元素执行return调用。这有点类似于在Java等语言中的map循环中使用break

在我们的例子中,我们想确保关于Json中元素的某些事情,比如它具有正确的键和类型,并且如果在任何时候我们的任何假设都不正确,我们想要返回正确的错误信息。现在我们只能{J}上的for字段,然后在map操作完成后检查结果,但考虑是否有人向我们发送了非常大的 Json成千上万的密钥没有我们想要的结构。我们必须将我们的函数应用于所有值,即使我们知道在第一次应用之后我们有错误。使用map我们可以在知道错误后立即结束return应用程序,而无需花费时间在结果已知的情况下将map应用程序应用于其余元素。

无论如何,我希望一点点迂腐的解释是有帮助的!

答案 1 :(得分:5)

假设您要反序列化为以下案例类:

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

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

implicit val sizeReads = Json.reads[Size]

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

从那里,我需要做的就是将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))))),)

如果您利用Writes[Image]来模仿所需输出JSON的结构,那么实现Json.obj会更容易一些。由于输出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
                )
            )
        }
    }
}

答案 2 :(得分:0)

使用基本类型可能更正常。只有我们定义了两个类:

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