ceran ceran - 9 months ago 46
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 Source

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)