Skip to main content

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's const: 1 alongside a redundant type: integer (const_with_type) — the type is now dropped once const is set.
  • The node items schema's required: ["$kind"] had no sibling properties entry to explain it (required_properties_in_properties) — a properties: { "$kind": { "type": "string" } } documents the shape every anyOf branch narrows further.
  • jsonschema validate failed Locals.netpc.json: its local variableGetter/variableSetter nodes omit variable.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's DefaultIgnoreCondition = WhenWritingDefault), so it must not be required: in practice MethodRef.modifiers and VariableRef.modifiers (None = 0). An enum whose zero value is Invalid (MemberVisibility) is always written, so visibility stays required; the generator's rule is "optional only when the zero value is a defined enum member not named Invalid".

Two lint findings are excluded rather than fixed, via lint's --exclude <rule>:

  • top_level_examples: an inline example would duplicate the fixtures eng/validate-schemas.sh already validates, and — since this file is generated, never hand-edited — would drift from them instead of staying in sync.
  • simple_properties_identifiers: it flags $schema and $kind. Both are the wire format's actual property names (document-format.md §1.1's $schema header, §1.4's $kind node-kind discriminator), not a naming choice the generator can change without a breaking format revision.

Decision​

  • mise.toml pins jsonschema to 17.0.0 (mise ls-remote jsonschema's latest at the time); the CI job pins mise itself (2026.9.1) via jdx/mise-action's version input.
  • eng/validate-schemas.sh runs jsonschema metaschema, jsonschema lint (with the two exclusions above), and jsonschema validate against every tracked .netpc.json (git ls-files; it fails if none is found), exiting non-zero on the first failure.
  • .github/workflows/ci.yml gets a jsonschema job: jdx/mise-action (installs the pinned CLI) then eng/validate-schemas.sh, with only contents: read (inherited from the workflow-level permissions).
  • 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-cli or another Node-based validator. No project-established Node toolchain; mise (new in this PR, via mise.toml; the .NET SDK is pinned separately by global.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.json document is validated automatically; SchemaTests validates the sample and every fixture in-process too.
  • mise.toml is now the place any other single-purpose CLI tool this repo adopts gets pinned.