File structure

A template has a top-level definitions array, plus optional project-wide settings. The base package comes from the file's path under src/main/resources/nomos/templates/.

{
  "listType": "List",
  "definitions": [
    {
      "name": "User",
      "subPackage": "user",
      "description": "A user record",
      "template": { "id": "string" }
    }
  ]
}
Top-level fieldDefaultMeaning
definitions—Array of type definitions (required)
listType"List"Collection type for arrays (List or Array)
mapType"Map"Collection type for $map fields: Map (immutable) or java.util.Map for a Java-interop-friendly surface
dateType / dateTimeTypejava.time.LocalDate / LocalDateTimeGenerated temporal types
fromJsonStyle"either"Only "throwing" is special (fromJson throws on failure); any other value, including the default, yields the Either-returning fromJson. See Code Generation.
visibility—Access prefix applied to every generated top-level definition (e.g. private, private[models]) for package-private models.

These settings are project-wide. In a multi-file project, set a value in one template and it applies everywhere — you don't repeat it in every file. If two files set the same setting to different values, generation fails with an error naming the setting, so the result is never ambiguous. Any unrecognized top-level key is rejected, so a typo or removed setting fails loudly instead of being silently ignored.

Scalar types

TemplateScala
"string"String
"number" / "double"Double
"int"Int (whole numbers only)
"long"Long (whole numbers only)
"decimal"BigDecimal
"boolean"Boolean
"date" / "datetime"java.time.LocalDate / LocalDateTime

Constraints

Use the long form { "type": ..., ...constraints } to attach validation constraints.

{
  "username": { "type": "string", "minLength": 1, "maxLength": 40, "pattern": "^[a-z0-9_]+$" },
  "email":    { "type": "string", "format": "email" },
  "age":      { "type": "int", "min": 0, "max": 150 },
  "ratio":    { "type": "number", "multipleOf": 0.5 }
}
Applies toConstraints
stringminLength, maxLength, pattern, format, enum
number / int / long / decimalmin, max, multipleOf
arrayminItems, maxItems, uniqueItems, uniqueBy

Built-in format values (register more — see Validation):

FormatAccepts
emailAn email address
urlAn http/https URL
uuid / guidA UUID/GUID (hex, any case)
uuidUpper/guidUpper, uuidLower/guidLowerA GUID restricted to upper- or lower-case hex
iso8601An ISO-8601 UTC timestamp (YYYY-MM-DDThh:mm:ssZ)
alpha / alphaNoWhitespaceLetters only
alphaUpper / alphaLowerUpper- or lower-case letters only
alphanumericLetters and digits
alphanumericUpper / alphanumericLowerUpper- or lower-case letters and digits
pascalCaseAn upper-case-initial identifier (Foo, UserV2)
camelCaseA lower-case-initial identifier (fooBar)
snakeCaseLower-case words joined by _ (foo_bar)
kebabCaseLower-case words joined by - (foo-bar)
screamingSnakeCaseUpper-case words joined by _ (FOO_BAR)
majorAndMinorA major.minor version (e.g. 1.2)

A required string field may also declare an adapter to map between its on-the-wire form and the model value, e.g. { "type": "string", "adapter": "epochMillis" }. Adapters are accepted only on required string fields — see Validation.

Enums

A closed value set either validates a String, or generates a dedicated enum type.

To validate a string against a closed set:

{ "priority": { "type": "string", "enum": ["low", "medium", "high"] } }

To generate a sealed-trait enum type named Priority and type the field as it:

{ "priority": { "type": "string", "enum": ["low", "high"], "as": "enumType", "name": "Priority" } }

Arrays & maps

{
  "tags":   ["string"],
  "scores": { "type": "array", "items": "number", "minItems": 1, "uniqueItems": true },
  "entries": { "type": "array", "items": "$ref:Entry", "uniqueBy": "id" },
  "labels": { "type": "array", "items": "string", "enum": ["red", "green", "blue"] },
  "settings": { "$map": "string" }
}

uniqueItems compares whole elements, so two objects sharing an id but differing elsewhere both pass it. uniqueBy compares only the named field, and a list of names forms a composite key ("uniqueBy": ["group", "name"]). A duplicate is reported at the offending element's index, naming the key fields. Elements that are not objects, or that omit a key field, carry no key and are skipped — their shape is already reported by the element's own type check.

$map generates a Map[String, T] with unrestricted keys and validated values. Set the project-wide mapType to java.util.Map to generate java.util.Map[String, T] instead — useful when the generated models are consumed from Java.

