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

标签 json scala playframework playframework-2.0

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

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

我已经为案例类定义了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 实例生成一个结果).

编辑更多信息。

首先,我应该提到案例类 NodeEdge 基本上具有相同的结构,至少目前是这样。目前,单独类的唯一原因是获得更多的类型安全性。

元素的 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 结构不足以区分 NodeEdge,您可以在每种类型的读取中强制执行它。

基于简化的 NodeEdge 案例类(只是为了避免任何不相关的代码混淆答案)

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 伴随对象与 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 作为 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/

相关文章:

json - 创建没有组合器模式的递归 JSON 写入

playframework - Play Framework JS拼接

php - 错误 : HTTP 0 Error: HTTP 0 http://127. 0.0.0:7212/

java - 对从服务器检索为 JSON 的数据实现搜索过滤器

php - 有人对从 ColdFusion 的 serializeJSON 方法到 PHP 的 json_decode 有问题吗?

scala - 在 Scala 中读取包含多行字符串的 CSV 文件

javascript - 将字符串数组解析为 MVC 方法

Scala 元组到字符串(使用 mkString)

java - 从 AKKA 等 Actor 模型框架调用基于线程的 API

java - 用于在 Amazon Beanstalk Linux 上下载 Java 8 的 Dockerfile