3

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"
            ]
         }
      ]
   }
}
ceran
  • 1,392
  • 1
  • 17
  • 43

1 Answers1

2

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))))))
}
longshorej
  • 381
  • 1
  • 2
  • Thanks for your response. The problem is that there is not a single sub-object (such as `data`) - instead, all the payload is on the same hierachical level. I edited the example in my question. – ceran Nov 23 '16 at 10:46
  • Okay, I see what you're saying. I updated my original post with a new decoder that should handle what you need. – longshorej Nov 23 '16 at 16:18
  • Sorry for wasting your time. My classes look a bit different and I couldn't make it work. Maybe we can start a final try - I added a complete example above. Big thanks. – ceran Nov 23 '16 at 17:52
  • I made another edit to reflect your actual format. Hopefully we got it this time! – longshorej Nov 23 '16 at 19:49