Derivation FAQ

Fully-automatic derivation (via the deriveAll... macros) comes with a few special cases/gotchas that are detailed on this page.

Custom Overrides

Fully-automatic derivation of encoders/decoders for ADTs allows for providing custom codecs for a subsets of the type hierarchy. If an implicit typeclass for a certain sub-type is already available at the deriveAll... macro call site then this implicit will be used rather than a new (and potentially conflicting) one generated.

Example:

sourcesealed trait Animal
case class Dog(age: Int, name: String)                      extends Animal
case class Cat(weight: Double, color: String, home: String) extends Animal
case class Fish(color: String)                              extends Animal
case object Yeti                                            extends Animal
sourceimport io.bullet.borer.{Codec, Json}
import io.bullet.borer.derivation.MapBasedCodecs.*

// custom codec only for `Fish`
given Codec[Fish] =
  Codec.bimap[Int, Fish](_ => 0, _ => Fish("red"))

// let borer derive the codecs for the rest of the Animal ADT
given Codec[Animal] = deriveAllCodecs

val animal: Animal = Fish("blue")

Json.encode(animal).toUtf8String ==> """{"Fish":0}"""

If you provide a custom codec for a whole abstract branch of the ADT hierarchy then borer will offload encoding and decoding of all sub-types of that branch to your custom codec.

Explicit Upcasts

Sometimes the interplay of typeclasses and ADTs can become a bit confusing, especially when explicit upcasts are required.

The important thing to remember is that borer’s typeclasses (Encoder[T], Decoder[T] and Codec[T]) are intentionally invariant in their type parameters. This means that they will only be found and used when the types match up exactly.

With this ADT, for example:

sourcesealed trait Animal
case class Dog(age: Int, name: String)                      extends Animal
case class Cat(weight: Double, color: String, home: String) extends Animal
case class Fish(color: String)                              extends Animal
case object Yeti                                            extends Animal

the following codec definition gives you an Encoder[Animal] and a Decoder[Animal],
nothing more, nothing less:

sourceimport io.bullet.borer.Codec
import io.bullet.borer.derivation.MapBasedCodecs.*

given Codec[Animal] = deriveAllCodecs[Animal]

It does not give you an Encoder[Dog] or a Decoder[Cat]!
If you want codecs for specific ADT sub-types like Dog or Cat you need to define them in addition to the codec for the ADT super-type, e.g. like this:

sourceimport io.bullet.borer.Codec
import io.bullet.borer.derivation.MapBasedCodecs.*

given Codec[Dog]    = deriveCodec[Dog]
given Codec[Cat]    = deriveCodec[Cat]
given Codec[Animal] = deriveAllCodecs[Animal]

An alternative would be to explicitly upcast a value of a more specific type to the ADT super type, e.g. like this:

sourceimport io.bullet.borer.Json

val animal: Animal = Dog(2, "Rex")

// Json.encode(animal).toByteArray or
Json.encode(animal).toUtf8String ==> """{"Dog":{"age":2,"name":"Rex"}}"""

However, note that the encoding of a Dog as an Animal is not the same as the encoding of the Dog itself!! The former includes the wrapping layer with the type-id while the latter doesn’t:

sourceimport io.bullet.borer.Json

val dog = Dog(2, "Rex")
Json.encode(dog: Animal).toUtf8String ==> """{"Dog":{"age":2,"name":"Rex"}}"""
Json.encode(dog        ).toUtf8String ==> """{"age":2,"name":"Rex"}"""

This is because the Decoder[Animal] needs to somehow receive the information which Animal type to decode into, while the Decoder[Dog] doesn’t, as it already knows the concrete target exactly.

So, while explicit upcasts are sometimes what you want there are also cases where they are not what you want. The exact target types of your (de)serializations depends on your specific use case and, as such, should be chosen carefully.

Recursive ADTs

Before Scala 3 the codecs for non-generic ADTs were mostly assigned to an implicit val in order to not be recreated on every use. If the ADT is recursive this val had to be converted into an implicit lazy val with an explicit type annotation instead, as in this example:

sourceimport io.bullet.borer.Codec
import io.bullet.borer.derivation.MapBasedCodecs.*

sealed trait TreeNode
case object Leaf                                 extends TreeNode
case class Node(left: TreeNode, right: TreeNode) extends TreeNode

implicit lazy val codec: Codec[TreeNode] = deriveAllCodecs

While this still works with Scala 3 we can now resort to a simple given definition for all use-cases:

sourceimport io.bullet.borer.Codec
import io.bullet.borer.derivation.MapBasedCodecs.*

sealed trait TreeNode
case object Leaf                                 extends TreeNode
case class Node(left: TreeNode, right: TreeNode) extends TreeNode

given Codec[TreeNode] = deriveAllCodecs

The Scala 3 compiler compiles all such given definitions into the equivalent of lazy vals under the hood, so we don’t have to make the distinction ourselves anymore.