Play JSON Reads[T]: 将一个 JsArray 拆分成多个子集

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

我有一个包含事件数组的 JSON 结构。该数组是 "polymorphic",因为存在三种可能的事件类型 ABC:

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

三种事件类型没有相同的对象结构,所以我需要不同的Reads。除此之外,整个JSON文档的目标案例class区分了事件:

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

如何定义 Reads[Doc] 的内部结构,以便 json 数组 events 分成三个子集,分别映射到 aEventsbEventscEvents?


到目前为止我尝试了什么(没有成功):

首先,我定义了一个 Reads[JsArray] 来将原来的 JsArray 转换为另一个只包含特定类型事件的 JsArray:

 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")
    }
  }

然后想法是在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 _)

但是,我不知道如何从这里继续下去。我假设 andThen 部分应该看起来像这样(在事件 a 的情况下):

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

但这不起作用,因为我希望 API 通过显式传递 Reads[EventA] 而不是 Reads[Seq[EventA]] 来创建 Seq[EventA]。除此之外,因为我从来没有得到它 运行,我不确定这整个方法是否合理。

编辑: 如果原始 JsArray 包含未知事件类型(例如 DE),则应忽略这些类型并从最终结果中排除(而不是使整个 Reads 失败)。

我会模拟这样一个事实,即您将不同的事件类型存储在 JS 数组中作为 class 层次结构以确保其类型安全。

sealed abstract class Event
case class EventA() extends Event
case class EventB() extends Event
case class EventC() extends Event

然后您可以将所有事件存储在一个集合中,稍后使用模式匹配来优化它们。例如:

case class Doc(events: Seq[Event]) {
    def getEventsA: Seq[EventA] = events.flatMap(_ match {
        case e: EventA => Some(e)
        case _ => None
    })
}

Doc(Seq(EventA(), EventB(), EventC())).getEventsA // res0: Seq[EventA] = List(EventA())

为了实现你的Reads,Doc会自然的映射到case上class,你只需要提供一个Event的映射即可。它可能是这样的:

implicit val eventReads = new Reads[Event] {
    override def reads(json: JsValue): JsResult[Event] = json \ "eventType" match {
        case JsDefined(JsString("A")) => JsSuccess(EventA())
        case JsDefined(JsString("B")) => JsSuccess(EventB())
        case JsDefined(JsString("C")) => JsSuccess(EventC())
        case _ => JsError("???")
    }
}
implicit val docReads = Json.reads[Doc]

然后你可以像这样使用它:

val jsValue = Json.parse("""
 {
  "events": [
    { "eventType": "A"},
    { "eventType": "B"},
    { "eventType": "C"}
  ]
 }
""")
val docJsResults = docReads.reads(jsValue) // docJsResults: play.api.libs.json.JsResult[Doc] = JsSuccess(Doc(List(EventA(), EventB(), EventC())),/events)

docJsResults.get.events.length // res1: Int = 3
docJsResults.get.getEventsA // res2: Seq[EventA] = List(EventA())

希望这对您有所帮助。

为每个 Event 类型输入隐式 read,例如

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

并使用 Reads[Doc](折叠事件列表以按类型分隔序列并将结果应用于 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)})

它将通过事件列表一次性创建 Doc

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

源数据

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)