1

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)?

cchantep
  • 9,118
  • 3
  • 30
  • 41
Vlad Patryshev
  • 1,379
  • 1
  • 10
  • 16

3 Answers3

1

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.

Gaël J
  • 11,274
  • 4
  • 17
  • 32
1

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),
    )
  }
}
Michael Zajac
  • 55,144
  • 7
  • 113
  • 138
0

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

Gastón Schabas
  • 2,153
  • 1
  • 10
  • 17
  • thank you for the link, it did give me some information. But unfortunately neither the doc there no your response does not answer my question, which is like this: I do the "automated mapping", as described at that doc, but I also need to pass the whole `JsObject` in an additional field of my case class. That's the problem I was looking a solution for. Mapping individual fields is not a problem. – Vlad Patryshev May 01 '23 at 15:50
  • sorry, my bad. I guess I read your question quickly and missunderstood what you were asking. Do you have a field named `json` in the json that you are trying to parse? Could you give an example or more than one of the json that you are trying to deserialize? It's not clear for me the whole context – Gastón Schabas May 01 '23 at 16:07
  • see, what I need, in addition to extracting all the fields I need, is to get the whole content of the incoming JSON, and keep it in that `json` field. The context is this: I'm getting a JSON. I need several fields out of it, to be used in my code. The rest should stay available when I return this record, on demand. Maybe I should just receive JSON as is, and then extract ("manually") the fields I need. But if there's a easier, "automated", way, that would be nice to have. – Vlad Patryshev May 01 '23 at 16:34