Last active
March 2, 2025 13:32
Compile-Time JSON Encoding for Scala Case Classes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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