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

时间:2015-12-09 16:04:28

标签: json scala playframework playframework-2.0

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

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

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

编辑:更多信息。

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

元素的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 _) 的所有内容看起来都是一样的,至少目前是这样。

1 个答案:

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

基于简化的NodeEdge案例类(仅为避免任何无关代码混淆答案)

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伴随对象与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)
    )
}

不幸的是,在这种情况下你不能使用_