Search code examples
jsonscalaargonaut

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

Solution

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