Scala play json: how to capture the whole JSON source?

84 views Asked by At

I receive, in my client, a WSResponse, and use play's deserializeJson method to extract the data, specified by paths, e.g.

  implicit val lsmf: Format[MyData] = (
    (__).formatNullable[JsValue] ~
    (__ \ "id").format[Int] ~
    (__ \ "name").format[String])
  (MyData.apply, unlift(MyData.unapply))

The receiving class will look like

  case class MyData(
    json: JsValue,
    id:   Int,
    name: String) {...}

See, the first member of parsed data is supposed to contain the whole JSON as received.

I don't see how I can accomplish it. If I specify the path as (__), this is a bad path, and the parser fails. If I specify the path as (__ \ ""), the parser looks for a field named "", which is obviously missing.

Is there any reasonable solution, beyond just doing parsing manually (with my own hands)?

3

There are 3 answers

3
Gastón Schabas On

You don't need to do manual mapping if the fields of your case class has the same fields and types of json fields. You can you use directly an implicit val using Json.writes[A] for serializing, Json.reads[A] for deserializing or Json.format[A] for serialize and deserialize.

Here in the official docs shows how to do json automated mapping

0
Gaël J On

One way to do it:

implicit val reads: Reads[MyData] = (json: JsValue) => {
  val id = (json \ "id").as[Int] // or another way to extract it
  // Same for other fields...
  MyData(json, id, name)
}

// Do you even need a Writes? Not sure it makes much sense.
implicit val writes: Writes[MyData] = (data: MyData) => {
  data.json
  // Or:
  JsObject("id" -> JsNumber(data.id), ...)
}

Another one would be to rely on Play's macros by using a wrapper case class:

case class ActualData(id: Long, name: String)

case class MyData(json: JsValue, data: ActualData)

implicit val actualFormat: Format[ActualData] = Json.format[ActualData]

implicit val myReads: Reads[MyData] = (json: JsValue) => {
  MyData(json, json.as[ActualData])
}

The second having the benefit of auto adjusting if the data changed without having to update the parser.

2
Michael Zajac On

You're pretty close, but using formatNullable over just format doesn't make sense, because you're looking for a JsValue and not an Option[JsValue]. JsObject will work too.

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

case class MyData(
  json: JsObject,
  id: Int,
  name: String
)

implicit val fmt: Format[MyData] = (
  (__).format[JsObject] ~
  (__ \ "id").format[Int] ~
  (__ \ "name").format[String]
)(MyData.apply _, unlift(MyData.unapply))

val js = Json.parse("""
  {
    "id": 123,
    "name": "test",
    "foo": {
      "bar": 123
    },
    "baz": false
  }
""")

Yields:

scala> js.as[MyData]
val res0: MyData = MyData({"id":123,"name":"test","foo":{"bar":123},"baz":false},123,test)

scala> res0.copy(id = 456)
val res1: MyData = MyData({"id":123,"name":"test","foo":{"bar":123},"baz":false},456,test)

scala> Json.toJson(res1)
val res2: play.api.libs.json.JsValue = {"foo":{"bar":123},"name":"test","baz":false,"id":456}

Note that with res1, you can even modify the fields and they will still be serialized correctly (assuming the original json value is first).


A more manual way to do this would look like:

implicit val fmt: Format[MyData] = new Format[MyData] {
  def reads(js: JsValue): JsResult[MyData] = for {
    json <- js.validate[JsObject]
    id <- (js \ "id").validate[Int]
    name <- (js \ "name").validate[String]
  } yield MyData(json, id, name)

  def writes(value: MyData): JsValue = {
    value.json ++ Json.obj(
      "id" -> Json.toJson(value.id),
      "name" -> Json.toJson(value.name),
    )
  }
}