Skip to content

Author a schema for your metadata standard

Your metadata standard already exists, even if it only lives in a style guide or in people’s heads. Every page needs a type. A timestamp should be an ISO date. API pages must declare a version. A JSON Schema turns those agreements into a file that manni meta can enforce on every commit. This page walks through writing that schema, from a minimal one-field example up to the fuller built-in OKF schema. It also shows how a document points itself at the schema you wrote.

manni meta validates against standard JSON Schema. If you can write JSON, you can write a manni meta schema; there is no manni-specific schema format to learn. A schema describes the frontmatter object: which keys are required, what type each value should be, and whether unexpected keys are allowed.

Here is the smallest schema worth shipping. It says: the metadata must be an object, it must have a title, and that title must be a non-empty string.

title.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["title"],
"properties": {
"title": { "type": "string", "minLength": 1 }
}
}

This is the extra.schema.json fixture that manni meta’s own test suite runs, so you can trust it compiles and validates exactly as shown. Four things are doing the work:

  • "$schema" at the top names the JSON Schema dialect this file is written in, which here is draft 2020-12. The line tells manni meta which validation engine to use. This is the schema’s own dialect declaration. It differs from the $schema key you put in a document to choose a schema, covered below.
  • "type": "object" says the metadata is a set of key/value pairs, which frontmatter always is.
  • "required": ["title"] is the list of fields that must be present. A document missing title fails validation.
  • "properties" describes individual fields. title must be a string, and minLength: 1 rejects an empty title: that a contributor left blank.

A document with this frontmatter passes:

---
title: Getting started
---

A document with empty or missing title fails. That is the whole loop: declare the shape, and manni meta holds every file to it.

The single most important decision in a metadata schema is which fields are required and which are merely recommended. JSON Schema models this with one list:

  • Required. List the field in "required". Its absence is a hard failure.
  • Recommended (optional). Describe the field in "properties" but leave it out of "required". If the field is present, manni meta checks its type and format; if it’s absent, that’s fine.

This distinction is what lets you encode a standard that has a strict core and a softer edge. In the example below, type is mandatory, while title and tags are validated when present but never demanded:

metadata.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["type"],
"properties": {
"type": { "type": "string", "minLength": 1 },
"title": { "type": "string" },
"tags": {
"type": "array",
"items": { "type": "string" }
}
}
}

Requiring a field guarantees it exists; constraining it guarantees it’s usable. JSON Schema gives you the building blocks, and manni meta supports the standard format checks through ajv-formats. Two formats come up constantly in document metadata:

Use "format": "date-time" to require an ISO 8601 timestamp. This catches the classic fat-fingered date: 2026-13-01, June 5th, or a bare 2026-06-25 where a full timestamp was expected.

"timestamp": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 datetime of last meaningful change."
}

Passes: 2026-06-25T14:30:00Z Fails: June 5th, 2026-13-01

Beyond formats, the everyday constraints carry most of the weight:

  • enum: restrict a field to a fixed set of values, ideal for a controlled type or status: "enum": ["guide", "reference", "tutorial"].
  • minLength: reject empty strings ("minLength": 1).
  • array + items: validate every element of a list, as with the tags example above.

By default, JSON Schema lets a document carry fields your schema never mentions. Whether to permit that is a deliberate choice, controlled by additionalProperties:

  • "additionalProperties": true (or omitting it): extra keys are tolerated. Contributors can add experimental or tool-specific frontmatter without tripping the check. This is the lenient default and a good fit for a young or shared standard.
  • "additionalProperties": false: every key must be declared in "properties". An unexpected key is a failure. Use this to catch typos like titel: or to keep frontmatter tightly governed.

The schema anchoring manni meta’s default set is Open Knowledge Format (OKF) v0.1, addressed as google:okf:0.1. Run manni meta schemas for the full built-in list. OKF is a useful reference because it combines every technique on this page in a realistic standard. Reading it is the fastest way to see how the pieces fit together. When no other schema applies, manni meta falls back to a set rather than to OKF alone. That set is google:okf:0.1 together with passo-uno:seven-action:1.0, which constrains an optional action field and requires nothing of its own.

src/schemas/okf/0.1.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "google:okf:0.1",
"title": "Open Knowledge Format (OKF) v0.1",
"description": "Frontmatter schema for OKF v0.1 concept files. OKF requires only `type`; all other fields are recommended, and unknown keys are explicitly tolerated.",
"type": "object",
"required": ["type"],
"additionalProperties": true,
"properties": {
"type": {
"type": "string",
"minLength": 1,
"description": "A short string identifying the kind of concept."
},
"title": {
"type": "string",
"description": "Human-readable display name."
},
"description": {
"type": "string",
"description": "Single-sentence summary used for indexing and previews."
},
"resource": {
"type": "string",
"format": "uri",
"description": "A URI uniquely identifying the underlying asset."
},
"tags": {
"type": "array",
"items": { "type": "string" },
"description": "Short strings for categorization."
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 datetime of last meaningful change."
}
}
}

