Skip to content

feat(validation)!: ship precompiled JSON Schema validators - #78

Merged
frantuma merged 2 commits into
mainfrom
frantuma/precompiled-json-schema-validators
Oct 20, 2026
Merged

frantuma merged 2 commits into
mainfrom
frantuma/precompiled-json-schema-validators

Conversation

@frantuma

@frantuma frantuma commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

This is the fifth step of the plan to remove Ajv and the components flagged by the dependency policy scan from the published dependency tree, and the one that removes Ajv itself. The package no longer installs ajv, ajv-errors, ajv-formats or their dependencies: require-from-string@2.0.2, which the scan reports as a critical end-of-life component, and fast-deep-equal, fast-uri and json-schema-traverse. It is a breaking change and releases as 3.0.0.

The change

The JSON Schema validation providers validate with functions that Ajv's standalone code generator writes ahead of time, instead of compiling the embedded schemas with Ajv at runtime. scripts/generate-json-schema-validators.mjs (npm run generate:validators) gives each of the 15 embedded schemas its own Ajv instance with the options ajv-utils.ts used, including the instance without the built-in draft-07 meta-schema that the AsyncAPI 2.3 to 2.6 schemas need since #73, and writes src/services/validation/json-schema/validators/<name>.ts. The output is committed and deterministic; the files are @ts-nocheck and ignored by ESLint, and their default export is typed as JsonSchemaValidateFunction. A new check-generated-validators CI job runs the generator and fails when a committed file differs or is missing, so a schema change or an ajv, ajv-errors or ajv-formats bump has to regenerate them in the same PR; CLAUDE.md records this.

The generated code needs two of Ajv's runtime helpers, equal (a re-export of fast-deep-equal) and ucs2length. They are copied into json-schema/runtime/ with their licence headers, the generator rewrites Ajv's require calls into imports of the copies, and it fails if the code references anything else of Ajv. ajv, ajv-errors, ajv-formats and fast-deep-equal become exact-pinned devDependencies, which also keeps the generation reproducible. NOTICE credits Ajv and fast-deep-equal for the copied helpers, the browser bundle's THIRD-PARTY-NOTICES.txt keeps listing both through additionalModules, and the rule in scripts/retired-identifiers-allow.txt follows the ApiDOM attribution to its new NOTICE lines. ajv-utils.ts is deleted and optionalDependencies is gone.

Babel compiles the generated directory with compact: true. Its default output reprints every inlined schema constant expanded and indented, which made the published .mjs and .cjs 40 % larger than the generated sources. The committed .ts files stay readable, because the generator keeps Ajv's line breaks.

Breaking change

The providers no longer accept a schema of their own. JsonSchemaValidationProvider takes a validation function instead of a schema and no longer exposes its ajv, ajv2020 and jsonSchema fields; the function is typed with the structural JsonSchemaValidateFunction, which Ajv's compiled functions satisfy, and which is exported from the package entry point together with AjvErrorObject. The OpenAPI 2.0, 3.0 and 3.1, Arazzo 1.0 and 1.1 and Overlay providers take no constructor arguments, the deprecated Overlay1JsonSchemaValidationProvider included, which picks among the three precompiled Overlay validators by document version. Nothing reads the schema any more since the formatter port in #77 dropped its unused schema argument, so the providers no longer import the schema JSON, which Babel used to inline into every provider module.

A "Migrating to 3.0" section in the README shows the replacement: compile your schema with your own Ajv and pass the function to a subclass of JsonSchemaValidationProvider, or, when a modified schema only added organisation-specific checks, write a linting rule instead. The modified-schema case in test/validate.ts is rewritten as that subclass with its expected diagnostics unchanged, and the Overlay 1 custom-schema test in overlay-yaml.ts goes with the feature. The first commit carries a BREAKING CHANGE: footer, so lerna releases 3.0.0.

