Scala 3 FAQ: How does this ZIO HTTP and ZIO JSON code works, specifically the derives JsonEncoder
portion of the code:
case class Greeting(message: String) derives JsonEncoder
Solution
In Scala 3, the derives
keyword is used to automatically generate implementations for type classes. Here’s how this works in the context of ZIO JSON.
Code explanation
Given this code:
case class Greeting(message: String) derives JsonEncoder
-
Case class definition:
case class Greeting(message: String)
defines a case class calledGreeting
with a single field,message
, of typeString
. -
Type class derivation with
derives
: Thederives JsonEncoder
part tells Scala to automatically derive an implementation of theJsonEncoder
type class for theGreeting
class.- JsonEncoder: This is a type class provided by ZIO JSON that enables an instance of
Greeting
to be converted to JSON. When you deriveJsonEncoder
, Scala will automatically create aJsonEncoder
instance for theGreeting
case class, so you don’t need to write any manual encoding logic.
- JsonEncoder: This is a type class provided by ZIO JSON that enables an instance of
-
How it works: When you use
derives JsonEncoder
, Scala automatically generates code that conforms to theJsonEncoder
interface based on the structure of theGreeting
class. This means it will create a JSON representation of aGreeting
instance, mapping themessage
field to a JSON key-value pair like{ "message": "Hello!" }
.
How derivation works internally
The derives
keyword leverages Scala 3's new metaprogramming capabilities. Libraries like ZIO JSON provide derivation macros that inspect the structure of the case class and produce a JsonEncoder
implementation without you needing to write boilerplate code. For example, in ZIO JSON:
- The library includes a
JsonEncoder
implementation that uses macros to automatically generate an encoder for case classes, based on the fields in the class.
More details on derives, type classes, and macros
derives
is a Scala 3 feature that automatically generates type class instances — in this case, a JsonEncoder
— using compile-time derivation. It’s similar to older approaches like @deriveJsonEncoder
annotations in Scala 2, but with cleaner syntax.
Here’s how it works:
-
When you write
derives JsonEncoder
, the compiler will automatically generate the code needed to convert aGreeting
instance to JSON. -
Behind the scenes, it creates something roughly equivalent to:
case class Greeting(message: String)
object Greeting {
implicit val jsonEncoder: JsonEncoder[Greeting] = new JsonEncoder[Greeting] {
def encode(greeting: Greeting): Json =
Json.Obj("message" -> Json.Str(greeting.message))
}
}
Other
You can also derive multiple type classes at once:
case class Greeting(message: String) derives JsonEncoder, JsonDecoder
This is particularly useful when you need both encoding (to JSON) and decoding (from JSON).
Benefits
The main benefits of using derives
are:
- Less boilerplate code
- Automatic updates if you change the case class fields
- Compile-time generation (better performance than runtime reflection)
Bonus: derives compared to using “given” instances
As an additional note, this code:
case class ToDo(
id: String,
task: String,
completed: Boolean
) derives JsonEncoder, JsonDecoder
is the equivalent of this much-longer code:
case class ToDo(
id: String,
task: String,
completed: Boolean
)
object ToDo:
given JsonEncoder[ToDo] = DeriveJsonEncoder.gen[ToDo]
given JsonDecoder[ToDo] = DeriveJsonDecoder.gen[ToDo]