Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Play 2.2 JSON Reads with combinators: how to deal with nested optional objects?

I'm going crazy trying to parse this JSON structure in Play Framework 2.2:

val jsonStr = """{ personFirstName: "FirstName",
  personLastName: "LastName"
  positionLat: null,
  positionLon: null }"""

I have 2 case classes:

case class Position( val lat: Double, val lon: Double)
case class Person( firstName: String, lastName: String, p: Option[Position] )

As you can see, Position is not mandatory in Person case class.

I was trying to get an instance of Person using something like this

implicit val reader = (
  (__ \ 'personFirstName ).read[String] ~
  (__ \ 'personLastName ).read[String] ~
  ( (__ \ 'positionLat ).read[Double] ~
    (__ \ 'positionLon ).read[Double] )(Position)
)(Person)

but I soon realized I have no idea how to deal with the Option[Position] object: the intention would be to instantiate a Some(Position(lat,lon)) if both 'lat' and 'lon' are specified and not null, otherwise instantiate None.

How would you deal with that?

like image 352
Max Avatar asked Oct 20 '13 16:10

Max


2 Answers

I'm pretty sure there's a better way of doing what you want than what I'm going to post, but it's late and I can't figure it out now. I'm assuming that simply changing the JSON structure you're consuming isn't an option here.

You can supply a builder function that takes two optional doubles for lat/lon and yields a position if they're both present.

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

val jsonStr = """{
  "personFirstName": "FirstName",
  "personLastName": "LastName",
  "positionLat": null,
  "positionLon": null }"""

case class Position(lat: Double, lon: Double)

case class Person( firstName: String, lastName: String, p: Option[Position] )

object Person {
  implicit val reader = (
    (__ \ "personFirstName" ).read[String] and
    (__ \ "personLastName" ).read[String] and (
      (__ \ "positionLat" ).readNullable[Double] and
      (__ \ "positionLon" ).readNullable[Double]
    )((latOpt: Option[Double], lonOpt: Option[Double]) => {
      for { lat <- latOpt ; lon <- lonOpt} yield Position(lat, lon)
    })
  )(Person.apply _)
}

Json.parse(jsonStr).validate[Person] // yields JsSuccess(Person(FirstName,LastName,None),)

Also, note that to be valid JSON you need to quote the data keys.

like image 92
Mikesname Avatar answered Nov 01 '22 21:11

Mikesname


Your javascript object should match the structure of your case classes. Position will need to have a json reader as well.

val jsonStr = """{ "personFirstName": "FirstName",
    "personLastName": "LastName",
    "position":{
        "lat": null,
        "lon": null
    } 
}"""

case class Person( firstName: String, lastName: String, p: Option[Position] )

object Person {

    implicit val reader = (
        (__ \ 'personFirstName ).read[String] ~
        (__ \ 'personLastName ).read[String] ~
        (__ \ 'position ).readNullable[Position]
    )(Person.apply _)

}

case class Position( val lat: Double, val lon: Double)

object Position {

    implicit val reader = (
        (__ \ 'lat ).read[Double] ~
        (__ \ 'lon ).read[Double]
    )(Position.apply _)

}

If either of the fields of Position are null/missing in the json object, it will be parsed as None. So, jsonStr.as[Person] = Person("FirstName", "LastName", None)

like image 37
Michael Zajac Avatar answered Nov 01 '22 23:11

Michael Zajac