RichyHBM RichyHBM - 2 months ago 11
Scala Question

Dynamic Json values in Play Framework Json

I am currently using the Play framework json parser in order to parse a json string in my scala code.

I have the following class:

case class Address(address: String,
gps: GPS,
country: String) {}

object Address {
implicit val reads: Reads[Address] = (
(JsPath \ "address").read[String] and
(JsPath \ "gps").read[GPS] and
(JsPath \ "country").read[String]
) (Address.apply _)

implicit val writes: Writes[Address] = (
(JsPath \ "address").write[String] and
(JsPath \ "gps").write[GPS] and
(JsPath \ "country").write[String]
) (unlift(Address.unapply))
}


Which works fine with the following json:

{
"address": "123 Fake Street",
"country": "USA",
"gps": { ... }
}


The problem is that in some situations the json may instead have the gps field be a string which doesnt parse, i.e.

{
"address": "123 Fake Street",
"country": "USA",
"gps": "123abc"
}


Now I know that I cant have the gps member be both a string or a GPS object, but is there any way to have it be say an Option[GPS] and only have a value if the json contained a gps object?

Answer

Only very little is needed to be changed in your impl. You need to read the field "gps" as something that is 'safe' like JsValue and then try to map it into your GPS case class if it can be done, if not, return None.

case class GPS(a:String, b:String)
object GPS {
  val travelInfoReads = Json.reads[GPS]
  val travelInfoWrites = Json.writes[GPS]
  implicit val travelInfoFormat: Format[GPS] = Format(travelInfoReads, travelInfoWrites)
}
case class Address(address: String,
                   gps: Option[GPS],
                   country: String) {}

object Address {
  implicit val reads: Reads[Address] = (
    (JsPath \ "address").read[String] and
      (JsPath \ "gps").read[JsValue].map(js => js.asOpt[GPS]) and
      (JsPath \ "country").read[String]
    ) (Address.apply _)

  implicit val writes: Writes[Address] = (
    (JsPath \ "address").write[String] and
      (JsPath \ "gps").writeNullable[GPS] and
      (JsPath \ "country").write[String]
    ) (unlift(Address.unapply))
}

I also tested it:

val json = Json.toJson(Address("1",Some(GPS("a","b")),"2"))
      println(json)
      println(json.as[Address])
      val newObj: JsObject = (json.as[JsObject] - "gps") + ("gps" -> JsNumber(1))
      println(newObj)
      val a = newObj.as[Address]
      println(a)
      a must beEqualTo(Address("1",None,"2"))

Output was like

{"address":"1","gps":{"a":"a","b":"b"},"country":"2"}
Address(1,Some(GPS(a,b)),2)
{"address":"1","country":"2","gps":1}
Address(1,None,2)
Comments