Search code examples
scalaplay-json

Scala play json - update all values with the same key


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!


Solution

  • 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!