Skip to content

Instantly share code, notes, and snippets.

@depareddy
Last active March 2, 2025 13:32
Compile-Time JSON Encoding for Scala Case Classes
package com.example.macros
import scala.deriving.Mirror
import scala.compiletime.{erasedValue, constValue, summonInline}
// Domain Model
case class Address(street: String, postalcode: String, city: String, country: String)
case class Student(name: String, age: Int, address: Address)
case class Classroom(name: String, students: List[Student])
case class School(name: String, classrooms: List[Classroom])
object MacroJson {
trait JsonEncoder[T] {
def encode(value: T): String
}
object JsonEncoder {
// Primitive type encoders
given stringEncoder: JsonEncoder[String] with
def encode(value: String): String = s""""$value""""
given intEncoder: JsonEncoder[Int] with
def encode(value: Int): String = value.toString
given booleanEncoder: JsonEncoder[Boolean] with
def encode(value: Boolean): String = value.toString
// List encoder
given listEncoder[T](using encoder: JsonEncoder[T]): JsonEncoder[List[T]] with
def encode(list: List[T]): String =
list.map(encoder.encode).mkString("[", ", ", "]")
// Case class derivation
inline given derived[T](using m: Mirror.Of[T]): JsonEncoder[T] = new JsonEncoder[T] {
def encode(value: T): String = {
inline m match {
case p: Mirror.ProductOf[T] =>
val labels = summonLabels[p.MirroredElemLabels]
val encoders = summonEncoders[p.MirroredElemTypes]
val values = value.asInstanceOf[Product].productIterator.toList
val encodedFields = (labels, encoders, values).zipped.map {
(label, encoder, value) =>
s""""$label": ${encoder.asInstanceOf[JsonEncoder[Any]].encode(value)}"""
}
s"{${encodedFields.mkString(", ")}}"
case _ => throw new IllegalArgumentException(s"Unsupported type: ${m.toString}")
}
}
}
// Helper to summon field labels
private inline def summonLabels[T <: Tuple]: List[String] = inline erasedValue[T] match {
case _: EmptyTuple => Nil
case _: (head *: tail) => constValue[head].toString :: summonLabels[tail]
}
// Helper to summon encoders for each field type
private inline def summonEncoders[T <: Tuple]: List[JsonEncoder[_]] =
inline erasedValue[T] match {
case _: EmptyTuple => Nil
case _: (t *: ts) => summonInline[JsonEncoder[t]] :: summonEncoders[ts]
}
}
// Public interface
def encode[T](value: T)(using encoder: JsonEncoder[T]): String = encoder.encode(value)
}
@main
def TestSchool(): Unit = {
import MacroJson.{encode, JsonEncoder}
import JsonEncoder.given
val address1 = Address("123 Main St", "12345", "Anytown", "USA")
val address2 = Address("456 Elm St", "67890", "Othertown", "USA")
val student1 = Student("Alice", 20, address1)
val student2 = Student("Bob", 22, address2)
val classroom1 = Classroom("Math 101", List(student1, student2))
val classroom2 = Classroom("History 101", List(student1))
val school = School("Scala Academy", List(classroom1, classroom2))
val schoolJson = encode(school)
println("Generated JSON:\n" + schoolJson)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment