Oleg
Oleg

Reputation: 3210

Play Framework JSON reader

I am trying to read JSON in following format

[
{type: "city",name:"Paris",lan:10.1,lot:20.0},
{type: "country",name:"France",...}

]

to Array[GeoObject] where GeoObject is parent for Country and City classes

how can I do this trick with Json Readers ? I have tryed something like

def mkObject(..,type:String,..):GeoObject = type match {
case 'city' => City(...)
case 'country' => Country(...)
}
implicit val geoobjectReads = (
      (JsPath \ "name").read[String] and
....
)(mkObject _)

but without success.

And actually I have the other different question.

I have in JSON two fields 'lan','lot' - but GeoObject has only one

coordinates:LanLot

where LanLot is just case class with 2 arguments.

Upvotes: 0

Views: 310

Answers (2)

RomanI
RomanI

Reputation: 403

I'm not a huge fan of Play! json read syntax. So I would prefer to use Json Inception to read into temporary case class, and then I would convert from one case class to another in the code explicitly.

Something like this. I replaced type with typ, to avoid code pollution with backticks.

import play.api.libs.json.Json

// let's just define all the structures you need
case class Coordinates(latitude: Double, longitude: Double)

abstract class GeoObject {
  def typ: String
  def name: String
  def coordinates: Coordinates
}

val CityType = "city"
val CountryType = "country"

case class City(name: String, coordinates: Coordinates) extends GeoObject {
  val typ = CityType
}

case class Country(name: String, coordinates: Coordinates) extends GeoObject {
  val typ = CountryType
}

// case class representing json
case class GeoObjectJson(typ: String, name: String, lat: Double, long: Double)
implicit val geoObjectJsonReads = Json.reads[GeoObjectJson]

// companion object (as a factory)
object GeoObject {

  def apply(geoJson: GeoObjectJson): GeoObject = {
    val coord = Coordinates(geoJson.lat, geoJson.long)
    geoJson.typ match {
      case CityType => City(geoJson.name, coord)
      case CountryType => Country(geoJson.name, coord)
    }
  }
}
// tests

val cityJsonString = """{"typ":"city", "name": "New York", "lat": 40.77, "long": -73.92}"""
val cityJson = Json.parse(cityJsonString).as[GeoObjectJson]
val cityGeoObject = GeoObject(cityJson)
// City(New York,Coordinates(40.77,-73.92))
val countryJsonString = """{"typ":"country", "name": "Canada", "lat": 40.77, "long": -73.92}"""
val countryJson = Json.parse(countryJsonString).as[GeoObjectJson]
val countryGeoObject = GeoObject(countryJson)
// Country(Canada,Coordinates(40.77,-73.92))

Upvotes: 0

Michael Zajac
Michael Zajac

Reputation: 55569

You'll also want to make json reads/writes for your coordinates if you want them in a separate case class, and they can be parsed as a nested object inside GeoObject. You were definitely on the right track, though.

Putting it all together:

case class Coordinates(latitude: Double, longitude: Double)

object Coordinates {

    implicit val jsonReads: Reads[Coordinates] = (
        (__ \ "latitude").read[Double] and 
        (__ \ "longitude").read[Double]
    )(Coordinates.apply _)

}

class GeoObject(`type`: String, name: String, coordinates: Coordinates)

object GeoObject {

    def apply(`type`: String, name: String, coordinates: Coordinates) = {
        `type` match {
            case "city" => City(`type`, name, coordinates)
            case "country" => Country(`type`, name, coordinates)
        }
    }

    implicit val jsonReads: Reads[GeoObject] = (
        (__ \ "type").read[String] and 
        (__ \ "name").read[String] and 
        (__ \ "coordinates").read[Coordinates]
    )(GeoObject.apply _)

 }

case class City(`type`: String, name: String, coordinates: Coordinates) extends GeoObject(`type`, name, coordinates)

case class Country(`type`: String, name: String, coordinates: Coordinates) extends GeoObject(`type`, name, coordinates)

And in action (play console):

scala> Json.parse("""{"type":"city", "name": "New York", "coordinates": {"latitude": 40.77, "longitude": -73.92}}""").as[GeoObject]
res2: GeoObject = City(city,New York,Coordinates(40.77,-73.92))

I overrode GeoObject.apply to always convert to City or Country, but you can use whatever function you want.

Edit: I went ahead and used type with backticks as Vikas pointed out.

Upvotes: 2

Related Questions