我有一个JsArray
,其中包含代表两种不同类型实体的JsValue
个对象 - 其中一些代表节点,另一部分代表边缘
在Scala方面,已经有名为Node
和Edge
的案例类,其超类型为Element
。目标是将JsArray
(或Seq[JsValue]
)转换为包含Scala类型的集合,例如Seq[Element]
(=>包含Node
和Edge
类型的对象。
我为案例类定义了Read
:
implicit val nodeReads: Reads[Node] = // ...
implicit val edgeReads: Reads[Edge] = // ...
除此之外,Read
本身有JsArray
的第一步:
implicit val elementSeqReads = Reads[Seq[Element]](json => json match {
case JsArray(elements) => ???
case _ => JsError("Invalid JSON data (not a json array)")
})
如果JsSuccess(Seq(node1, edge1, ...)
的所有元素都是有效节点和边缘,则带有问号的部分负责创建JsArray
;如果不是这种情况,则负责创建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
)。如果没有错误,我仍然需要根据JsSuccess[Seq[Element]]
生成result
。
也许最好避免调用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": [],
...
}
相应的case类看起来像这样(注意我们上面看到的type属性是不是类的属性 - 而是由类的类型表示 - &gt; {{ 1}})。
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 _)
的所有内容看起来都是一样的,至少目前是这样。
答案 0 :(得分:2)
尝试将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]
分别。不幸的是,由于两个case类都具有相同的结构,因此这些读取将忽略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解析出的字符串与作为方法参数传递的预期JsResult
不匹配,它将使用自定义验证错误isNotOfType
过滤type
。当然,如果type
属性不是json中的字符串,则Reads [String]将返回一个错误,表明它需要一个String。
既然我们有一个可以强制json中type
属性值的读取,我们所要做的就是为我们期望的每个类型的值构建一个读取,并用相关的值组合它案例类读取。我们可以使用Reads#flatMap
来忽略输入,因为解析后的字符串对我们的case类没用。
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
的位置,在这个答案中,我最初将它与Element
实例一起放在elementReads
伴随对象中,如下面的代码所示。 / p>
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)
)
}
不幸的是,在这种情况下你不能使用_
。