播放 Json API:将 JsArray 转换为 JsResult[Seq[Element]]
Play Json API: Convert a JsArray to a JsResult[Seq[Element]]
我有一个 JsArray
,其中包含代表两种不同类型实体的 JsValue
个对象 - 其中一些代表 节点 ,另一部分代表 边.
在 Scala 方面,已经有名为 Node
和 Edge
的案例 class,它们的超类型是 Element
。目标是将 JsArray
(或 Seq[JsValue]
)转换为包含 Scala 类型的集合,例如Seq[Element]
(=> 包含 Node
和 Edge
类型的对象)。
我已经为案例 class 定义了 Read
:
implicit val nodeReads: Reads[Node] = // ...
implicit val edgeReads: Reads[Edge] = // ...
除此之外,JsArray
本身还有一个Read
的第一步:
implicit val elementSeqReads = Reads[Seq[Element]](json => json match {
case JsArray(elements) => ???
case _ => JsError("Invalid JSON data (not a json array)")
})
如果 JsArray
的所有元素都是有效的节点和边,带问号的部分负责创建 JsSuccess(Seq(node1, edge1, ...)
,如果不是,则创建 JsError
。
但是,我不确定如何优雅地执行此操作。
区分节点和边的逻辑可以如下所示:
def hasType(item: JsValue, elemType: String) =
(item \ "elemType").asOpt[String] == Some(elemType)
val result = elements.map {
case n if hasType(n, "node") => // use nodeReads
case e if hasType(e, "edge") => // use edgeReads
case _ => JsError("Invalid element type")
}
问题是我现在不知道如何处理 nodeReads
/ edgeReads
。当然我可以直接调用他们的 validate
方法,但是 result
将具有 Seq[JsResult[Element]]
类型。所以最终我将不得不检查是否有任何 JsError
对象并以某种方式将它们委托给顶部(记住:一个无效的数组元素应该导致 JsError
整体)。如果没有错误,我仍然要根据 result
.
产生一个 JsSuccess[Seq[Element]]
也许最好避免调用 validate
并暂时使用 Read
实例。但是我不确定最后如何 "merge" 所有 Read
实例(例如,在简单的情况下 class 映射,你有一堆对 JsPath.read
的调用(其中 returns Read
) 最后,validate
基于使用 and
关键字连接的所有 Read 实例生成一个结果。
编辑:更多信息。
首先,我应该提到 class 和 Node
和 Edge
的情况基本上具有相同的结构,至少目前是这样。目前,单独 classes 的唯一原因是为了获得更多的类型安全性。
元素的 JsValue
具有以下 JSON 表示形式:
{
"id" : "aet864t884srtv87ae",
"type" : "node", // <-- type can be 'node' or 'edge'
"name" : "rectangle",
"attributes": [],
...
}
对应的情况class看起来像这样(注意我们上面看到的type属性是不是 class的属性——而是它由 class -> Node
) 的类型表示。
case class Node(
id: String,
name: String,
attributes: Seq[Attribute],
...) extends Element
Read
如下:
implicit val nodeReads: Reads[Node] = (
(__ \ "id").read[String] and
(__ \ "name").read[String] and
(__ \ "attributes").read[Seq[Attribute]] and
....
) (Node.apply _)
Edge
的一切看起来都一样,至少现在是这样。
尝试将 elementReads
定义为
implicit val elementReads = new Reads[Element]{
override def reads(json: JsValue): JsResult[Element] =
json.validate(
Node.nodeReads.map(_.asInstanceOf[Element]) orElse
Edge.edgeReads.map(_.asInstanceOf[Element])
)
}
并将其导入范围内,然后您应该可以编写
json.validate[Seq[Element]]
如果您的 json 的结构不足以区分 Node
和 Edge
,您可以在每种类型的读取中强制执行它。
基于简化的 Node
和 Edge
案例 class(只是为了避免任何不相关的代码混淆答案)
case class Edge(name: String) extends Element
case class Node(name: String) extends Element
这些案例 classes 的默认读取将由
派生
Json.reads[Edge]
Json.reads[Node]
分别。不幸的是,由于两种情况 classes 具有相同的结构,这些读取将忽略 json 中的 type
属性并愉快地将节点 json 转换为 Edge
实例或相反。
让我们看看如何单独表达对 type
的约束:
def typeRead(`type`: String): Reads[String] = {
val isNotOfType = ValidationError(s"is not of expected type ${`type`}")
(__ \ "type").read[String].filter(isNotOfType)(_ == `type`)
}
此方法构建一个 Reads[String] 实例,它将尝试在提供的 json 中查找 type
字符串属性。如果从 json 中解析出的字符串与作为方法参数传递的预期 type
不匹配,它将使用自定义验证错误 isNotOfType
过滤 JsResult
。当然,如果 type
属性不是 json 中的字符串,Reads[String] 将 return 一个错误,指出它需要一个字符串。
现在我们有一个读取可以在 json 中强制执行 type
属性的值,我们所要做的就是为我们期望的每个类型值构建一个读取并将其与相关案例 class 读取组合。我们可以使用 Reads#flatMap
来忽略输入,因为解析的字符串对我们的情况 classes 没有用。
object Edge {
val edgeReads: Reads[Edge] =
Element.typeRead("edge").flatMap(_ => Json.reads[Edge])
}
object Node {
val nodeReads: Reads[Node] =
Element.typeRead("node").flatMap(_ => Json.reads[Node])
}
请注意,如果 type
上的约束失败,将绕过 flatMap
调用。
问题仍然是将方法 typeRead
放在哪里,在这个答案中,我最初将它与 elementReads
实例一起放在 Element
伴随对象中,如下面的代码所示.
import play.api.libs.json._
trait Element
object Element {
implicit val elementReads = new Reads[Element] {
override def reads(json: JsValue): JsResult[Element] =
json.validate(
Node.nodeReads.map(_.asInstanceOf[Element]) orElse
Edge.edgeReads.map(_.asInstanceOf[Element])
)
}
def typeRead(`type`: String): Reads[String] = {
val isNotOfType = ValidationError(s"is not of expected type ${`type`}")
(__ \ "type").read[String].filter(isNotOfType)(_ == `type`)
}
}
这实际上是定义 typeRead
的一个非常糟糕的地方:
- 它没有特定于 Element
- 它在 Element
伴随对象与 Node
和 Edge
伴随对象之间引入了循环依赖关系
不过我会让你想出正确的位置:)
证明这一切一起工作的规范:
import org.specs2.mutable.Specification
import play.api.libs.json._
import play.api.data.validation.ValidationError
class ElementSpec extends Specification {
"Element reads" should {
"read an edge json as an edge" in {
val result: JsResult[Element] = edgeJson.validate[Element]
result.isSuccess should beTrue
result.get should beEqualTo(Edge("myEdge"))
}
"read a node json as an node" in {
val result: JsResult[Element] = nodeJson.validate[Element]
result.isSuccess should beTrue
result.get should beEqualTo(Node("myNode"))
}
}
"Node reads" should {
"read a node json as an node" in {
val result: JsResult[Node] = nodeJson.validate[Node](Node.nodeReads)
result.isSuccess should beTrue
result.get should beEqualTo(Node("myNode"))
}
"fail to read an edge json as a node" in {
val result: JsResult[Node] = edgeJson.validate[Node](Node.nodeReads)
result.isError should beTrue
val JsError(errors) = result
val invalidNode = JsError.toJson(Seq(
(__ \ "type") -> Seq(ValidationError("is not of expected type node"))
))
JsError.toJson(errors) should beEqualTo(invalidNode)
}
}
"Edge reads" should {
"read a edge json as an edge" in {
val result: JsResult[Edge] = edgeJson.validate[Edge](Edge.edgeReads)
result.isSuccess should beTrue
result.get should beEqualTo(Edge("myEdge"))
}
"fail to read a node json as an edge" in {
val result: JsResult[Edge] = nodeJson.validate[Edge](Edge.edgeReads)
result.isError should beTrue
val JsError(errors) = result
val invalidEdge = JsError.toJson(Seq(
(__ \ "type") -> Seq(ValidationError("is not of expected type edge"))
))
JsError.toJson(errors) should beEqualTo(invalidEdge)
}
}
val edgeJson = Json.parse(
"""
|{
| "type":"edge",
| "name":"myEdge"
|}
""".stripMargin)
val nodeJson = Json.parse(
"""
|{
| "type":"node",
| "name":"myNode"
|}
""".stripMargin)
}
如果你不想使用 asInstanceOf
作为演员你可以写
elementReads
像这样的实例 :
implicit val elementReads = new Reads[Element] {
override def reads(json: JsValue): JsResult[Element] =
json.validate(
Node.nodeReads.map(e => e: Element) orElse
Edge.edgeReads.map(e => e: Element)
)
}
很遗憾,在这种情况下您不能使用 _
。
我有一个 JsArray
,其中包含代表两种不同类型实体的 JsValue
个对象 - 其中一些代表 节点 ,另一部分代表 边.
在 Scala 方面,已经有名为 Node
和 Edge
的案例 class,它们的超类型是 Element
。目标是将 JsArray
(或 Seq[JsValue]
)转换为包含 Scala 类型的集合,例如Seq[Element]
(=> 包含 Node
和 Edge
类型的对象)。
我已经为案例 class 定义了 Read
:
implicit val nodeReads: Reads[Node] = // ...
implicit val edgeReads: Reads[Edge] = // ...
除此之外,JsArray
本身还有一个Read
的第一步:
implicit val elementSeqReads = Reads[Seq[Element]](json => json match {
case JsArray(elements) => ???
case _ => JsError("Invalid JSON data (not a json array)")
})
如果 JsArray
的所有元素都是有效的节点和边,带问号的部分负责创建 JsSuccess(Seq(node1, edge1, ...)
,如果不是,则创建 JsError
。
但是,我不确定如何优雅地执行此操作。
区分节点和边的逻辑可以如下所示:
def hasType(item: JsValue, elemType: String) =
(item \ "elemType").asOpt[String] == Some(elemType)
val result = elements.map {
case n if hasType(n, "node") => // use nodeReads
case e if hasType(e, "edge") => // use edgeReads
case _ => JsError("Invalid element type")
}
问题是我现在不知道如何处理 nodeReads
/ edgeReads
。当然我可以直接调用他们的 validate
方法,但是 result
将具有 Seq[JsResult[Element]]
类型。所以最终我将不得不检查是否有任何 JsError
对象并以某种方式将它们委托给顶部(记住:一个无效的数组元素应该导致 JsError
整体)。如果没有错误,我仍然要根据 result
.
JsSuccess[Seq[Element]]
也许最好避免调用 validate
并暂时使用 Read
实例。但是我不确定最后如何 "merge" 所有 Read
实例(例如,在简单的情况下 class 映射,你有一堆对 JsPath.read
的调用(其中 returns Read
) 最后,validate
基于使用 and
关键字连接的所有 Read 实例生成一个结果。
编辑:更多信息。
首先,我应该提到 class 和 Node
和 Edge
的情况基本上具有相同的结构,至少目前是这样。目前,单独 classes 的唯一原因是为了获得更多的类型安全性。
元素的 JsValue
具有以下 JSON 表示形式:
{
"id" : "aet864t884srtv87ae",
"type" : "node", // <-- type can be 'node' or 'edge'
"name" : "rectangle",
"attributes": [],
...
}
对应的情况class看起来像这样(注意我们上面看到的type属性是不是 class的属性——而是它由 class -> Node
) 的类型表示。
case class Node(
id: String,
name: String,
attributes: Seq[Attribute],
...) extends Element
Read
如下:
implicit val nodeReads: Reads[Node] = (
(__ \ "id").read[String] and
(__ \ "name").read[String] and
(__ \ "attributes").read[Seq[Attribute]] and
....
) (Node.apply _)
Edge
的一切看起来都一样,至少现在是这样。
尝试将 elementReads
定义为
implicit val elementReads = new Reads[Element]{
override def reads(json: JsValue): JsResult[Element] =
json.validate(
Node.nodeReads.map(_.asInstanceOf[Element]) orElse
Edge.edgeReads.map(_.asInstanceOf[Element])
)
}
并将其导入范围内,然后您应该可以编写
json.validate[Seq[Element]]
如果您的 json 的结构不足以区分 Node
和 Edge
,您可以在每种类型的读取中强制执行它。
基于简化的 Node
和 Edge
案例 class(只是为了避免任何不相关的代码混淆答案)
case class Edge(name: String) extends Element
case class Node(name: String) extends Element
这些案例 classes 的默认读取将由
派生Json.reads[Edge]
Json.reads[Node]
分别。不幸的是,由于两种情况 classes 具有相同的结构,这些读取将忽略 json 中的 type
属性并愉快地将节点 json 转换为 Edge
实例或相反。
让我们看看如何单独表达对 type
的约束:
def typeRead(`type`: String): Reads[String] = {
val isNotOfType = ValidationError(s"is not of expected type ${`type`}")
(__ \ "type").read[String].filter(isNotOfType)(_ == `type`)
}
此方法构建一个 Reads[String] 实例,它将尝试在提供的 json 中查找 type
字符串属性。如果从 json 中解析出的字符串与作为方法参数传递的预期 type
不匹配,它将使用自定义验证错误 isNotOfType
过滤 JsResult
。当然,如果 type
属性不是 json 中的字符串,Reads[String] 将 return 一个错误,指出它需要一个字符串。
现在我们有一个读取可以在 json 中强制执行 type
属性的值,我们所要做的就是为我们期望的每个类型值构建一个读取并将其与相关案例 class 读取组合。我们可以使用 Reads#flatMap
来忽略输入,因为解析的字符串对我们的情况 classes 没有用。
object Edge {
val edgeReads: Reads[Edge] =
Element.typeRead("edge").flatMap(_ => Json.reads[Edge])
}
object Node {
val nodeReads: Reads[Node] =
Element.typeRead("node").flatMap(_ => Json.reads[Node])
}
请注意,如果 type
上的约束失败,将绕过 flatMap
调用。
问题仍然是将方法 typeRead
放在哪里,在这个答案中,我最初将它与 elementReads
实例一起放在 Element
伴随对象中,如下面的代码所示.
import play.api.libs.json._
trait Element
object Element {
implicit val elementReads = new Reads[Element] {
override def reads(json: JsValue): JsResult[Element] =
json.validate(
Node.nodeReads.map(_.asInstanceOf[Element]) orElse
Edge.edgeReads.map(_.asInstanceOf[Element])
)
}
def typeRead(`type`: String): Reads[String] = {
val isNotOfType = ValidationError(s"is not of expected type ${`type`}")
(__ \ "type").read[String].filter(isNotOfType)(_ == `type`)
}
}
这实际上是定义 typeRead
的一个非常糟糕的地方:
- 它没有特定于 Element
- 它在 Element
伴随对象与 Node
和 Edge
伴随对象之间引入了循环依赖关系
不过我会让你想出正确的位置:)
证明这一切一起工作的规范:
import org.specs2.mutable.Specification
import play.api.libs.json._
import play.api.data.validation.ValidationError
class ElementSpec extends Specification {
"Element reads" should {
"read an edge json as an edge" in {
val result: JsResult[Element] = edgeJson.validate[Element]
result.isSuccess should beTrue
result.get should beEqualTo(Edge("myEdge"))
}
"read a node json as an node" in {
val result: JsResult[Element] = nodeJson.validate[Element]
result.isSuccess should beTrue
result.get should beEqualTo(Node("myNode"))
}
}
"Node reads" should {
"read a node json as an node" in {
val result: JsResult[Node] = nodeJson.validate[Node](Node.nodeReads)
result.isSuccess should beTrue
result.get should beEqualTo(Node("myNode"))
}
"fail to read an edge json as a node" in {
val result: JsResult[Node] = edgeJson.validate[Node](Node.nodeReads)
result.isError should beTrue
val JsError(errors) = result
val invalidNode = JsError.toJson(Seq(
(__ \ "type") -> Seq(ValidationError("is not of expected type node"))
))
JsError.toJson(errors) should beEqualTo(invalidNode)
}
}
"Edge reads" should {
"read a edge json as an edge" in {
val result: JsResult[Edge] = edgeJson.validate[Edge](Edge.edgeReads)
result.isSuccess should beTrue
result.get should beEqualTo(Edge("myEdge"))
}
"fail to read a node json as an edge" in {
val result: JsResult[Edge] = nodeJson.validate[Edge](Edge.edgeReads)
result.isError should beTrue
val JsError(errors) = result
val invalidEdge = JsError.toJson(Seq(
(__ \ "type") -> Seq(ValidationError("is not of expected type edge"))
))
JsError.toJson(errors) should beEqualTo(invalidEdge)
}
}
val edgeJson = Json.parse(
"""
|{
| "type":"edge",
| "name":"myEdge"
|}
""".stripMargin)
val nodeJson = Json.parse(
"""
|{
| "type":"node",
| "name":"myNode"
|}
""".stripMargin)
}
如果你不想使用 asInstanceOf
作为演员你可以写
elementReads
像这样的实例 :
implicit val elementReads = new Reads[Element] {
override def reads(json: JsValue): JsResult[Element] =
json.validate(
Node.nodeReads.map(e => e: Element) orElse
Edge.edgeReads.map(e => e: Element)
)
}
很遗憾,在这种情况下您不能使用 _
。