0011: The sourcemeta jsonschema CLI validates schemas/netpc.v1.schema.json
Status
Accepted (2026-09-29).
Context
schemas/netpc.v1.schema.json (generated by NetPrintsJsonSchema.GenerateV1(), pinned byte-for-byte
by SchemaTests.GeneratedSchemaMatchesCommittedFile) had no check that it is itself a well-formed
JSON Schema, no lint pass for schema-authoring anti-patterns, and no validation of the repo's own
.netpc.json documents outside the two fixtures SchemaTests happens to load in-process through
JsonSchema.Net. Six documents in the repo declare "$schema": ".../netpc.v1.schema.json":
samples/HelloWorld/HelloWorld.Program.netpc.json and five fixtures under
tests/NetPrints.Core.Tests/Fixtures/ (AllNodes, EventGraphs, ForLoop, HelloWorld, Locals).
The owner chose the sourcemeta jsonschema CLI (https://github.com/sourcemeta/jsonschema), installed
through mise, as a second, independent validator: a dedicated meta-schema check
(jsonschema metaschema), an opinionated lint pass (jsonschema lint) that JsonSchema.Net has no
equivalent for, and a fast way to validate arbitrary instance files from the shell or CI without
writing a C# test for each one.
Running it surfaced real, fixable issues, addressed in the same PR (NetPrintsJsonSchema.cs):
- No top-level
description— added. schemaVersion'sconst: 1alongside a redundanttype: integer(const_with_type) — thetypeis now dropped onceconstis set.- The node
itemsschema'srequired: ["$kind"]had no siblingpropertiesentry to explain it (required_properties_in_properties) — aproperties: { "$kind": { "type": "string" } }documents the shape everyanyOfbranch narrows further. jsonschema validatefailedLocals.netpc.json: its localvariableGetter/variableSetternodes omitvariable.modifiers(None, the common case for a local), but the schema required it.FixRequired's non-Never-convention branch (the reference/value DTOs of document-format.md §1.6) required every constructor parameter with no C# default, including value-typed ones. A value-typed property whose default is a valid wire value is silently defaulted on read and silently omitted on write (NetPrintsJsonOptions'sDefaultIgnoreCondition = WhenWritingDefault), so it must not be required: in practiceMethodRef.modifiersandVariableRef.modifiers(None= 0). An enum whose zero value isInvalid(MemberVisibility) is always written, sovisibilitystays required; the generator's rule is "optional only when the zero value is a defined enum member not namedInvalid".
Two lint findings are excluded rather than fixed, via lint's --exclude <rule>:
top_level_examples: an inline example would duplicate the fixtureseng/validate-schemas.shalready validates, and — since this file is generated, never hand-edited — would drift from them instead of staying in sync.simple_properties_identifiers: it flags$schemaand$kind. Both are the wire format's actual property names (document-format.md §1.1's$schemaheader, §1.4's$kindnode-kind discriminator), not a naming choice the generator can change without a breaking format revision.
Decision
mise.tomlpinsjsonschemato17.0.0(mise ls-remote jsonschema's latest at the time); the CI job pinsmiseitself (2026.9.1) viajdx/mise-action'sversioninput.eng/validate-schemas.shrunsjsonschema metaschema,jsonschema lint(with the two exclusions above), andjsonschema validateagainst every tracked.netpc.json(git ls-files; it fails if none is found), exiting non-zero on the first failure..github/workflows/ci.ymlgets ajsonschemajob:jdx/mise-action(installs the pinned CLI) theneng/validate-schemas.sh, with onlycontents: read(inherited from the workflow-levelpermissions).- The existing
JsonSchema.Net-based C# tests (SchemaTests) are unchanged and stay the source of truth for generator-match (GeneratedSchemaMatchesCommittedFile) and structural assertions (RootDeclaresItsOwnUrlAsId,NodeDocumentHasOneAnyOfBranchPerBuiltInKindPlusExtensions, ...): they run in-process against the exact generator output before it is written, and assert C#-level properties (attribute counts,JsonSerializerOptions) the CLI cannot see. The CLI checks the committed file from the outside, in a different implementation, after generation.
Alternatives considered:
- CLI validation only, drop
SchemaTests. Loses the generator-match guarantee and the structural assertions tied to C# reflection (derived-type counts, etc.); the CLI only sees the final JSON. - A custom lint step in C#. Reimplements checks (
const_with_type,required_properties_in_properties, ...) a maintained tool already provides; more code to own for no benefit. ajv-clior another Node-based validator. No project-established Node toolchain;mise(new in this PR, viamise.toml; the .NET SDK is pinned separately byglobal.json) is a natural place to pin a single-purpose binary tool.
Consequences
- A schema change is checked from two independent angles: the C# tests it must keep matching
byte-for-byte, and the CLI's meta-schema/lint/validate pass, both locally (
eng/validate-schemas.sh) and in CI. - A new tracked
.netpc.jsondocument is validated automatically;SchemaTestsvalidates the sample and every fixture in-process too. mise.tomlis now the place any other single-purpose CLI tool this repo adopts gets pinned.