我有一个 JsArray
,其中包含代表两种不同类型实体的 JsValue
对象 - 其中一些代表节点,另一部分代表 < em>边缘。
在 Scala 方面,已经有名为 Node
和 Edge
的案例类,它们的父类(super class)型是 Element
。目标是将 JsArray
(或 Seq[JsValue]
)转换为包含 Scala 类型的集合,例如Seq[Element]
(=> 包含类型为 Node
和 Edge
的对象)。
我已经为案例类定义了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
对象并以某种方式将它们委托(delegate)给顶部(记住:一个无效的数组元素应该导致 JsError
整体)。如果没有错误,我仍然需要根据 result
生成一个 JsSuccess[Seq[Element]]
。
也许避免调用 validate
并临时使用 Read
实例会更好。但我不确定如何在最后“合并”所有 Read
实例(例如,在简单的案例类映射中,您有一堆对 JsPath.read
(返回 Read
),最后,validate
根据使用 and
关键字连接的所有 Read 实例生成一个结果).
编辑更多信息。
首先,我应该提到案例类 Node
和 Edge
基本上具有相同的结构,至少目前是这样。目前,单独类的唯一原因是获得更多的类型安全性。
元素的 JsValue
具有以下 JSON 表示形式:
{
"id" : "aet864t884srtv87ae",
"type" : "node", // <-- type can be 'node' or 'edge'
"name" : "rectangle",
"attributes": [],
...
}
对应的案例类看起来像这样(注意我们上面看到的 type 属性不是类的属性——而是用类的类型表示 -> 节点
).
case class Node(
id: String,
name: String,
attributes: Seq[Attribute],
...) extends Element
读取
如下:
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
案例类(只是为了避免任何不相关的代码混淆答案)
case class Edge(name: String) extends Element
case class Node(name: String) extends Element
这些案例类的默认读取将由
Json.reads[Edge]
Json.reads[Node]
分别。不幸的是,由于两个案例类具有相同的结构,因此这些读取将忽略 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] 将返回一个错误,指出它需要一个字符串。
现在我们有了一个可以在 json 中强制执行 type
属性值的读取,我们所要做的就是为我们期望的每个类型值构建一个读取并组合它与相关的案例类读取。我们可以使用 Reads#flatMap
来忽略输入,因为解析的字符串对我们的案例类没有用。
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
作为 Actor 你可以写
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)
)
}
不幸的是,您不能在这种情况下使用 _
。
关于json - Play Json API : Convert a JsArray to a JsResult[Seq[Element]],我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34183429/