简体   繁体   中英

How do I transform a JSON fields into a Seq in Scala Play framework 2?

I have some JSON coming from an external API which I have no control over. Part of the JSON is formatted like this:

{
  "room_0": {
    "area_sq_ft": 151.2
  },
  "room_1": {
    "area_sq_ft": 200.0
  }
}

Instead of using an array like they should have, they've used room_n for a key to n number of elements. Instead of creating a case class with room_0, room_1, room_2, etc., I want to convert this to a Seq[Room] where this is my Room case class:

case class Room(area: Double)

I am using Reads from play.api.libs.json for converting other parts of the JSON to case classes and would prefer to use Reads for this conversion. How could I accomplish that?

Here's what I've tried.

val sqFtReads = (__ \ "size_sq_ft").read[Double]
val roomReads = (__ \ "size_sq_ft").read[Seq[Room]](sqFtReads).map(Room) 
cmd19.sc:1: overloaded method value read with alternatives:
  (t: Seq[$sess.cmd17.Room])play.api.libs.json.Reads[Seq[$sess.cmd17.Room]] <and>
  (implicit r: play.api.libs.json.Reads[Seq[$sess.cmd17.Room]])play.api.libs.json.Reads[Seq[$sess.cmd17.Room]]
 cannot be applied to (play.api.libs.json.Reads[Double])
val roomReads = (__ \ "size_sq_ft").read[Seq[Room]](sqFtReads).map(Room)

A tricky little challenge but completely achievable with Reads .

First, Reads[Room] - ie the converter for a single Room instance:

val roomReads = new Reads[Room] {
  override def reads(json: JsValue): JsResult[Room] = {
    (json \ "area_sq_ft").validate[Double].map(Room(_))
  }
}

Pretty straightforward; we peek into the JSON and try to find a top-level field called area_sq_ft which validates as a Double . If it's all good, we return the populated Room instance as needed.

Next up, the converter for your upstream object that in good Postel's Law fashion, you are cleaning up for your own consumers.

val strangeObjectReads = new Reads[Seq[Room]] {
  override def reads(json: JsValue): JsResult[Seq[Room]] = {

    json.validate[JsObject].map { jso =>

      val roomsSortedNumerically = jso.fields.sortBy { case (name, contents) =>
        val numericPartOfRoomName = name.dropWhile(!_.isDigit)
        numericPartOfRoomName.toInt
      }

      roomsSortedNumerically.map { case (name, contents) =>
        contents.as[Room](roomReads)
      }

    }
  }
}

The key thing here is the json.validate[JsObject] around the whole lot. By map ping over this we get the JsResult that we need to wrap the whole thing, plus, we can get access to the fields inside the JSON object, which is defined as a Seq[(String, JsValue)] .

To ensure we put the fields in the correct order in the output sequence, we do a little bit of string manipulation, getting the numeric part of the room_1 string, and using that as the sortBy criteria. I'm being a bit naive here and assuming your upstream server won't do anything nasty like skip room numbers!

Once you've got the rooms sorted numerically, we can just map over them, converting each one with our roomReads converter.

You've probably noticed that my custom Reads implementations are most definitely not one-liners. This comes from bitter experience dealing with oddball upstream JSON formats. Being a bit verbose, using a few more variables and breaking things up a bit pays off big time when that upstream server changes its JSON format suddenly!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM