播放 Json API:将 JsArray 转换为 JsResult[Seq[Element]]

Play Json API: Convert a JsArray to a JsResult[Seq[Element]]

我有一个 JsArray,其中包含代表两种不同类型实体的 JsValue 个对象 - 其中一些代表 节点 ,另一部分代表 .

在 Scala 方面,已经有名为 NodeEdge 的案例 class,它们的超类型是 Element。目标是将 JsArray(或 Seq[JsValue])转换为包含 Scala 类型的集合,例如Seq[Element](=> 包含 NodeEdge 类型的对象)。

我已经为案例 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 和 NodeEdge 的情况基本上具有相同的结构,至少目前是这样。目前,单独 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 的结构不足以区分 NodeEdge,您可以在每种类型的读取中强制执行它。

基于简化的 NodeEdge 案例 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 伴随对象与 NodeEdge 伴随对象之间引入了循环依赖关系

不过我会让你想出正确的位置:)

证明这一切一起工作的规范:

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)
    )
}

很遗憾,在这种情况下您不能使用 _