Notice the design choices OKF makes, each one a decision you’ll face in your own schema:

  • Exactly one required field (type). OKF demands the bare minimum, so adopting it rarely breaks existing files.
  • additionalProperties: true, stated explicitly. Unknown keys are welcome by design, because OKF is meant to be extended.
  • Recommended fields with real constraints. title, description, and tags are optional, but if present they’re checked. resource and timestamp carry uri and date-time formats.
  • A $id and human-readable title/description. Validation does not need these, but they document the schema for the humans who read it. $id is what gives the built-in its google:okf:0.1 address. Setting a $id in a schema file of your own does not register a new built-in id, because built-ins are bundled into manni meta. You reference your own schema by its file path or URL, never by its $id. Give it an id of your own, or leave it out; see the warning below before reusing one.

Everything above assumes you’re defining the field set yourself. manni meta ships twenty-three schemas you can adopt without authoring anything. The built-in schema registry lists all of them in one table, with what each constrains and which two are on by default.

If your site is built with Docusaurus, start with docusaurus:docs:3.10 and its blog and pages counterparts. They encode the front matter contract of Docusaurus 3.10 and require nothing. Switching one on cannot fail a page that was already building. It only catches a sidebar_position: "2" that your site would have rejected later. They claim none of the type or action keys below, so they stack with any of the vocabularies. They do share tags with OKF, which allows only strings where Docusaurus also allows tag objects. See the overlap note before pairing those two.

If what you need is a closed list of document types, three of the built-ins are vocabularies:

  • diataxis:diataxis:1.0 requires type and constrains it to the four Diátaxis forms.
  • tgdp:templates:1.0 requires type and constrains it to the 25 template slugs of The Good Docs Project.
  • passo-uno:seven-action:1.0 constrains action to the seven reader actions of the Seven-Action model.

The first two both claim type, so they are alternatives; either one pairs with the third.

The two type vocabularies each demand the key as well as constraining it, so neither needs a partner for presence-checking. Stack one with OKF for the other OKF fields. Both schemas apply in full, in either order:

Terminal window
manni meta validate docs/ -s diataxis:diataxis:1.0 -s google:okf:0.1

Seven-Action is the exception: it rules that an action, if present, is legitimate, without insisting on one. That is what makes it safe to layer onto what you already have. Every value of all three, which combinations compose, and the crosswalk from Seven-Action to Diátaxis are in the taxonomy schemas reference.

A schema only matters once documents are validated against it. The most direct way to connect the two is to let a document name its own schema with a $schema key in its frontmatter. manni meta reads that key and validates the file against whatever it points to.

Here is a document naming the built-in OKF schema, the schema-ref.md fixture:

schema-ref.md
---
$schema: google:okf:0.1
type: guide
title: Self-Describing Document
---
# Self-Describing Document
This file names its own schema via `$schema`.

The $schema value can be a built-in id (as here), a path to a .json file you wrote, or a URL. To point at a local schema file instead:

---
$schema: ./schemas/metadata.schema.json
type: guide
title: My page
---
  1. Write your schema as a .json file using the patterns above, keeping the "$schema" dialect line at the top.

  2. Add $schema: to a document’s frontmatter, pointing at your file (or a built-in id, or a URL).

  3. Validate it:

    Terminal window
    npx @hawkeyexl/manni meta validate path/to/document.md

    manni meta resolves the document’s $schema, validates the frontmatter, and reports any missing or malformed fields.

Some fields your standard requires are not for publishing. An owner, an internal ticket, a service name. Mark the property x-manni-encrypt, and the page has to hold that value encrypted:

schemas/page.json
"owner": { "type": "string", "enum": ["platform", "billing"], "x-manni-encrypt": true }

The field stays required and your enum still holds, because validation checks the decrypted value against the property’s whole schema. Require a field and keep its value private follows one such value through every command that touches it. Put the key where every tool finds it settles where the key lives first.

Some fields are for whoever fetches the page, and some only serve the people and the CI that maintain it. Mark a property x-manni-location to say which:

schemas/page.json
"owner": { "type": "string", "x-manni-location": "external" }

page keeps a value in the document, and external keeps it in the collection’s external-metadata manifest. validate warns about a value on the wrong side, and manni meta relocate moves it. Keep maintainer metadata out of delivered pages follows the mark through every command.