abdolence / circe-tagged-adt-codec

Circe encoder/decoder implementation for ADT/JSON for Scala 2

Version Matrix

Circe encoder/decoder implementation for ADT to JSON with a configurable type field.

Maven Central

This library provides an efficient, type safe and macro based ADT to JSON encoder/decoder for circe with configurable JSON type field mappings.

Because in Scala 3 meta-programming is completely different, this codec was completely revamped for Scala 3, and it lives in a separate repository here.

When you have ADTs (as trait and case classes) defined like this

sealed trait TestEvent

case class MyEvent1(anyYourField : String /*, ...*/) extends TestEvent
case class MyEvent2(anyOtherField : Long /*, ...*/) extends TestEvent
// ...

and you would like to encode them to JSON like this:

  "type" : "my-event-1",
  "anyYourField" : "my-data", 
  "..." : "..."

The main objectives here are:

  • Avoid JSON type field in Scala case class definitions.
  • Configurable JSON type field values and their mapping to case classes. They don't have to be Scala class names.
  • Avoid writing circe Encoder/Decoder manually.
  • Check at the compile time JSON type field mappings and Scala case classes.

Scala support

  • Scala v2.12 / v2.13
  • Scala.js v1+

** For Scala 3 look here.

Getting Started

Add the following to your build.sbt:

libraryDependencies += "org.latestbit" %% "circe-tagged-adt-codec" % "0.10.0"

or if you need Scala.js support:

libraryDependencies += "org.latestbit" %%% "circe-tagged-adt-codec" % "0.10.0"


import io.circe._
import io.circe.parser._
import io.circe.syntax._

// This example uses auto coding for case classes. 
// You decide here if you need auto/semi/custom coders for your case classes.
import io.circe.generic.auto._ 

// One import for this ADT/JSON codec
import org.latestbit.circe.adt.codec._

sealed trait TestEvent

//@JsonAdt annotation is required only if you'd like to specify JSON type field value yourself. 
// Otherwise it would be the class name  

case class MyEvent1(anyYourField : String /*, ...*/) extends TestEvent
case class MyEvent2(anyOtherField : Long /*, ...*/) extends TestEvent

// Encoding

implicit val encoder : Encoder[TestEvent] = JsonTaggedAdtCodec.createEncoder[TestEvent]("type")

val testEvent : TestEvent = TestEvent1("test")
val testJsonString : String = testEvent.asJson.dropNullValues.noSpaces

// Decoding
implicit val decoder : Decoder[TestEvent] = JsonTaggedAdtCodec.createDecoder[TestEvent]("type")

decode[TestEvent] (testJsonString) match {
   case Right(model : TestEvent) => // ...

Configure and customise base ADT Encoder/Decoder implementation

In case you need a slightly different style of coding of your ADT to JSON, there is an API to change it.

Let's assume that you'd like to produce a bit different JSON like this:

  "type" : "my-event-1",
  "body" : {
    "anyYourField" : "my-data", 
     "..." : "..."

Then you should specify it with your own implementation:

implicit val encoder: Encoder[TestEvent] =
        createEncoderDefinition[TestEvent] { case (converter, obj) =>

            // converting our case classes accordingly to obj instance type
            // and receiving JSON type field value from annotation
            val (jsonObj, typeFieldValue) = converter.toJsonObject(obj)

            // Our custom JSON structure
                "type" -> Json.fromString(typeFieldValue),
                "body" -> Json.fromJsonObject(jsonObj)

implicit val decoder: Decoder[TestEvent] =
        createDecoderDefinition[TestEvent] { case (converter, cursor) =>
            // Reading JSON type field value
            cursor.get[Option[String]]("type").flatMap {
                case Some(typeFieldValue) =>
                    // Decode a case class from body accordingly to typeFieldValue
                        jsonTypeFieldValue = typeFieldValue,
                        cursor = cursor.downField("body")
                case _ =>
                    Decoder.failedWithMessage(s"'type' isn't specified in json.")(cursor)

Complex ADT definitions and trait inheritance

All the following examples are supported by this codec:

sealed trait MyTrait
case class MyCaseClass() extends MyTrait
// case objects
case object MyCaseObject extends MyTrait 

// trait inheritance with passing through tags - 
// so, direct children of MyTrait and 
// direct children of MyChildTrait now
// share the same tags namespace 
sealed trait MyChildTrait extends MyTrait 
case class MyChildCaseClass() extends MyChildTrait
case class MyOtherChildCaseClass() extends MyChildTrait

// Now this is has its own decoder/encoder 
// and the children of MyIsolatedChildTrait have their own tags
sealed trait MyIsolatedChildTrait extends MyTrait 
case class MyIsolatedCaseClass() extends MyIsolatedChildTrait
case class MyIsolatedOtherChildCaseClass() extends MyIsolatedChildTrait

// The same like previous, except here we now define our own tag on a child trait 
// (instead of default behaviour where a tag would be a trait name) 
sealed trait MySecondIsolatedChildTrait extends MyTrait 

Pure enum constants / case objects ADT definitions support

Sometimes you just need tags constants themselves for declarations like this, without any additional type tags and objects:

sealed trait MyEnum
case object Enum1 extends MyEnum

case object Enum2 extends MyEnum

to produce JSON strings for enum constants in json (instead of objects). To help with this scenario, ADT codec provides the specialized encoder and decoder implementations:

implicit val encoder : Encoder[MyEnum] = JsonTaggedAdtCodec.createPureEnumEncoder[MyEnum]()
implicit val decoder : Decoder[MyEnum] = JsonTaggedAdtCodec.createPureEnumDecoder[MyEnum]()


Apache Software License (ASL)


Abdulla Abdurakhmanov