The example's Ajv options are the ones the providers compiled a schema of your own with before 3.0. The second commit, after review, adds the one the first left out, unicodeRegExp: false: without it Ajv compiles patterns in Unicode mode, so a pattern with an identity escape such as \_ no longer compiles, and . matches a whole emoji where it matched half of one, which let documents the 2.x providers reported pass. The README now says why the option matters, and a new case in test/validate.ts validates a document against a schema of one's own holding both kinds of pattern, in both formatting modes, and expects the diagnostics the removed 2.x compiler gives for it; it fails in Unicode mode on either pattern. Compiled with the README's options, every embedded 2020-12 schema reaches the same verdicts and errors as its generated validator on all 596 fixture documents, apart from schemaPath values that no diagnostic shows.

Diagnostics

Diagnostics are unchanged. All recorded baselines pass as before, and so does the formatter parity test from #77. A new test, test/json-schema-validators.ts, runs every fixture document through each generated validator and through the same schema compiled by Ajv at runtime, and expects the same verdict and the same errors. It checks each document against every schema, not only its own version's, and requires valid and invalid documents for every schema; two valid fixtures were added for AsyncAPI 2.3 and Arazzo 1.1, which no fixture covered. Before ajv-utils.ts was deleted, a one-off comparison of the old runtime path with the generated validators over 594 documents and the 15 schemas (8,910 comparisons, 348 valid and 8,562 invalid) found no difference in verdict or errors.

