Structured data · Validation
JSON Schema validation.
A JSON file tells Nift what your data is. A JSON Schema can additionally describe what that data is allowed to look like: which properties must exist, which values are strings or numbers, which arrays contain which kinds of items, and which values are permitted.
It is a widely used standard for describing and validating JSON documents. Nift understands a deliberately useful Draft 2020-12-compatible subset, so a schema can remain ordinary JSON and can also be understood by editors, CI tools and other software outside Nift.
The problem it solves
Without a schema, this is perfectly valid JSON:
{
"name": "Coffee mug",
"price": "free",
"published": "yes"
} But your template may expect price to be a number and published to be a boolean. The JSON parser can only tell you that the file is syntactically valid; it cannot know your application's intended shape.
A schema lets you state that contract:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["name", "price", "published"],
"properties": {
"name": { "type": "string", "minLength": 1 },
"price": { "type": "number", "minimum": 0 },
"published": { "type": "boolean" }
},
"additionalProperties": false
} Then load and validate the data in one operation:
@json('data/product.json', product, 'schemas/product.schema.json') Nift parses the JSON, parses the schema, validates the document, and only creates the product binding if validation succeeds.
What a failure looks like
If price is the string "free" instead of a number, the build fails rather than allowing a bad assumption to reach deeper into the template:
json: data/product.json does not satisfy schema schemas/product.schema.json
(at $.price: expected number, received string) The $ means “the root of the JSON document.” Therefore $.price means the price property on the root object, while a path such as $.products[3].price points to an exact nested value.
A realistic collection example
Suppose data/products.json contains:
{
"products": [
{
"name": "Desk lamp",
"slug": "desk-lamp",
"price": 79.95,
"status": "published",
"featured": true
},
{
"name": "Notebook",
"slug": "notebook",
"price": 12,
"status": "draft",
"featured": false
}
]
} A schema for the collection can describe both the outer object and every product inside the array:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["products"],
"properties": {
"products": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "slug", "price", "status"],
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"slug": {
"type": "string",
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
},
"price": {
"type": "number",
"minimum": 0
},
"status": {
"enum": ["draft", "published", "archived"]
},
"featured": {
"type": "boolean"
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
} Once validation succeeds, the template can use the same ordinary Nift data features:
@json('data/products.json', data, 'schemas/products.schema.json')
<div class="products">
@for(product : data.products by product.price asc) {
@if(product.status == "published") {
<article>
<h2>$[product.name]</h2>
<p>$[product.price]</p>
</article>
}
}
</div> required: make fields mandatory
{
"type": "object",
"required": ["title", "date"],
"properties": {
"title": { "type": "string" },
"date": { "type": "string" }
}
} This does not mean every property listed under properties is automatically required. properties describes fields when present; required names the fields that must be present.
type: prevent accidental shape changes
| Schema type | Accepts |
|---|---|
object | JSON objects such as {"name":"Nift"} |
array | JSON arrays such as [1,2,3] |
string | JSON strings |
number | Any finite JSON number |
integer | Numbers with no fractional part |
boolean | true or false |
null | null |
A type can also be an array when more than one type is intentionally valid:
{
"type": ["string", "null"]
} additionalProperties: catch typos
This is one of the most useful schema rules for project content:
{
"type": "object",
"properties": {
"description": { "type": "string" }
},
"additionalProperties": false
} If somebody accidentally writes "descripton", the schema rejects it instead of silently creating a property the template never reads.
additionalProperties can also itself be a schema. For example, this object may contain arbitrary keys, but every value must be a string:
{
"type": "object",
"additionalProperties": {
"type": "string"
}
} Arrays: items, sizes, uniqueness and contains
{
"type": "array",
"minItems": 1,
"maxItems": 20,
"uniqueItems": true,
"items": {
"type": "string",
"minLength": 1
}
} items applies one schema to every array item. contains can require that at least one item matches another schema; minContains/maxContains constrain how many matches are acceptable.
Numbers: bounds and multiples
{
"type": "number",
"minimum": 0,
"exclusiveMaximum": 100,
"multipleOf": 0.5
} Nift supports minimum, maximum, exclusiveMinimum, exclusiveMaximum and multipleOf.
Strings: length and patterns
{
"type": "string",
"minLength": 3,
"maxLength": 40,
"pattern": "^[a-z0-9-]+$"
} pattern uses C++'s ECMAScript-style regular-expression grammar. Length constraints count UTF-8 code points rather than raw bytes.
enum and const
Use enum when a value must come from a small known set:
{
"enum": ["draft", "published", "archived"]
} Use const when exactly one JSON value is permitted:
{
"const": "product"
} Reuse schemas with $defs and local $ref
Large schemas do not need to repeat the same object shape everywhere. Put reusable schemas under $defs and reference them with a local JSON Pointer:
{
"$defs": {
"person": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" },
"url": { "type": "string" }
},
"additionalProperties": false
}
},
"type": "object",
"properties": {
"author": {
"$ref": "#/$defs/person"
}
}
} Nift intentionally supports local $ref values only. It does not download remote schemas during a build. Keeping validation project-local makes builds deterministic, fast and usable offline.
Combine rules with allOf, anyOf, oneOf and not
{
"allOf": [
{ "type": "number" },
{ "minimum": 0 }
]
} allOf requires every child schema to match; anyOf requires at least one; oneOf requires exactly one; not requires its child schema not to match.
The schema is part of the dependency graph
@json('data/products.json', products, 'schemas/products.schema.json') records both files as dependencies. If the data changes, Nift can rebuild and revalidate the page. If the schema changes, Nift can also rebuild and revalidate the page. A stricter contract therefore cannot quietly leave old generated output behind.
data/products.json ──────────┐
├── validate ── render page
schemas/products.schema.json ┘
│
└── both participate in incremental rebuild state Schema validation is optional
The two-parameter form remains valid:
@json('data/site.json', site) Use the three-parameter form when the data has a contract worth enforcing:
@json('data/site.json', site, 'schemas/site.schema.json') This makes schemas opt-in rather than imposing ceremony on tiny files where it would add little value.
What Nift currently supports
| Area | Supported keywords |
|---|---|
| Annotations / identifiers | $schema, $id, $anchor, $comment, title, description, default, examples |
| Types / values | type, enum, const |
| Objects | properties, required, additionalProperties, minProperties, maxProperties |
| Arrays | items, minItems, maxItems, uniqueItems, contains, minContains, maxContains |
| Strings | minLength, maxLength, pattern |
| Numbers | minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf |
| Composition | allOf, anyOf, oneOf, not |
| Reuse | $defs and local $ref JSON Pointers such as #/$defs/product |
| Boolean schemas | true accepts every value; false rejects every value. |
Nift does not silently ignore a validation keyword it does not implement, because that could give a false impression that a contract is being enforced. For example, format currently produces an unsupported-keyword build error rather than pretending to validate an email address. Annotation keywords listed above are accepted but do not change validation.
Why Nift uses an existing standard instead of inventing one
The schema should not trap your content inside Nift. The same document can be validated in an editor, a pre-commit hook, CI, a backend, a TypeScript workflow or another JSON Schema-aware tool. Nift only needs to enforce the contract at the point where it consumes the data.
That keeps the architecture consistent with the rest of Nift: use a small project-aware build layer, and reuse standards and specialist tools from the wider ecosystem instead of creating Nift-specific substitutes for them.