如何使用play-json对JsArrays进行排序

时间:2014-11-12 14:14:22

标签: json scala playframework

简单的问题:

如何使用play-json(2.3.x)对某些JsValue(递归)中的所有JsArrays进行排序?

我的用例:

考虑在内部使用Set[String]的应用,并在请求数据时, 输出JSON将该集序列化为JSON数组。订单并不重要。

现在,如果有人想编写一些测试来覆盖这个功能,因为项目的顺序并不重要(毕竟它是一个集合。内部和概念上),我要检查的是所有返回的内容它应该,我可能想要将响应JSON与我明确创建的“预期”JSON对象进行比较。

出于这个原因,我想对JSON数组进行排序,并比较JsValue。 怎么会写这样的变压器?

编辑:

我设法编写了一个满足我需求的转换器,但它不会对某些JsArry中的每个JsValue进行排序。我会在这里发布,因为它可能对其他人有用,但它不是我要求的。

val jsonSortTransformer = (__ \ 'fields).json.update(
  Reads.JsObjectReads.map{
    case JsObject(xs) => JsObject(
      xs.map{
        case (n,jv) => {
          n -> (jv match {
            case JsArray(arr) if arr.forall(_.isInstanceOf[JsString]) => JsArray(arr.sortBy(_.as[String]))
            case _ => jv
          })
        }
      }
    )
  }
)

2 个答案:

答案 0 :(得分:3)

您可以使用value上的JsArray属性获取Seq[JsValue],然后进行任意排序,然后重新创建JsArray。例如:

scala> myJsArray
play.api.libs.json.JsArray = ["11","4","5","1","22","2"]

scala> JsArray(myJsArray.value.sortBy(_.as[JsString].value.toInt))
play.api.libs.json.JsArray = ["1","2","4","5","11","22"]

如果您所做的只是尝试比较您知道的集合的实际值和期望值,您也可以在两个属性上使用value,构建Set并检查为了平等:

Set(actual.value: _*) == Set(expected.value: _*)

或者对它们进行排序:

val sortedSeq: JsArray => Seq[String] = array => array.value.map(_.toString).sorted
sortedSeq(actual) == sortedSeq(expected)

要对任意JsArrays中的所有JsValue进行递归排序,它可能类似于:

def sortArrays(json: JsValue): JsValue = json match {
    case JsObject(obj) => JsObject(obj.toMap.mapValues(sortArrays(_)).toList)
    case JsArray(arr) => JsArray(arr.map(sortArrays).sortBy(_.toString))
    case other => other
}

scala> myObj
play.api.libs.json.JsValue = {"a":[2,1],"b":[{"c":[3,2]},{"d":[4,3]}],"e":{"f":[5,4]}}

scala> sortArrays(myObj)
play.api.libs.json.JsValue = {"a":[1,2],"b":[{"c":[2,3]},{"d":[3,4]}],"e":{"f":[4,5]}}

答案 1 :(得分:0)

我担心@Ben的答案是错误的。

我会通过为Ordering定义一个JsValue类来解决这个问题,然后使用它的比较方法来验证相等性(这意味着它实际上应该是object - 而不是一个匿名类,如示例所示。

一个人不必使用Ordering,我发现它比简单的compareTo方法更方便。当然,也可以将此类/对象定义为implicit

val jsonOrdering: Ordering[JsValue] = new Ordering[JsValue]() {

override def compare(x: JsValue, y: JsValue): Int = {

  x.getClass.getName.compareTo(y.getClass.getName) match {
    case 0 =>
      (x, y) match {
        case (JsNull, JsNull) => 0
        case (JsString(valueX), JsString(valueY)) =>
          valueX.compareTo(valueY)
        case (JsNumber(valueX), JsNumber(valueY)) =>
          valueX.compare(valueY)
        case (JsBoolean(boolX), JsBoolean(boolY)) =>
          boolX.compareTo(boolY)
        case (JsArray(elementsX), JsArray(elementsY)) =>
          elementsX.size.compareTo(elementsY.size) match {
            case 0 =>
              elementsX
//                      .sorted(this) // uncomment if array order DOES NOT matter
                .zip(elementsY
//                        .sorted(this) // uncomment if array order DOES NOT matter
                )
                .view
                .map {
                  case (elementX, elementY) => compare(elementX, elementY)
                }
                .find(_ != 0)
                .getOrElse(0)
            case nonZero => nonZero
          }
        case (JsObject(fieldsX), JsObject(fieldsY)) =>
          fieldsX.size.compareTo(fieldsY.size) match {
            case 0 =>
              fieldsX.toSeq
                .sortBy(_._1)
                .zip(fieldsY.toSeq.sortBy(_._1))
                .view
                .flatMap {
                  case ((keyX, valueX), (keyY, valueY)) =>
                    Seq(keyX.compareTo(keyY), compare(valueX, valueY))
                }
                .find(_ != 0)
                .getOrElse(0)
            case nonZero => nonZero
          }
      }
    case nonZero => nonZero
  }
}

我可能会将一些部分分成私有/嵌套函数(这次我很懒)。无论如何,让我们来看看:

  1. 比较两个值'类名,如果它们不相同,则返回它们名称之间的比较。
  2. 如果值是任何原始JSON类型,只需返回它们之间的比较。
  3. 如果值是数组,则:
    1. 比较它们的尺寸,如果它们不相同,则返回尺寸之间的比较。
    2. 只有当数组的顺序时才重要 - 对每个数组进行排序(使用相同的排序类;即,这是递归的)。
    3. 压缩两个数组的元素(以便获得一对元素对的数组)。
    4. 找到它的两个元素不相同的第一对,并返回它们的比较。
    5. 如果不存在这样的对,则表示数组相同(返回0)。
  4. 如果值是地图(对象):
    1. 比较它们的尺寸,如果它们不相同,则返回尺寸之间的比较。
    2. 将地图转换为一系列元组,并按其键(元组的第一个元素)对这些序列进行排序。
    3. 压缩两个序列的元组(以便获得一对元组对)。
    4. 找到它的元组不相同的第一对,并返回它们的比较。以下列方式比较这些元组:
      1. 比较他们的键(字符串),如果它们不相同则返回它们的比较。
      2. 比较它们的值(JsValue,因此递归使用相同的方法),如果它们不相同则返回它们的比较。
      3. 否则,他们是一样的。
    5. 如果不存在这样的对,则意味着地图(对象)是相同的(返回0)。
  5. 请注意,虽然这种顺序是一致的和确定性的,但它是非常随意的,并没有传达很多逻辑意义。