ceran ceran - 1 month ago 12
JSON Question

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

Answer

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)