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 calledGreetingwith a single field,message, of typeString. -
Type class derivation with
derives: Thederives JsonEncoderpart tells Scala to automatically derive an implementation of theJsonEncodertype class for theGreetingclass.- JsonEncoder: This is a type class provided by ZIO JSON that enables an instance of
Greetingto be converted to JSON. When you deriveJsonEncoder, Scala will automatically create aJsonEncoderinstance for theGreetingcase 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 theJsonEncoderinterface based on the structure of theGreetingclass. This means it will create a JSON representation of aGreetinginstance, mapping themessagefield 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
JsonEncoderimplementation 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 aGreetinginstance 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]

