A Scala 3 library that leverages type classes and macros to derive JSON schemas from case classes at compile time.
This project is purely experimental and was created for me to experiment with Claude Code and explore vibe-based coding workflows. It's a learning exercise and playground for testing AI-assisted development patterns. Use at your own risk!
Add to your build.sbt:
libraryDependencies += "io.github.ramytanios" %% "json-schema-lib" % "<version>"
libraryDependencies += "io.github.ramytanios" %% "json-schema-lib-excel" % "<version>"deriveskeyword support - Idiomatic Scala 3 derivation at the definition site- Compile-time schema generation - Zero runtime overhead with macro-based derivation
- Primitive type support - String, Int, Long, Float, Double, Boolean
java.timesupport -LocalDateβdate,LocalTime/OffsetTimeβtime,Instant/LocalDateTime/OffsetDateTime/ZonedDateTimeβdate-time,Durationβduration- Scala duration support -
scala.concurrent.duration.DurationandFiniteDurationmap to{ "type": "string" }(no standard JSON Schema format exists for Scala durations) java.util.UUIDsupport -UUIDmaps to{ "type": "string", "format": "uuid" }- Enum support - Scala 3 enums automatically map to JSON Schema enums
- Nested case class support - Case class fields are recursively inlined into the schema
- Seq support - Mutable and immutable sequences
- Map support -
Map[String, V]maps to{ "type": "object", "additionalProperties": ... } - Option support - Optional fields automatically excluded from required list
- Circe integration - Built-in JSON encoding for schemas
$schemadeclaration - Annotate the root schema with a JSON Schema draft URI viawithSchemaVersion
import jsonschema.*
enum Role:
case Admin, User, Guest
case class Address(street: String, city: String)
// Idiomatic `derives` syntax (recommended)
@Title("User profile")
case class Profile(
@MinLength(3) @MaxLength(50) username: String,
@MinimumInt(18) @MaximumInt(120) age: Int,
active: Boolean,
role: Role,
address: Address,
@MinItems(1) tags: List[String],
bio: Option[String]
) derives JsonSchema
// Equivalent explicit form (also supported)
// object Profile:
// given JsonSchema[Profile] = DeriveJsonSchema.derived
val json = JsonSchema[Profile].schema
.withSchemaVersion(JsonSchemaVersion.Draft202012)
.toJsonGenerated JSON Schema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"title": "User profile",
"properties": {
"username": { "type": "string", "minLength": 3, "maxLength": 50 },
"age": { "type": "integer", "minimum": 18, "maximum": 120 },
"active": { "type": "boolean" },
"role": { "type": "string", "enum": ["Admin", "User", "Guest"] },
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" }
},
"required": ["street", "city"]
},
"tags": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
"bio": { "type": "string" }
},
"required": ["username", "age", "active", "role", "address", "tags"]
}Nested case classes are inlined (no $ref) and work to arbitrary depth.
Limitation: Mutually recursive case classes (e.g.
AcontainsB,BcontainsA) will cause a compile-time stack overflow. Workaround: provide an explicitgiven JsonSchema[B]before derivingA.
An optional module (excel) exposes Scala functions as Excel custom functions via an HTTP server.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Excel β
β β
β ββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββ β
β β Functions runtime β β Task pane β β
β β (functions.html) β β (taskpane.html) β β
β β β β β β
β β on startup β β on load / "Reload Functions" β β
β β ββββββββββββββββββ β β ββββββββββββββββββββββββββββββ β β
β β βloadAndRegister ββββΌβββββββββββββΌββΆβ GET /functions.json β β β
β β ββββββββββββββββββ β β ββββββββββββββββββββββββββββββ β β
β β β β β re-render list β β
β β poll every 2 s β β βΌ β β
β β ββββββββββββββββββ β signal β OfficeRuntime.storage β β
β β βOfficeRuntime ββββΌβββββββββββββΌβ .setItem("cf-reload-signal") β β
β β β.storage.getItemβ β β β β
β β βββββββββ¬βββββββββ β ββββββββββββββββββββββββββββββββββββ β
β β β detected β β
β β βΌ β β
β β ββββββββββββββββββ β β
β β βloadAndRegister β β (all IDs re-associated on every reload) β
β β β (full reload) β β β
β β βββββββββ¬βββββββββ β β
β β β β β
β ββββββββββββΌββββββββββββ β
βββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β POST /invoke {functionId, params}
βΌ
βββββββββββββββββββ
β Scala server β
β (http4s/ember) β
βββββββββββββββββββ
/functions.jsonβ served at startup from the in-memory function list; always reflects the running server's functions./functions.jsβ fetches/functions.jsonat runtime and callsCustomFunctions.associate()dynamically; pollsOfficeRuntime.storageevery 2 s for a reload signal.- Reload without add-in restart β clicking "Reload Functions" signals the runtime to re-associate all functions from scratch and refreshes the taskpane list. Formula-bar autocomplete for brand-new functions still requires a full add-in reload (Excel limitation).