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 val
s under the hood, so we don’t have to make the distinction ourselves anymore.