15

This question has come up a few times recently, so I'm FAQ-ing it here. Suppose I've got some case classes like this:

import io.circe._, io.circe.generic.semiauto._

object model {
  case class A(a: String)
  case class B(a: String, i: Int)
  case class C(i: Int, b: Boolean)

  implicit val encodeA: Encoder[A] = deriveEncoder
  implicit val encodeB: Encoder[B] = deriveEncoder
  implicit val encodeC: Encoder[C] = deriveEncoder
  implicit val decodeA: Decoder[A] = deriveDecoder
  implicit val decodeB: Decoder[B] = deriveDecoder
  implicit val decodeC: Decoder[C] = deriveDecoder
}

And I want to encode a value that could be any one of these as JSON using circe and Shapeless coproducts.

import io.circe.shapes._, io.circe.syntax._
import shapeless._

import model._

type ABC = A :+: B :+: C :+: CNil

val c: ABC = Coproduct[ABC](C(123, false))

This looks fine at first:

scala> c.asJson
res0: io.circe.Json =
{
  "i" : 123,
  "b" : false
}

But the problem is that I can never decode a coproduct containing a B element, since any valid JSON document that could be decoded as B can also be decoded as A, and the coproduct decoders provided by circe-shapes try the elements in the order they appear in the coproduct.

scala> val b: ABC = Coproduct[ABC](B("xyz", 123))
b: ABC = Inr(Inl(B(xyz,123)))

scala> val json = b.asJson
json: io.circe.Json =
{
  "a" : "xyz",
  "i" : 123
}

scala> io.circe.jawn.decode[ABC](json.noSpaces)
res1: Either[io.circe.Error,ABC] = Right(Inl(A(xyz)))

How can I disambiguate the elements of my coproduct in my encoding?

Travis Brown
  • 135,682
  • 12
  • 352
  • 654

1 Answers1

11

The circe-shapes module encodes ordinary hlists and coproducts without labels, as seen above: a coproduct as the bare JSON representation of the element, and an hlist ends up as just a JSON array (the same as the default tuple encoding):

scala> ("xyz" :: List(1, 2, 3) :: false :: HNil).asJson.noSpaces
res2: String = ["xyz",[1,2,3],false]

In the case of hlists there's no danger of ambiguity because of overlapping names, but for coproducts there is. In both cases, though, you can add labels to the JSON representation using Shapeless's labeling mechanism, which tags values with a type-level symbol.

An hlist with labels is called a "record", and a coproduct with labels is a "union". Shapeless provides special syntax and operations for both of these, and circe-shapes treats both differently from unlabeled hlists or coproducts. For example (assuming the definitions and imports above):

scala> import shapeless.union._, shapeless.syntax.singleton._
import shapeless.union._
import shapeless.syntax.singleton._

scala> type ABCL = Union.`'A -> A, 'B -> B, 'C -> C`.T
defined type alias ABCL

scala> val bL: ABCL = Coproduct[ABCL]('B ->> B("xyz", 123))
bL: ABCL = Inr(Inl(B(xyz,123)))

scala> val jsonL = bL.asJson
jsonL: io.circe.Json =
{
  "B" : {
    "a" : "xyz",
    "i" : 123
  }
}

scala> io.circe.jawn.decode[ABCL](jsonL.noSpaces)
res3: Either[io.circe.Error,ABCL] = Right(Inr(Inl(B(xyz,123))))

The encoding for records similarly includes the member names:

scala> ('a ->> "xyz" :: 'b ->> List(1) :: 'c ->> false :: HNil).asJson.noSpaces
res4: String = {"c":false,"b":[1],"a":"xyz"}

In general if you want your hlist and coproduct encodings to include labels (and look like the encodings you'd get from circe-generic for case classes and sealed trait hierarchies), you can use records or coproducts to tell circe-shapes to do this with a minimal amount of extra syntactic and runtime overhead.

Travis Brown
  • 135,682
  • 12
  • 352
  • 654