ceran ceran - 16 days ago 4
Scala Question

Argonaut: decoding a polymorphic array

The JSON object for which I'm trying to write a

DecodeJson[T]
contains an array of different "types" (meaning the JSON structure of its elements is varying). The only common feature is the
type
field which can be used to distinguish between the types. All other fields are different. Example:

{
...,
array: [
{ type: "a", a1: ..., a2: ...},
{ type: "b", b1: ...},
{ type: "c", c1: ..., c2: ..., c3: ...},
{ type: "a", a1: ..., a2: ...},
...
],
...
}


Using argonaut, is it possible to map the JSON array to a Scala
Seq[Element]
where
Element
is a supertype of suitable case classes of type
ElementA
,
ElementB
and so on?

I did the same thing with
play-json
and it was quite easy (basically a
Reads[Element]
that evaluates the
type
field and accordingly forwards to more specific
Reads
). However, I couldn't find a way to do this with argonaut.




edit: example

Scala types (I wish to use):

case class Container(id: Int, events: List[Event])

sealed trait Event
case class Birthday(name: String, age: Int) extends Event
case class Appointment(start: Long, participants: List[String]) extends Event
case class ... extends Event


JSON instance (not under my control):

{
"id":1674,
"events": {
"data": [
{
"type": "birthday",
"name": "Jones Clinton",
"age": 34
},
{
"type": "appointment",
"start": 1675156665555,
"participants": [
"John Doe",
"Jane Doe",
"Foo Bar"
]
}
]
}
}

Answer

You can create a small function to help you build a decoder that handles this format.

See below for an example.

import argonaut._, Argonaut._

def decodeByType[T](encoders: (String, DecodeJson[_ <: T])*) = {
  val encMap = encoders.toMap

  def decoder(h: CursorHistory, s: String) =
    encMap.get(s).fold(DecodeResult.fail[DecodeJson[_ <: T]](s"Unknown type: $s", h))(d => DecodeResult.ok(d))

  DecodeJson[T] { c: HCursor =>
    val tf = c.downField("type")

    for {
      tv   <- tf.as[String]
      dec  <- decoder(tf.history, tv)
      data <- dec(c).map[T](identity)
    } yield data
  }
}

case class Container(id: Int, events: ContainerData)
case class ContainerData(data: List[Event])

sealed trait Event
case class Birthday(name: String, age: Int) extends Event
case class Appointment(start: Long, participants: List[String]) extends Event

implicit val eventDecoder: DecodeJson[Event] = decodeByType[Event](
  "birthday" -> DecodeJson.derive[Birthday],
  "appointment" -> DecodeJson.derive[Appointment]
)

implicit val containerDataDecoder: DecodeJson[ContainerData] = DecodeJson.derive[ContainerData]
implicit val containerDecoder: DecodeJson[Container] = DecodeJson.derive[Container]

val goodJsonStr =
  """
    {
       "id":1674,
       "events": {
          "data": [
             {
                "type": "birthday",
                "name": "Jones Clinton",
                "age": 34
             },
             {
                "type": "appointment",
                "start": 1675156665555,
                "participants": [
                   "John Doe",
                   "Jane Doe",
                   "Foo Bar"
                ]
             }
          ]
       }
    }
  """

def main(args: Array[String]) = {
  println(goodJsonStr.decode[Container])

  // \/-(Container(1674,ContainerData(List(Birthday(Jones Clinton,34), Appointment(1675156665555,List(John Doe, Jane Doe, Foo Bar))))))
}