I'm using sealed traits as enums for exhaustive pattern matching. In cases where I have case objects instead of case classes extending my trait, I'd like to encode and decode (via Circe) as just a plain string.
For example:
sealed trait State
case object On extends State
case object Off extends State
val a: State = State.Off
a.asJson.noSpaces // trying for "Off"
decode[State]("On") // should be State.On
I understand that this will be configurable in 0.5.0, but can anyone help me write something to tide me over until that's released?
To highlight the problem—assuming this ADT:
circe's generic derivation will (currently) produce the following encodings:
This is because the generic derivation mechanism is built on Shapeless's
LabelledGeneric
, which represents case objects as emptyHList
s. This will probably always be the default behavior, since it's clean, simple, and consistent, but it's not always what you want (as you note the configuration options that are coming soon will support alternatives).You can override this behavior by providing your own generic instances for case objects:
This says, "if the generic representation of
A
is an emptyHList
, encode it as its name as a JSON string". And it works as we'd expect for case objects that are statically typed as themselves:When the value is statically typed as the base type, the story is a little different:
We get a generically derived instance for
State
, and it respects our manually defined generic instance for case objects, but it still wraps them in an object. This makes some sense if you think about it—the ADT could contain case classes, which can only reasonably be represented as a JSON object, and so the object-wrapper-with-constructor-name-key approach is arguably the most reasonable thing to do.It's not the only thing we can do, though, since we do know statically whether the ADT contains case classes or only case objects. First we need a new type class that witnesses that an ADT is made up only of case objects (note that I'm assuming a fresh start here, but it should be possible to make this work alongside generic derivation):
And then our generic
Encoder
instances:Might as well go ahead and write the decoder too.
And then:
Which is what we wanted.