1

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?

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks a lot @TomerShetah for your input. Just to clarify - is there no possibility to flatten the JsArray on Reads?
@TomerShetah well that would work when TransportOffer only takes one parameter right?
Yes, for more parameters you need to use the and operator

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.