Decoding into recursive ADTs with argonaut

267 Views Asked by At

I'm trying to parse json like

{
  "element": "string",
  "content": "application/json"
}

where element decides which type the json is. But my code fails to parse.

http://scastie.org/15213

import scalaz._, Scalaz._
import argonaut._, Argonaut._, Shapeless._

case class ArrayAttributes(default: List[StringElement])

sealed trait Element
case class StringElement(content: String) extends Element
case class ArrayElement(attributes: ArrayAttributes, content: List[Element]) extends Element
case class Reference(element: String) extends Element { def content = element }

object Parser {

  def kindDecode[T](
    kinds: Map[String, DecodeJson[T]],
    fail: HCursor => DecodeResult[T] = { c: HCursor => DecodeResult.fail[T]("expected one of ${kind.keys}", c.history) }): DecodeJson[T] = DecodeJson(c =>
    (c --\ "element").as[String].flatMap { kind =>
      kinds.get(kind).map(_.decode(c)).getOrElse(fail(c))
    }
  )

  implicit def elementDecode: DecodeJson[Element] = kindDecode(
    Map(
      "string" -> DecodeJson.of[StringElement].map(identity[Element]),
      "array" -> arrayDecode.map(identity[Element])
    ),
    { c => DecodeJson.of[Reference].decode(c).map(identity[Element]) }
  )

  def arrayDecode: DecodeJson[ArrayElement] = jdecode2L(ArrayElement.apply)("attributes", "content")

}
1

There are 1 best solutions below

0
On BEST ANSWER

I'm going to answer with the current milestone of argonaut-shapeless (1.0.0-M1), which has had relevant additions since the 0.3.1 version.

It allows to specify so-called JsonSumCodecs for sum types, that drive how the sub-types are encoded / discriminated.

By defining one for Element in its companion object, like

implicit val jsonSumCodecForElement = derive.JsonSumCodecFor[Element](
  derive.JsonSumTypeFieldCodec(
    typeField = "element",
    toTypeValue = Some(_.stripSuffix("Element").toLowerCase)
  )
)

your example just works:

> Parse.decodeEither[Element](member)
res1: (String \/ (String, CursorHistory)) \/ Element =
  \/-(StringElement(application/json))

JsonSumCodecFor above is a type class that provides a JsonSumCodec for a given type, here Element. As a JsonSumCodec, we choose JsonSumTypeFieldCodec, that uses a field, "type" by default, to discriminate between sub-types. Here, we choose "element" instead of "type", and we also transform the sub-type names (toTypeValue argument), so that these match the names of the example input.