Nomos logo

Schema-first JSON tooling for Scala

One template for your models, codecs, and validation.

Declare your JSON structure once. Nomos generates typed Scala models, codecs with no third-party JSON-library dependency, and runtime validation from that same template, keeping every representation aligned by construction.

Scala 2.12 Maven Maven Central

Why Nomos?

Nomos takes inspiration from schema-first ORM tooling, which generates application code from a central schema. It applies that workflow to JSON-heavy Scala systems, where models, serialization, and validation are often maintained separately and can drift apart. Define the JSON structure once, then generate the Scala interface used by application code.

TemplatesDeclare the structure
GenerateTypes & serializers
ValidateData matches schema
ScalaType-safe by design

A hand-written model, its codecs, and its validator are separate descriptions of the same data. Add a field, tighten a constraint, or rename a variant and each copy must change together. Nomos replaces that coordination with generation: one declarative JSON template produces the Scala types, serialization code, and embedded runtime validator.

Nomos is not an ORM and does not manage persistence. It is a build-time source generator for the boundary between JSON and Scala, designed to make that boundary typed, validated, lightweight at runtime, and straightforward to extend with domain-specific rules.

In Greek, nomos (νόμος) is the law — the agreed-upon order a community lives by. Nomos makes your template the law: the single declaration your Scala types and your JSON both answer to.

Compared to annotation libraries

Most JSON tooling annotates a hand-written model — serialization hints and validation constraints declared as field annotations (Gson's @SerializedName, Bean Validation's @NotNull/@Size, and the like). The class stays the source of truth, and serialization and validation rules are scattered across annotations on its fields. Nomos inverts that: the template is the source of truth, and the class, its (de)serialization, and its validator are generated from it.

Annotating a model

  • You author and maintain the class by hand.
  • Rules are spread across field annotations; the full schema isn't readable in one place.
  • The schema is bound to one class in one language — JSON consumers elsewhere can't reuse it.
  • Custom validation beyond single fields means annotations plus a separate validator class, wired onto the model.
  • Reordering or renaming means touching the class, its serialization hints, and its constraints together.

Declaring with Nomos

  • One declarative JSON template defines the structure; the class is generated.
  • Types, constraints, and structure live together in the declaration.
  • The same template validates JSON, embeds in the artifact, and remains the shared source of truth.
  • Custom validators (any check over the payload, such as comparing or combining several fields) are named in the template and registered as functions; formats and adapters extend the same way.
  • Field order and naming come from one place and stay consistent across model, JSON, and validation.

The trade-off is a build-time generation step instead of plain hand-written classes — in return, the model and its validation are always derived from the same declaration.

A quick look

Drop a template under your resources, matching the package you want: src/main/resources/nomos/templates/com/example/models/user.json

{
  "definitions": [
    {
      "name": "User",
      "subPackage": "user",
      "template": {
        "id": "string",
        "username": { "type": "string", "minLength": 1 },
        "email": { "type": "string", "format": "email" },
        "age": { "$optional": "int" },
        "roles": ["string"]
      }
    }
  ]
}

Run mvn generate-sources, and Nomos writes a case class plus a companion with parse, serialize, and validate:

import com.example.models.user.User

// Validate against the embedded template, then parse — no template file needed at runtime
User.validate("""{"id":"1","username":"jo","email":"jo@x.com","roles":["admin"]}""") match {
  case Right(user)  => println(user.email)
  case Left(errors) => errors.foreach(e => println(e.fullMessage))
}

// Or round-trip directly
val json = User.toJson(user)
val back = User.fromJson(json)   // Either[String, User]

What you get

Rich type system

Strings, numbers (int, long, decimal, double), date/datetime, booleans, arrays, maps, unions, enums, defaults, and nullable raw fields.

Validation built in

The template is embedded in the generated code, so every model validates JSON at runtime with no separate schema to ship or keep in sync.

Polymorphic families

Type discriminators become sealed traits with variants — including $ref bodies, prefix- and regex-matched tags, and shared variant collapse.

Extensible

Register named custom validators, custom string formats, and (de)serialization adapters via Nomos.validators, Nomos.formats, and Nomos.adapters.

Build-time generation

A Maven plugin scans your templates and generates sources during generate-sources, into target/generated-sources/nomos (added as a compile root) by default.

Multi-file projects

Split a large model one template per package; $ref resolves across files into a shared definition space.

Where to next