Validation no longer compiles schemas or evaluates generated code at runtime. Loading Ajv and compiling one embedded schema with it, which the first validation of each version used to cost, takes 91 to 146 ms in a fresh process (OpenAPI 2.0, 3.0 and 3.1, AsyncAPI 2.0 and 2.6, Arazzo 1.1 and Overlay 1.2, median of seven runs); loading the precompiled validator takes 2 to 6 ms. The browser bundle still contains one new Function("return this"), a global-object fallback in a dependency, and one eval(, so this PR makes no claim about strict content security policies.

Sizes

The generated code ships in both module formats, so the package grows while the install tree shrinks. Measured against the #77 code:

Before After
Packed tarball 1.95 MB 2.38 MB
Installed package 14.6 MB 19.4 MB
A consumer's whole install 132.0 MB, 91 packages 135.4 MB, 84 packages
Browser bundle 5.99 MB 6.45 MB

Testing

npm test passes with 570 tests. Lint (no new warnings), type-check, npm run build (declarations, ESM, CommonJS and the browser bundle), the generator drift check, ./scripts/check-retired-identifiers.sh and ./scripts/check-package-contract.sh pass. In a scratch project that installs the packed package, nothing from the Ajv family remains (ajv, ajv-errors, ajv-formats, require-from-string, fast-deep-equal, fast-uri, json-schema-traverse), nor any package the earlier steps removed. The README's migration example, copied into a project of its own with ajv installed, type-checks and reports its schema's errors through the language service, the two patterns above included.

This PR touches the same devDependencies block and lockfile as #76, so whichever of the two lands second needs a small rebase.

The JSON Schema validation providers validate with functions that Ajv's
standalone code generator writes at build time, instead of compiling
the embedded schemas with Ajv at runtime. The package no longer
installs ajv, ajv-errors, ajv-formats or their dependencies, among them
require-from-string, which a dependency policy scan reports as end of
life. Validation compiles no schema and evaluates no generated code at
runtime.

scripts/generate-json-schema-validators.mjs, run with npm run
generate:validators, compiles the 15 embedded schemas with the options
the runtime used and writes json-schema/validators, which is committed.
A new CI job regenerates it and fails on any difference. The generated
code imports Ajv's equal and ucs2length helpers from copies in
json-schema/runtime. ajv, ajv-errors, ajv-formats and fast-deep-equal
become exact-pinned devDependencies, and NOTICE and the browser
bundle's notices credit Ajv and fast-deep-equal; the retired-identifier
allowlist follows the ApiDOM attribution to its new NOTICE lines.
Babel compiles the generated code compactly, which keeps the published
modules near the size of the generated sources.

Diagnostics are unchanged. Every recorded baseline passes as before,
and a new test runs every fixture through each generated validator and
through Ajv at runtime, expecting the same verdict and the same errors.

BREAKING CHANGE: the JSON Schema validation providers no longer accept
a schema of their own. The OpenAPI 2.0, 3.0 and 3.1, Arazzo 1.0 and 1.1
and Overlay providers take no constructor arguments, and
JsonSchemaValidationProvider takes a validation function instead of a
schema and no longer exposes its ajv, ajv2020 and jsonSchema fields. To
validate against a schema of your own, compile it with your own Ajv and
pass the function to a subclass of JsonSchemaValidationProvider, as the
README's "Migrating to 3.0" section shows.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The breaking provider API and extensive generated-code replacement warrant human confirmation of diagnostic parity and published-package behavior.

0 open findings

What changed in this PR

Replaces runtime Ajv compilation with precompiled JSON Schema validators, removing Ajv from the published dependency tree as a breaking 3.0 change.

Changes:

  • Switches providers to generated validation functions and documents custom-schema migration.
  • Adds deterministic generation, CI drift checks, and validator parity tests.
  • Updates dependency classification, bundled helpers, and attribution.
File Description
scripts/​retired-identifiers-allow.txt Updates NOTICE attribution locations.
packages/​api-languageservice/​test/​validate.ts Migrates custom-schema provider testing.
packages/​api-languageservice/​test/​test-utils.ts Shares fixture parsing.
packages/​api-languageservice/​test/​overlay-yaml.ts Removes obsolete custom-schema test.
packages/​api-languageservice/​test/​json-schema-validators.ts Checks generated/runtime validator parity.
packages/​api-languageservice/​test/​fixtures/​json-schema-validators/​asyncapi-2-3-valid.yaml Adds valid AsyncAPI 2.3 coverage.
packages/​api-languageservice/​test/​fixtures/​json-schema-validators/​arazzo-1-1-valid.yaml Adds valid Arazzo 1.1 coverage.
packages/​api-languageservice/​test/​better-errors.ts Uses generated validators for formatter tests.
packages/​api-languageservice/​src/​services/​validation/​providers/​overlay-12-json-schema-validation-provider.ts Uses precompiled Overlay 1.2 validation.
packages/​api-languageservice/​src/​services/​validation/​providers/​overlay-11-json-schema-validation-provider.ts Uses precompiled Overlay 1.1 validation.
packages/​api-languageservice/​src/​services/​validation/​providers/​overlay-10-json-schema-validation-provider.ts Uses precompiled Overlay 1.0 validation.
packages/​api-languageservice/​src/​services/​validation/​providers/​overlay-1-json-schema-validation-provider.ts Selects version-specific precompiled delegates.
packages/​api-languageservice/​src/​services/​validation/​providers/​openapi-31-json-schema-validation-provider.ts Uses precompiled OpenAPI 3.1 validation.
packages/​api-languageservice/​src/​services/​validation/​providers/​openapi-30-json-schema-validation-provider.ts Uses precompiled OpenAPI 3.0 validation.
packages/​api-languageservice/​src/​services/​validation/​providers/​openapi-20-json-schema-validation-provider.ts Uses precompiled OpenAPI 2.0 validation.
packages/​api-languageservice/​src/​services/​validation/​providers/​json-schema-validation-provider.ts Accepts validation functions instead of schemas.
packages/​api-languageservice/​src/​services/​validation/​providers/​better-errors/​index.ts Marks the error interface public.
packages/​api-languageservice/​src/​services/​validation/​providers/​asyncapi-26-json-schema-validation-provider.ts Uses precompiled AsyncAPI 2.6 validation.
packages/​api-languageservice/​src/​services/​validation/​providers/​asyncapi-25-json-schema-validation-provider.ts Uses precompiled AsyncAPI 2.5 validation.
packages/​api-languageservice/​src/​services/​validation/​providers/​asyncapi-24-json-schema-validation-provider.ts Uses precompiled AsyncAPI 2.4 validation.
packages/​api-languageservice/​src/​services/​validation/​providers/​asyncapi-23-json-schema-validation-provider.ts Uses precompiled AsyncAPI 2.3 validation.
packages/​api-languageservice/​src/​services/​validation/​providers/​asyncapi-22-json-schema-validation-provider.ts Uses precompiled AsyncAPI 2.2 validation.
packages/​api-languageservice/​src/​services/​validation/​providers/​asyncapi-21-json-schema-validation-provider.ts Uses precompiled AsyncAPI 2.1 validation.
packages/​api-languageservice/​src/​services/​validation/​providers/​asyncapi-20-json-schema-validation-provider.ts Uses precompiled AsyncAPI 2.0 validation.
packages/​api-languageservice/​src/​services/​validation/​providers/​arazzo-11-json-schema-validation-provider.ts Uses precompiled Arazzo 1.1 validation.
packages/​api-languageservice/​src/​services/​validation/​providers/​arazzo-1-json-schema-validation-provider.ts Uses precompiled Arazzo 1.0 validation.
packages/​api-languageservice/​src/​services/​validation/​providers/​ajv-utils.ts Removes runtime Ajv compilation.
packages/​api-languageservice/​src/​services/​validation/​json-schema/​validators/​overlay-1-2.ts Adds generated Overlay 1.2 validator.
packages/​api-languageservice/​src/​services/​validation/​json-schema/​validators/​overlay-1-1.ts Adds generated Overlay 1.1 validator.
packages/​api-languageservice/​src/​services/​validation/​json-schema/​validators/​overlay-1-0.ts Adds generated Overlay 1.0 validator.
packages/​api-languageservice/​src/​services/​validation/​json-schema/​runtime/​ucs2length.ts Includes attributed string-length helper.
packages/​api-languageservice/​src/​services/​validation/​json-schema/​runtime/​equal.ts Includes attributed deep-equality helper.
packages/​api-languageservice/​src/​index.ts Exports validation function and error types.
packages/​api-languageservice/​scripts/​generate-json-schema-validators.mjs Generates standalone validators with local helpers.
packages/​api-languageservice/​scripts/​generate-json-schema-validators.d.mts Types generator exports for tests.
packages/​api-languageservice/​README.md Documents migration to 3.0.
packages/​api-languageservice/​package.json Adds generation command and development-only dependencies.
packages/​api-languageservice/​config/​webpack/​browser.config.js Preserves bundled-code attribution.
packages/​api-languageservice/​.babelrc Compacts generated validator output.
package-lock.json Records development-only dependency classification.
NOTICE Credits copied helpers and generated code.
eslint.config.js Excludes generated validators and removes obsolete exception.
CLAUDE.md Documents regeneration and helper maintenance.
.github/​workflows/​build.yml Adds generated-validator drift check.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

The removed runtime compiler set `unicodeRegExp: false` on every Ajv
instance, so a schema of one's own was compiled without Unicode mode,
as the generated validators still are. The README's migration example
left the option out. With its options a pattern holding an identity
escape such as `\_` no longer compiles, and `.` matches a whole emoji
where it matched half of one, so documents the 2.x providers reported
pass.

The example and its copy in `test/validate.ts` set the option now, and
the README says why it matters. A new case validates a document
against a schema of one's own with both kinds of pattern, in both
formatting modes, and expects the diagnostics the 2.x compiler gives;
it fails in Unicode mode on either pattern. Compiled with the README's
options, every embedded 2020-12 schema reaches the same verdicts and
errors on all fixtures as its generated validator, apart from
`schemaPath` values that no diagnostic shows.
@frantuma
frantuma merged commit 33f19e4 into main Oct 20, 2026
10 of 11 checks passed
@frantuma
frantuma deleted the frantuma/precompiled-json-schema-validators branch October 20, 2026 07:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants