Home Documentation Templates Examples Showcase GitHub ↗
Theme

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.

JSON Schema is not a Nift-specific schema language.

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 typeAccepts
objectJSON objects such as {"name":"Nift"}
arrayJSON arrays such as [1,2,3]
stringJSON strings
numberAny finite JSON number
integerNumbers with no fractional part
booleantrue or false
nullnull

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

AreaSupported keywords
Annotations / identifiers$schema, $id, $anchor, $comment, title, description, default, examples
Types / valuestype, enum, const
Objectsproperties, required, additionalProperties, minProperties, maxProperties
Arraysitems, minItems, maxItems, uniqueItems, contains, minContains, maxContains
StringsminLength, maxLength, pattern
Numbersminimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf
CompositionallOf, anyOf, oneOf, not
Reuse$defs and local $ref JSON Pointers such as #/$defs/product
Boolean schemastrue accepts every value; false rejects every value.
Unsupported validation keywords fail explicitly.

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.