Search code examples
jsonscalaplay-json

Transform JSON structure to piped format


Is there a quick and easy way to convert a JsObject, e.g.

  Json.obj(
    "title" -> "Working Title",
    "author" -> Json.obj(
      "name" -> "Peter Trunes",
      "location" -> Json.obj(
        "birthplace" -> "Perth",
        "nationality" -> "Australian",
        ..
      ),
      ..
    ),
    ..
  )

into a format where the JsPath is piped for all nested jsObjects? e.g.

  Json.obj(
    "title" -> "Working Title",
    "author.name" -> "Peter Trunes",
    "author.location.birthplace" -> "Perth",
    "author.location.nationality" -> "Australian",
    ..
  )

I'm using transformers to manipulate the Json data structure using the coast to coast technique (as documented here) and am able to do this for author for example like so:

  def authorTrans: Reads[JsObject] =
    (__ \ 'author).read[JsObject].flatMap(
      _.fields.foldLeft((__ \ 'author).json.prune) {
        case (acc, (k, v)) => acc andThen __.json.update(
          Reads.of[JsObject].map(_ + (s"author.$k" -> v))
        )
      }
    )

  def tryTransformAsJsObj(obj: JsValue, transformer: Reads[JsObject]) = {

    obj.transform(transformer) match {
      case JsSuccess(r: JsObject, _) => r
      case e: JsError => JsError.toJson(e)
    }

  }

  tryTransformAsJsObj(jso, authorTrans) // jso is the JsObject structure to be transformed

I have played around with using a recursive method here to transform each nested JsObject but I am struggling to get this to right and wonder if perhaps I am missing an easier technique. Thoughts & suggestions welcome!


Solution

  • Here is some recursive code for you, you should just extend the pattern match to cover all the cases, else you will get a warning:

    import play.api.libs.json.{JsObject, JsString, JsValue, Json}
    
    val original = Json.obj(
      "title" -> "Working Title",
      "author" -> Json.obj(
        "name" -> "Peter Trunes",
        "location" -> Json.obj(
          "birthplace" -> "Perth",
          "nationality" -> "Australian"
        )
      )
    )
    
    def transform(input: scala.collection.Map[String, JsValue], accum: JsObject, 
      curPath: String): JsObject = {
      val result = input.foldLeft(accum) {
        case (acc, (k, v)) =>
          v match {
            case JsString(str) => acc + (curPath + k -> Json.toJson(str))
            case JsObject(kvs) =>
              val newPath = if (curPath.isEmpty) s"$k." else s"$curPath$k."
              transform(kvs, acc, newPath)
          }
      }
    
      result
    }
    
    transform(original.value, Json.obj(), "")