Template Format
A template is a JSON file describing one or more named type definitions. Its location sets the base package; its contents drive the generated types — a case class per object definition, or a sealed trait plus variant case classes per discriminator — and their validators.
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 field | Default | Meaning |
|---|---|---|
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 / dateTimeType | java.time.LocalDate / LocalDateTime | Generated 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
| Template | Scala |
|---|---|
"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 to | Constraints |
|---|---|
| string | minLength, maxLength, pattern, format, enum |
| number / int / long / decimal | min, max, multipleOf |
| array | minItems, maxItems, uniqueItems, uniqueBy |
Built-in format values (register more — see Validation):
| Format | Accepts |
|---|---|
email | An email address |
url | An http/https URL |
uuid / guid | A UUID/GUID (hex, any case) |
uuidUpper/guidUpper, uuidLower/guidLower | A GUID restricted to upper- or lower-case hex |
iso8601 | An ISO-8601 UTC timestamp (YYYY-MM-DDThh:mm:ssZ) |
alpha / alphaNoWhitespace | Letters only |
alphaUpper / alphaLower | Upper- or lower-case letters only |
alphanumeric | Letters and digits |
alphanumericUpper / alphanumericLower | Upper- or lower-case letters and digits |
pascalCase | An upper-case-initial identifier (Foo, UserV2) |
camelCase | A lower-case-initial identifier (fooBar) |
snakeCase | Lower-case words joined by _ (foo_bar) |
kebabCase | Lower-case words joined by - (foo-bar) |
screamingSnakeCase | Upper-case words joined by _ (FOO_BAR) |
majorAndMinor | A 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" }
| Value | Effect |
|---|---|
absent or false | Reject any property not in the template (the default) |
true | Allow 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" }
}
| Directive | Requires |
|---|---|
$oneOf | Exactly one of the listed keys is present |
$atLeastOne | One 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"].
| Reference | Meaning |
|---|---|
$ref:Name | Another 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.Name | Another 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.Name | An 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"
}
}
}
| Option | Effect |
|---|---|
variants | Map of discriminator value → variant body (inline object or $ref) |
commonFields | Fields shared by every variant |
variantNames | Map several values to one class to collapse identical variants |
variantMatch | exact (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. |
variantSubPackage | Place variant case classes in a sub-package |
fallbackVariant | Name 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. |
discriminatorEnum | Render 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. |
includeDiscriminator | Must 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.