更新于2016年7月28日
下面描述的解决方案由于使用了return
关键字,破坏了引用透明性,因此并不是我今天推荐的内容。然而,出于历史原因,我不会将其删除。
介绍
这里的问题是,您需要找到一个地方来保存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] {
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")
}
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
对象的参数,即路径、高度和宽度,还需要将
Size
的
name
指定为
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:
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] {
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")
}
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:
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”值。这使得它更易于使用和思考。
第一个示例中对“读取”(reads)的简短评论
尽管我认为第一种解决方案略逊于第二种解决方案,但我们在第一次实现“读取”(reads)时使用了一些有趣的习语,这些习语在更一般的情况下非常有用,但常常不为人所理解。因此,我想花时间详细介绍它们,供那些感兴趣的人参考。如果您已经了解正在使用的习语,或者您只是不在乎,可以随意跳过本讨论。
flatMap链接
当我们试图从“valueMap”中获取所需的值时,在任何步骤中都可能出现问题。我们希望合理地处理这些情况,而不会抛出灾难性的异常。
为了实现这一点,我们使用
Option
值和常见的
flatMap
函数来链接我们的计算。对于每个所需值,我们实际上需要进行两个步骤:从
valueMap
中获取该值,并使用
asOpt[T]
函数将其强制转换为适当的类型。现在好处是,
valueMap.get(s: String)
和
jsValue.asOpt[T]
都返回
Option
值。这意味着我们可以使用
flatMap
来构建最终结果。
flatMap
具有很好的属性,即如果
flatMap
链中的任何步骤失败(即返回
None
),则不运行所有其他步骤,并将最终结果作为
None
返回。
这种习惯用法是通用的
单子 编程的一部分,它在函数式语言中很常见,特别是在 Haskell 和 Scala 中。在 Scala 中,它通常不被称为
单子,因为当概念在 Haskell 中引入时,经常解释不清楚,导致许多人不喜欢它,尽管它实际上非常有用。由于这个原因,人们经常害怕在 Scala 中使用 "M 词"。
函数短路
在Scala的reads
中,另一个惯用语是通过使用return
关键字来短路函数调用。
正如您可能知道的那样,在Scala中通常不鼓励使用return
关键字,因为任何函数的最终值都会自动成为该函数的返回值。然而,有一种非常有用的情况可以使用return
关键字,即当您调用代表对某些内容进行重复调用的函数时,例如map
函数。如果您在其中一个输入上遇到了终止条件,您可以使用return
关键字停止执行剩余元素上的map
调用。这在某种程度上类似于在像Java这样的语言中使用for
循环中的break
。
在我们的情况下,我们希望确保Json中的元素具有正确的键和类型,并且如果我们的任何假设不正确,我们希望返回正确的错误信息。现在,我们可以只对Json中的字段进行
map
操作,然后在
map
操作完成后检查结果,但是考虑一下,如果有人向我们发送了包含数千个不符合我们要求结构的键的
非常大的 Json,即使我们在第一次应用后就知道了错误,我们仍必须将函数应用于
所有值。使用
return
,我们可以在发现错误时立即结束
map
应用程序,而无需花费时间将
map
应用程序应用于已知结果的其余元素。无论如何,我希望这段迂腐的解释对您有所帮助!
Seq(size1, size2, ..., sizeN) -> imageDetails
或类似的东西中? - goralphSize
类里怎么样?Size(name: String, path: String, width: Int, height: Int)
,然后读入列表中?Image(..., sizes: List[Size])
- Michael Zajac