RFC-040: The Schema Documents Itself

ProposedImplemented
Depends on

Context

schema.json is the contract. RFC-013 makes it immutable and addressable by tag, and the conformance suite proves the Rust law-model conforms to it. It is the one artifact a third-party engine author has to read.

Until now it was also the one artifact nobody could read comfortably. The reference page at /reference/schema stated the current version, explained the tag URL scheme, listed which version introduced which construct, and then handed the reader a link to 1746 lines of JSON on GitHub. Someone who wanted to know what open_terms accepts, or which operands FOREACH takes, opened that file and scrolled.

Meanwhile the schema already held most of the answer. Of its 228 property schemas, 173 carried a description, and a good number of those were not labels but normative prose: valid_to sets out the whole vervalbepaling rule, legal_character documents each enum member, type_spec.unit warns that a unit is a label and never a computation. Nine RFC numbers appear in those strings. All of it sat in a file that only validators opened.

The gap had a cost beyond inconvenience. Documentation written alongside the schema drifts from it. concepts/law-format.md showed a $schema URL pinned at v0.5.2 while the released version was v0.5.9. The editor’s legal_character dropdown was missing RECHTSPOSITIE for the whole of the v0.5.9 release, so a law author could not set a value the schema had accepted since that version shipped. Both are copies of schema knowledge that nothing held to the schema.

Decision

The schema is the source of its own reference documentation, and the documentation site renders it.

Three parts.

1. Annotations are part of the contract

A property’s description is not a courtesy to future maintainers. It is how the schema explains itself, and it is expected to be present. Where an example clarifies more than a sentence, the property carries examples, taken from the corpus where a real law uses the construct.

Both are held to the code. A description that says what the engine does has to be true of the engine, and an example has to validate against the subschema it sits on. The second is now checked: script/check-schema-examples.py runs in CI and as a pre-commit hook, because two invalid examples shipped before it existed and the reference page renders them as copyable YAML.

Both are standard draft-07 annotation keywords. Neither affects validation, so filling them in never changes which documents are valid.

Schema v0.6.0 applies this to the existing schema. It adds 48 descriptions, taking property coverage from 76% to 97%, and 29 examples. The remaining gaps are if/then internals, which are machinery rather than fields a law author writes.

It also rewrites 39 descriptions that were already there, and ends every description with a period. A reader upgrading from v0.5.9 gets changed prose, not only added prose. Several of those rewrites are corrections: the schema said produces was annotation that does not change the computed value, where legal_character in fact selects which hooks fire and which Awb procedure applies.

A version that adds only annotations is still a version. Released schema directories are append-only, so there is no way to improve v0.5.9 in place, and a reader who pins a tag is entitled to know exactly what that tag contains.

2. The reference page is generated

/reference/schema renders the released schema at build time: the top-level fields, the conditional identifier rules, the machine_readable block and its nested structures, the field kinds, all fourteen operations, and the closed vocabularies. Each field is linked to the concept page that explains it, each RFC-NNN in a description becomes a link, and each vocabulary links to the glossary section that defines its terms.

What stays hand-written is the version history. It records what a version introduced and why, with the RFC that decided it. No schema can state that about itself.

Generation is not transcription. Six allOf/if/then blocks are the schema’s way of saying that a WET needs a bwb_id and an EU_VERORDENING needs a celex_nummer; the page states that, as a table, rather than reproducing the conditional structure. A generic JSON Schema renderer prints the machinery and leaves the reader to reconstruct the meaning.

3. A copy that CI does not check is not allowed

The docs image is built with only docs/ in its context, so the build cannot read schema/. The page therefore renders a committed snapshot, docs/src/data/schema-latest.json, written by npm run sync:schema.

A committed copy of a versioned artifact is exactly the kind of thing that goes stale, so check-schema-version.mjs asserts it matches schema/latest byte for byte and fails the build otherwise. That guard already held three hand-written statements on the page to the released version; the snapshot is the fourth.

The same rule applies to copies outside the docs. Where a component hard-codes a schema enum, as the editor does for legal_character, that copy needs a check or it will drift. The editor’s list is now correct; it is not yet guarded.

Why

The alternative is documentation that disagrees with the contract. Every hand-written field table is a promise to update two files whenever one changes, and the repository already shows what happens: a stale $schema URL, a dropdown missing a value the schema accepted. Generating the reference removes the promise rather than asking people to keep it.

Annotations pay twice. The same description that renders on the page is what an editor shows when someone writes a law against the $schema URL. Writing it once serves both.

One limit is worth stating. In draft-07 a $ref formally replaces its siblings, so a description written next to a $ref is ignored by a strict reader. Forty-five nodes in the schema are in that position. The reference page falls back to the target’s description, so nothing is lost there, but an editor that follows the specification will not show those. Moving to 2020-12 would settle it, along with making deprecated available. The schema uses none of the constructs that changed between drafts, so the migration is mechanical; whether to make it is a separate decision, because draft-07 is the most widely implemented dialect and the schema is a contract other engines read.

Why not a generic documentation generator. Tools like json-schema-for-humans exist and would have produced a page in an afternoon. What they produce reads as generated: expandable oneOf branches, $ref URIs, allOf nesting shown as structure rather than meaning. They also cannot know that source is the subject of a concept page or that regulatory_layer values are glossary terms, which is most of what makes the page useful.

Why not put the prose in the docs instead of the schema. That is the arrangement this RFC replaces. It puts the explanation where a validator, an editor and a third-party engine author cannot reach it.

References

  • RFC-001 YAML Schema Design Decisions
  • RFC-013 Execution Provenance, for the immutable tag URLs the page cites
  • Schema Reference, the page this RFC describes
  • Law Format, the guided walkthrough the reference links back to

RegelRecht

An exploration by Bureau Architectuur of the Dutch Ministry of Economic Affairs and Climate Policy into the possibilities of transparent, executable legislation.

Links

GitHub repository
How it works
Stay informed
Roadmap (Dutch)
Documentation
Research

Contact

regelrecht@minbzk.nl

Part of

Bureau Architectuur
Ministry of Economic Affairs and Climate Policy