gor*_*lph 7 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
},
...
}
...
Run Code Online (Sandbox Code Playgroud)
我被大小坚持了.它们显然是变量,我不知道如何为此编写格式化程序?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)
Run Code Online (Sandbox Code Playgroud)
对于我刚刚Json.reads[x]为所有类进行的格式化.但是我很确定大小的变量会抛弃格式化,因为它无法从JSON中创建一个Image对象.
由于使用了关键字,下面描述的解决方案打破了参考透明度,这return不是我今天推荐的.尽管如此,由于历史原因,我不会因此而离开.
这里的问题是你需要找到一个地方来保存Size对象中每个对象的密钥Image.有两种方法可以做到这一点,一种是将它保存在Size对象本身中.这是有道理的,因为名称与Size对象密切相关,并且将其存储在那里很方便.所以让我们先探索一下这个解
在我们深入研究任何解决方案之前,让我先介绍一下对称性的概念.这个想法是,当您读取任何Json值时,您可以使用Scala模型表示返回到完全相同的Json值.
处理编组数据时的对称性并不是严格要求的,实际上有时它是不可能的,或者强制执行它会成本太高而没有任何实际收益.但通常它很容易实现,它使序列化实现更好.在许多情况下,它也是必需的.
name在Sizeimport 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)
Run Code Online (Sandbox Code Playgroud)
在这个解决方案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>
Run Code Online (Sandbox Code Playgroud)
而且非常重要的是它既安全又对称
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>
Run Code Online (Sandbox Code Playgroud)
在生产代码中,您永远不会,永远不会想要.as[T]在a上使用该方法JsValue.这是因为如果数据不符合您的预期,它会在没有任何有意义的错误处理的情况下爆炸.如果必须,请使用.asOpt[T],但一般情况下更好的选择是.validate[T],因为这会在失败时产生某种形式的错误,您可以记录然后向用户报告.
现在,可能更好的方法是将Imagecase类声明更改为以下内容
final case class Image(s: Seq[(String, Size)])
Run Code Online (Sandbox Code Playgroud)
然后保持Size原来的状态,
final case class Size(path: String, height: Int, width: Int)
Run Code Online (Sandbox Code Playgroud)
然后,您只需要执行以下操作即可安全且对称.
如果我们这样做,那么实现变得更好,同时仍然是安全和对称的.
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]
}
Run Code Online (Sandbox Code Playgroud)
仍然像以前一样工作
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}}
Run Code Online (Sandbox Code Playgroud)
但是Size现在反映真正的Json 的好处是,你可以序列化和反序列化Size值.这使得它更易于使用和思考.
reads在第一个例子中虽然我认为第一个解决方案在某种程度上不如第二个解决方案,但我们确实在第一个实现中使用了一些有趣的习惯用法,这些习惯用语在reads更广泛的意义上非常有用,但通常不太清楚.所以我想花点时间详细介绍一下感兴趣的人.如果你已经理解了使用中的习语,或者你只是不在乎,请随意跳过这个讨论.
flatMap 链接当我们试图获取我们需要的值时valueMap,在任何步骤中都可能出错.我们希望合理地处理这些情况,而不会抛出灾难性的异常.
为了实现这一点,我们使用Optionvalue和common flatMap函数来链接我们的计算.我们确实为每个所需的值执行了两个步骤,从中获取值,valueMap并使用该asOpt[T]函数将其强制为正确的类型.现在的好处是,既valueMap.get(s: String)和jsValue.asOpt[T]都返回Option值.这意味着我们可以flatMap用来构建我们的最终结果.flatMap具有良好的属性,如果flatMap链中的任何步骤失败,即返回None,则不运行所有其他步骤,并返回最终结果None.
这个成语是函数式语言常用的Monadic编程的一部分,尤其是Haskell和Scala.在Scala中,它通常不被称为Monadic,因为当在Haskell中引入该概念时,它经常被解释得很差,导致许多人不喜欢它,尽管它实际上非常有用.因此,人们常常害怕使用关于Scala的"M字".
reads在两个版本中使用的另一个习惯用法是通过return在scala中使用关键字来短路函数调用.
您可能知道,return在Scala中通常不鼓励使用关键字,因为任何函数的最终值都会自动转换为函数的返回值.然而,有一个非常有用的时间来使用return关键字,即当您调用表示对某些事物(例如map函数)重复调用的函数时.如果您在其中一个输入上遇到某个终端条件,则可以使用该return关键字停止map对其余元素执行调用.这是有点类似于使用break在一个for像Java语言的循环.
在我们的例子中,我们想要确保关于Json中元素的某些事情,比如它具有正确的键和类型,并且如果在任何时候我们的任何假设都不正确,我们想要返回正确的错误信息.现在我们可以map在Json的字段上,然后在map操作完成后检查结果,但考虑是否有人向我们发送了非常大的 Json,其中包含数千个没有我们想要的结构的密钥.即使我们知道在第一次应用之后我们有错误,我们也必须将我们的函数应用于所有值.使用return我们可以在map我们知道错误后立即结束应用程序,而无需花时间应用map 当结果已知时,应用于其余元素.
无论如何,我希望一点点迂腐的解释是有帮助的!
假设您要反序列化为以下案例类:
case class Size(name: String, path: String, width: Int, height: Int)
case class Image(sizes: List[Size])
case class Foo(..., image: Option[Image])
Run Code Online (Sandbox Code Playgroud)
有很多方法可以通过自定义Reads实现来完成这项工作.我将使用reads宏来Size:
implicit val sizeReads = Json.reads[Size]
Run Code Online (Sandbox Code Playgroud)
然后,由于大小不是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(_))
}
}
Run Code Online (Sandbox Code Playgroud)
然后Foo也可以使用reads宏.
implicit val fooReads = Json.reads[Foo]
Run Code Online (Sandbox Code Playgroud)
例:
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))))),)
Run Code Online (Sandbox Code Playgroud)
Writes[Image]如果您利用Json.obj模拟所需输出JSON的结构,实现a 会更容易一些.由于输出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
)
)
}
}
}
Run Code Online (Sandbox Code Playgroud)