Search code examples
scalaplay-json

Scala Play Read: How to Flatten Json containing Array of arrays to model


I am looking for a way to define a Reads which allows me to map a JSON containing the following structure:

{
    "offers": [
        [
            {
                "id": "1234",
                (etc.)
            }
        ]
    ]
}

to model such case class TransportOffer(offers: List[Offer])

Unfortunately I haven't been able to do this yet. This is my code:

implicit val transportOfferReads: Reads[TransportOffer] = (
    (JsPath \ "offers").read[List[List[Offer]]].flatMap(r => r.flatten)
    )(TransportOffer.apply _)

In this case the flattening is not possible, as flatMap expects another Reads. How would I wrap the flattened List into another Reads? Or is there a simpler way?


Solution

  • I'll present 3 options:

    1. Flattening in a short reads:
    case class Offer(id: String)
    
    object Offer {
      implicit val format: OFormat[Offer] = Json.format[Offer]
    }
    
    case class TransportOffer(offers: List[Offer])
    
    object TransportOffer {
      implicit val transportOfferReads: Reads[TransportOffer] =
        (JsPath \ "offers").read[List[List[Offer]]].map(x => TransportOffer(x.flatten))
    }
    

    Then calling:

    Json.parse(jsonString).validate[TransportOffer].foreach(println)
    

    outputs:

    TransportOffer(List(Offer(1234)))
    

    Code run at Scastie

    1. Explicitly writing Reads:
    implicit val transportOfferReads: Reads[TransportOffer] = (json: JsValue) => {
      json \ "offers" match {
        case JsUndefined() =>
          JsError("offers undefined")
        case JsDefined(value) =>
          value.validate[List[List[Offer]]].map(x => TransportOffer(x.flatten))
      }
    

    Code run at Scastie.

    1. First transform the json, into the model you'd like. For that define a transformer:
    val jsonTransformer = (__ \ "offers").json
      .update(__.read[JsArray].map{o => {
        JsArray(o.value.flatMap(_.asOpt[JsArray].map(_.value)).flatten)
      }})
    

    Then, assuming we have the case classes and their formatters:

    case class Offer(id: String)
    
    object Offer {
      implicit val format: OFormat[Offer] = Json.format[Offer]
    }
    
    case class TransportOffer(offers: List[Offer])
    
    object TransportOffer {
      implicit val format: OFormat[TransportOffer] = Json.format[TransportOffer]
    }
    

    We can call:

    Json.parse(jsonString).transform(jsonTransformer) match {
      case JsSuccess(value, _) =>
        value.validate[TransportOffer].foreach(println)
      case JsError(errors) =>
        println(errors)
        ???
    }
    

    Output is:

    TransportOffer(List(Offer(1234)))
    

    Code run at Scastie.