Search code examples
scalaplayframeworkplay-json

Create and fill a JsArray with play-json transformer


With Play/Scala, I try to transform this json:

val json = Json.parse(""" 
{
  "name": "John Doe",
  "location": { 
    "lon": 48.858596, 
    "lat": 2.294481 
  }
} 
""")

into this result:

val result = Json.parse(""" 
{ 
  "name": "John Doe", 
  "location": { 
    "type": "Point", 
    "coordinates": [48.858596, 2.294481] 
  }
} 
""")

Any idea how to apply the magic? Here's what i tried:

import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._

val transformer = {
    val locationField = __ \ "location"
    val lon = (locationField \ "lon").json.pick
    val lat = (locationField \ "lat").json.pick

    __.json.update((
        (locationField \ "type").json.put( JsString("Point") ) and
        (locationField \ "coordinates").json.put( JsArray() )).reduce
      andThen
        (locationField \ "coordinates").json.update(of[JsArray].map { // How to add lon/lat into JsArray?????
          case JsArray(arr) => JsArray(arr :+ JsNumber(3L))
          }
        )
      andThen (
        (locationField \ "lon").json.prune and
        (locationField \ "lat").json.prune).reduce
    )
  }

json.transform(transformer)

Get the code here: https://gist.github.com/chrissom/20c5aa254210d7c32f53479df6a66f68


Solution

  • Assuming the import:

    import play.api.libs.json._
    

    You can define a Location that would represent the location value.

    case class Location(lon: Double, lat: Double) {
      def toPoint: JsObject = {
        Json.obj(
          "type" -> "Point",
          "coordinates" -> Json.arr(lon, lat)
        )
      }
    }
    
    object Location {
      implicit val reads: Reads[Location] = Json.reads[LonLat]
    }
    

    Now you could do:

    val locationJson = Json.parse("""{"lon": 48.858596, "lat": 2.294481}""")
    
    locationJson.transform(of[LonLat].map(_.toPoint))
    // Output: 
    // JsSuccess({"type":"Point","coordinates":[48.858596,2.294481]},)
    

    Now you can plug this into a transformation such as:

    (__ \ 'location).json.update(of[LonLat].map(_.toPoint))
    

    But you'll be left over with lat and lon fields in this transformation. So remove them:

    (__ \ 'location).json.update(of[LonLat].map(_.toPoint)) andThen
      (__ \ 'location \ 'lat).json.prune andThen
      (__ \ 'location \ 'lon).json.prune
    

    It appears to me that it is not possible to simplify this as update is the only operation of its kind - which happens to be a deep-merge.