A schema is never finished. Requirements shift, a field that was nice-to-have becomes mandatory, a format gets stricter. The hard part isn’t making the change — it’s making it without turning every consuming repo’s CI red the moment you publish. This page covers the two things that make schema evolution safe: versioning the standard so consumers choose when to adopt a change, and understanding dialects so the JSON Schema flavor you write in is never the thing that breaks.
When many repos validate against one schema, an in-place edit is a deployment to all of them at once. Add a required field, and every document that lacks it fails on the next run — across repos you don’t own and can’t fix. A version turns that all-at-once edit into a choice each consumer makes on their own schedule: they keep pinning the old version until they’re ready, then move to the new one deliberately.
So the first move in evolving a schema safely is making sure your schema has a version that consumers can pin to.
How you express the version depends on which of the three reference kinds your consumers use to name the schema. (For how docmeta classifies a reference as built-in, file, or URL, see the three reference kinds.)
A built-in id already carries its version in the third segment of vendor:name:version. The bundled OKF schema is google:okf:0.1 — the 0.1is the pin. A future revision ships as a new id (google:okf:0.2), and a consumer naming google:okf:0.1 keeps validating against exactly the schema they chose until they edit that reference themselves.
docmeta.config.yaml
schemas:
- google:okf:0.1# pinned; a new built-in version is a new id
Built-in versions are decided by docmeta, not by you — you adopt them, you don’t author them. If you’re defining your own standard, you’ll version a file or URL schema instead.
A schema you keep in the repo has no built-in version field, so encode the version in the path or filename. Both of these read as a pin at a glance:
docmeta.config.yaml
schemas:
# version in the filename
- ./schemas/metadata-1.2.schema.json
# ...or version in a directory
- ./schemas/v1/metadata.schema.json
When you ship a breaking change, add metadata-1.3.schema.json (or v2/metadata.schema.json) alongside the old file rather than overwriting it. Consumers move by editing the reference, and you can keep the previous version in the repo for as long as anyone still pins it.
A URL schema — the foundation for governing one schema across many repos — versions the same way, in the path. Publish each version at a distinct, stable URL:
Consumers pin by referencing a versioned URL, never a “latest” one. Keep old versions reachable so pinned repos don’t break, and announce a new version rather than mutating an existing URL in place. The fetch timeout and per-run caching that make URL schemas practical at scale are covered in Govern a shared schema by URL.
Once your schema is versioned, the next question is which changes you can make freely and which need a careful rollout. The line runs between changes that can only accept more documents and changes that can reject documents that used to pass.
Safe (additive)
Changes that never fail a document that passed before:
Adding a new optional field to properties (validated when present, not demanded).
Loosening a constraint — removing a field from required, widening an enum, dropping a format.
Relaxing additionalProperties from false to true.
Editing description/title text in the schema.
These you can usually ship as a routine update without a staged rollout.
Breaking (tightening)
Changes that can fail a document that passed before:
Adding a field to required.
Adding a new constraint — a format, a pattern, a narrower enum, a minLength.
Tightening additionalProperties from true to false.
Changing a field’s type.
These need the staged ratchet below, and a new version so consumers opt in.
The most common breaking change is also the most useful one: promoting a field from recommended to required. That’s worth its own playbook.
Demanding a field that documents don’t yet carry fails every one of them at once. The safe path is to add the field, populate it across the repo while it’s still optional, and only then make it required — so the day you flip the switch, everything already complies.
Add the field as optional. Describe it in properties but leave it out of required. It’s validated when present and ignored when absent, so nothing fails yet.
Backfill the field across existing documents. With the field optional, docmeta won’t block the repo while you work through it. Land the values incrementally.
Promote the field to required. Once coverage is complete, move it into required and ship that as a new schema version. Consumers adopt the stricter version when they pin to it.
This add-then-ratchet pattern is the heart of tightening a standard without a flag day. The full walkthrough — including staging the rollout across a large repo and using config to apply the stricter schema gradually — lives in Roll out a new required field without breaking the build.
The second thing that can break a schema isn’t a field you changed — it’s the JSON Schema dialect you wrote it in. JSON Schema has evolved through several versions (draft-04, draft-06, draft-07, 2019-09, 2020-12), and they aren’t fully compatible: a schema written for one dialect can fail to compile under another. docmeta handles this for you. It reads each schema’s own $schema line — the URI at the top that names the dialect’s meta-schema — and compiles that schema with the matching validation engine.
docmeta supports these dialects:
Dialect
Detected from a $schema URI containing
2020-12
2020-12 (also the fallback)
2019-09
2019-09
draft-07
draft-07 or draft/7
draft-06
draft-06 or draft/6 (shares the draft-07 engine)
draft-04
draft-04 or draft/4
If a schema omits its $schema line, or uses one docmeta doesn’t recognize, validation falls back to 2020-12 — the dialect of the built-in schemas.
Relying on the fallback works, but stating the dialect removes ambiguity for docmeta and for anyone reading the schema. Keep a $schema line at the top of every schema file, pointing at the meta-schema for the dialect you wrote in:
Both schemas validate the same documents here; the only difference is which engine docmeta compiles them with. Declaring the dialect is what lets you write to a specific version’s features without guessing how docmeta will read the file.
When a file is validated against more than one schema — a list-valued $schema, several config entries, or layered --schema flags — the schemas don’t all have to share a dialect. docmeta detects the dialect of each schema independently and compiles each one in its own engine. A 2020-12 house-style schema and a draft-07 schema you fetched from a partner can validate the same file in the same run, each held to its own dialect’s rules.
This is why adopting a schema from elsewhere — an older internal standard, a third-party schema that still targets draft-07 — never forces you to rewrite it to match your other schemas. Drop it into the set as-is; its $schema line tells docmeta how to read it.