A JSON array with more than one element type is a union. It generates an Any-typed field whose value round-trips as-is (the codec passes the raw JSON through, so branches aren't validated individually):

{ "value": ["string", "number"] }

Extra object properties

By default an object rejects any property not named in its template. $additionalProperties relaxes that:

{ "id": "string", "$additionalProperties": "string" }
ValueEffect
absent or falseReject any property not in the template (the default)
trueAllow any extras, collected as Map[String, Any] (kept but unvalidated)
a type (e.g. "string")Allow extras typed as Map[String, T]; each extra value is validated against that type

Presence groups

Some objects offer several mutually exclusive ways to say the same thing — an inline value or a reference to one, but not both. Each key is optional on its own, and a presence group declared on the enclosing object decides how many of them must be present.

{
  "$oneOf": ["inline_value", "value_ref"],
  "inline_value": { "$optional": "string" },
  "value_ref":    { "$optional": "string" }
}
DirectiveRequires
$oneOfExactly one of the listed keys is present
$atLeastOneOne or more of the listed keys is present

Wrap the key list in $optional to also accept none of them being present — a $oneOf group still rejects more than one:

{ "$oneOf": { "$optional": ["inline_value", "value_ref"] } }

A key present with a JSON null counts as absent, matching how decoding treats null for an optional field. Both directives may appear on the same object (and on a discriminator variant), and each is checked independently.

The listed keys must be declared siblings and must be optional — the group, not the field, decides what is required. A group naming an undeclared key, a required key, a duplicate key, or fewer than two keys is a template error rather than a rule that can never hold.

Optional, nullable & defaults

{
  "nickname": { "$optional": "string" },
  "tags":     { "$optional": ["string"], "nullable": true },
  "region":   { "$optional": "string", "nullable": "surfaceOnly" },
  "retries":  { "$optional": "int", "nullable": true },
  "verbose":  { "type": "boolean", "default": false }
}

nickname becomes Option[String]. nullable: true generates a raw nullable type instead of Option and accepts an explicit JSON null. nullable: "surfaceOnly" generates the same raw, null-defaulted surface but rejects an explicit JSON null; omission still decodes to null. Because a Scala value type like Int can't hold null, a nullable int is generated as its boxed Java wrapper — e.g. retries: java.lang.Integer (a reference type, so it can be null) rather than retries: Int (which can't). verbose becomes verbose: Boolean = false.

References

{
  "address":  "$ref:Address",
  "shipping": "$gen:com.example.other.Address",
  "rule":     "$extern:com.example.legacy.Rule"
}

Self-references (recursive types) work too: a TreeNode may contain ["$ref:TreeNode"].

ReferenceMeaning
$ref:NameAnother definition anywhere in the project. A simple name resolves to a same-package definition first, otherwise to the unique definition with that name. An ambiguous simple name (several definitions share it, none in the referrer's package) is an error — disambiguate with $gen:.
$gen:fully.qualified.NameAnother Nomos-generated type by fully-qualified name. Emits a direct Name.decode/Name.encode (no registry), and is how you disambiguate an otherwise-ambiguous simple $ref.
$extern:fully.qualified.NameAn opaque, hand-written type Nomos neither generates nor schema-validates. (De)serialization goes through the runtime CodecRegistry and fails closed if no codec is registered for the type.

Type discriminators

A discriminator becomes a sealed trait with a case-class variant per value.

Prefix matching supports parameterized tags while preserving the complete discriminator value:

{
  "$type": {
    "discriminator": "kind",
    "variantMatch": "prefix",
    "variants": {
      "Decimal": { "scale": "int" },
      "String": {}
    }
  }
}

Regex matching constrains the complete tag syntax and requires an explicit class name for every pattern:

{
  "$type": {
    "discriminator": "kind",
    "variantMatch": "regex",
    "variantNames": {
      "^Decimal(\\((\\d{1,2}),(\\d{1,2})\\))?$": "DecimalColumn",
      "^Circle$": "Circle"
    },
    "variantSubPackage": "shapes",
    "commonFields": { "label": "string" },
    "variants": {
      "^Decimal(\\((\\d{1,2}),(\\d{1,2})\\))?$": { "scale": "int" },
      "^Circle$":  "$ref:Circle"
    }
  }
}
OptionEffect
variantsMap of discriminator value → variant body (inline object or $ref)
commonFieldsFields shared by every variant
variantNamesMap several values to one class to collapse identical variants
variantMatchexact (the default) matches a literal value; prefix matches values beginning with the variant key; regex applies each key as a full-string regular expression in declaration order. Regex variants require a variantNames entry for every pattern, and invalid patterns are rejected during parsing.
variantSubPackagePlace variant case classes in a sub-package
fallbackVariantName of a catch-all variant that captures any unmatched discriminator value, preserving the raw value and payload so unknown types round-trip instead of failing.
discriminatorEnumRender the discriminator field as a generated enum type instead of a raw String. Combined with fallbackVariant, the enum is open — it gains an Unknown(value) member so unrecognized values round-trip. See Code Generation.
includeDiscriminatorMust be true (the default); false is rejected at generation because such a union cannot decode its own output.

Reserved keys & escaping

A field literally named like a template keyword (type, enum, …) must be backtick-quoted so it's read as a field, not a directive. Nomos strips the backticks and re-escapes Scala keywords in the output.

{ "name": "string", "`type`": "string" }

Next: Validation — how the embedded template validates JSON, and how to extend it.