Search code examples
jsonplayframeworkplay-json

Play JSON Reads[T]: split a JsArray into multiple subsets


I have a JSON structure that contains an array of events. The array is "polymorphic" in the sense that there are three possible event types A, B and C:

{
 ...
 "events": [
   { "eventType": "A", ...},
   { "eventType": "B", ...},
   { "eventType": "C", ...},
   ...
 ]
}

The three event types don't have the same object structure, so I need different Reads for them. And apart from that, the target case class of the whole JSON document distinguishes between the events:

case class Doc(
   ...,
   aEvents: Seq[EventA],
   bEvents: Seq[EventB],
   cEvents: Seq[EventC],
   ...
)

How can I define the internals of Reads[Doc] so that the json array events is split into three subsets which are mapped to aEvents, bEvents and cEvents?


What I tried so far (without being succesful):

First, I defined a Reads[JsArray] to transform the original JsArray to another JsArray that only contains events of a particular type:

 def eventReads(eventTypeName: String) = new Reads[JsArray] {
    override def reads(json: JsValue): JsResult[JsArray] = json match {
      case JsArray(seq) =>
        val filtered = seq.filter { jsVal =>
          (jsVal \ "eventType").asOpt[String].contains(eventTypeName)
        }
        JsSuccess(JsArray(filtered))
      case _ => JsError("Must be an array")
    }
  }

Then the idea is to use it like this within Reads[Doc]:

implicit val docReads: Reads[Doc] = (
    ...
    (__ \ "events").read[JsArray](eventReads("A")).andThen... and
    (__ \ "events").read[JsArray](eventReads("B")).andThen... and
    (__ \ "events").read[JsArray](eventReads("C")).andThen... and
    ...
)(Doc.apply _)

However, I don't know how to go on from here. I assume the andThen part should look something like this (in case of event a):

.andThen[Seq[EventA]](EventA.reads)

But that doesn't work since I expect the API to create a Seq[EventA] by explicitly passing a Reads[EventA] instead of Reads[Seq[EventA]]. And apart from that, since I've never got it running, I'm not sure if this whole approach is reasonable in the first place.

edit: in case the original JsArray contains unknown event types (e.g. D and E), these types should be ignored and left out from the final result (instead of making the whole Reads fail).


Solution

  • put implicit read for every Event type like

    def eventRead[A](et: String, er: Reads[A]) = (__ \ "eventType").read[String].filter(_ == et).andKeep(er)
    
    implicit val eventARead = eventRead("A", Json.reads[EventA])
    implicit val eventBRead = eventRead("B", Json.reads[EventB])
    implicit val eventCRead = eventRead("C", Json.reads[EventC])
    

    and use Reads[Doc] (folding event list to separate sequences by types and apply result to Doc):

    Reads[Doc] = (__ \ "events").read[List[JsValue]].map(
        _.foldLeft[JsResult[ (Seq[EventA], Seq[EventB], Seq[EventC]) ]]( JsSuccess( (Seq.empty[EventA], Seq.empty[EventB], Seq.empty[EventC]) ) ){
          case (JsSuccess(a, _), v) => 
            (v.validate[EventA].map(e => a.copy(_1 = e +: a._1)) or v.validate[EventB].map(e => a.copy(_2 = e +: a._2)) or v.validate[EventC].map(e => a.copy(_3 = e +: a._3)))      
          case (e, _) => e
        }  
      ).flatMap(p => Reads[Doc]{js => p.map(Doc.tupled)})
    

    it will create Doc in one pass through events list

    JsSuccess(Doc(List(EventA(a)),List(EventB(b2), EventB(b1)),List(EventC(c))),)
    

    the source data

    val json = Json.parse("""{"events": [
                            |   { "eventType": "A", "e": "a"},
                            |   { "eventType": "B", "ev": "b1"},
                            |   { "eventType": "C", "event": "c"},
                            |   { "eventType": "B", "ev": "b2"}
                            | ]
                            |}
                            |""")
    case class EventA(e: String)
    case class EventB(ev: String)
    case class EventC(event: String)