1

Let's say I have a JsValue in the form:

{
  "businessDetails" : {
    "name" : "Business",
    "phoneNumber" : "+44 0808 157 0192"
  },
  "employees" : [
    {
      "name" : "Employee 1",
      "phoneNumber" : "07700 900 982"
    },
    {
      "name" : "Employee 2",
      "phoneNumber" : "+44(0)151 999 2458"
    }
  ]
}

I was wondering if there is a way to do an update on every value belonging to a key with a certain name inside a JsValue regardless of its complexity? Ideally I'd like to map on every phone number to ensure that a (0) is removed if there is one. I have come across play-json-zipper updateAll but I'm getting unresolved dependency issues when adding the library to my sbt project. Any help either adding the play-json-zipper library or implementing this in ordinary play-json would be much appreciated. Thanks!

Ivan Kurchenko
  • 4,043
  • 1
  • 11
  • 28

1 Answers1

2

From what I can see in play-json-zipper project page, you might forgot to add resolver resolvers += "mandubian maven bintray" at "http://dl.bintray.com/mandubian/maven"

If it won't help, and you would like to proceed with custom implementation: play-json does not provide folding or traversing API over JsValue out of the box, so it can be implemented recursively in the next way:

/**
 * JSON path from the root. Int - index in array, String - field
 */
type JsPath = Seq[Either[Int,String]]
type JsEntry = (JsPath, JsValue)
type JsTraverse = PartialFunction[JsEntry, JsValue]

implicit class JsPathOps(underlying: JsPath) {
  def isEndsWith(field: String): Boolean = underlying.lastOption.contains(Right(field))
  def isEndsWith(index: Int): Boolean = underlying.lastOption.contains(Left(index))
  def /(field: String): JsPath = underlying :+ Right(field)
  def /(index: Int): JsPath = underlying :+ Left(index)
}

implicit class JsValueOps(underlying: JsValue) {
  /**
   * Traverse underlying json based on given partially defined function `f` only on scalar values, like:
   * null, string or number.
   *
   * @param f function
   * @return updated json
   */
  def traverse(f: JsTraverse): JsValue = {
    def traverseRec(prefix: JsPath, value: JsValue): JsValue = {
      val lifted: JsValue => JsValue = value => f.lift(prefix -> value).getOrElse(value)
      value match {
        case JsNull => lifted(JsNull)
        case boolean: JsBoolean => lifted(boolean)
        case number: JsNumber => lifted(number)
        case string: JsString => lifted(string)
        case array: JsArray =>
          val updatedArray = array.value.zipWithIndex.map {
            case (arrayValue, index) => traverseRec(prefix / index, arrayValue)
          }
          JsArray(updatedArray)

        case `object`: JsObject =>
          val updatedFields = `object`.fieldSet.toSeq.map {
            case (field, fieldValue) => field -> traverseRec(prefix / field, fieldValue)
          }
          JsObject(updatedFields)
      }
    }
    traverseRec(Nil, underlying)
  }
}

which can be used in the next way:

val json =
  s"""
     |{
     |  "businessDetails" : {
     |    "name" : "Business",
     |    "phoneNumber" : "+44(0) 0808 157 0192"
     |  },
     |  "employees" : [
     |    {
     |      "name" : "Employee 1",
     |      "phoneNumber" : "07700 900 982"
     |    },
     |    {
     |      "name" : "Employee 2",
     |      "phoneNumber" : "+44(0)151 999 2458"
     |    }
     |  ]
     |}
     |""".stripMargin

val updated = Json.parse(json).traverse {
  case (path, JsString(phone)) if path.isEndsWith("phoneNumber") => JsString(phone.replace("(0)", ""))
}

println(Json.prettyPrint(updated))

which will produce desired result:

{
  "businessDetails" : {
    "name" : "Business",
    "phoneNumber" : "+44 0808 157 0192"
  },
  "employees" : [ {
    "name" : "Employee 1",
    "phoneNumber" : "07700 900 982"
  }, {
    "name" : "Employee 2",
    "phoneNumber" : "+44151 999 2458"
  } ]
}

Hope this helps!

Ivan Kurchenko
  • 4,043
  • 1
  • 11
  • 28