Visitar URL original
perf: skip the Ajv schema lookup on every anyOf/oneOf/if validation by Tony133 · Pull Request #926 · fastify/fast-json-stringify · GitHub
Skip to content

perf: skip the Ajv schema lookup on every anyOf/oneOf/if validation - #926

Merged
Tony133 merged 2 commits into
mainfrom
perf/cache-schema-validators
Nov 11, 2026
Merged

Tony133 merged 2 commits into
mainfrom
perf/cache-schema-validators

Conversation

@Tony133

@Tony133 Tony133 commented Nov 10, 2026

Copy link
Copy Markdown
Member

Proposal:

  • Fetch the Ajv validation function of each anyOf / oneOf / if schema once per serializer, instead of looking it up on every serialization.

Problem:

For anyOf, oneOf and if/then/else the generated code calls validator.validate(schemaRef, value) to pick the branch. That goes through ajv.validate(ref, data). Ajv compiles each schema once and keeps the validation function, but it looks that function up again on every call: it normalizes the ref with a regular expression, reads it from its schemas / refs registries, and after the validation copies the errors to ajv.errors. On small values that lookup costs more than the validation itself.

This is the path taken by every nullable property written as anyOf: [{ type: 'string' }, { type: 'null' }], which is what TypeBox emits for Type.Union([Type.String(), Type.Null()]).

Change:

  • index.js: each schema ref used by the generated code gets one variable in the serializer scope. The call site becomes (validate_0 || (validate_0 = validator.getSchemaValidator(ref)))(value), so the function is fetched on first use and then called directly. A ref validated in several places shares one variable.
  • lib/validator.js: new getSchemaValidator(schemaRef), which returns the function from ajv.getSchema() and throws the same no schema with key or ref error as ajv.validate() when the ref is unknown. validate() is unchanged.
  • lib/standalone.js: the inlined validator object exposes the same method.

Schemas are still compiled lazily, on first use; compileValidators and the standalone modes work as before. The output does not change.

Tests:

test/compile-validators.test.js: nothing is looked up at build time, each schema is looked up once after eleven serializations, a shared schema uses one variable, and getSchemaValidator throws on an unknown ref. The existing assertions on the generated code are updated to the new call.

Benchmark:

Apple Silicon, default mechanism on both sides, median ops/sec over 15 alternating rounds, output consumed with Buffer.byteLength.

Node.js 26.12.0 (V8 14.6):

Case fast-json-stringify v7.3.5 (default mechanism) This PR (default mechanism) Difference
anyOf [string, null] x2, list of 50 objects 67.3k 102.0k +51.6%
anyOf [string, null], single object 5.93M 7.34M +23.8%
oneOf of two objects, list of 50 177.9k 314.4k +76.8%
if / then / else, single object 5.99M 7.50M +25.2%
control: user profile response 498.2k 502.2k +0.8%

Node.js 24.22.0 (V8 13.6):

Case fast-json-stringify v7.3.5 (default mechanism) This PR (default mechanism) Difference
anyOf [string, null] x2, list of 50 objects 64.1k 95.8k +49.4%
anyOf [string, null], single object 5.78M 7.21M +24.9%
oneOf of two objects, list of 50 163.9k 271.3k +65.6%
if / then / else, single object 6.35M 8.04M +26.5%
control: user profile response 494.6k 504.5k +2.0%

Node.js 22.23.1 (V8 12.4):

Case fast-json-stringify v7.3.5 (default mechanism) This PR (default mechanism) Difference
anyOf [string, null] x2, list of 50 objects 60.5k 93.7k +55.0%
anyOf [string, null], single object 5.90M 7.47M +26.6%
oneOf of two objects, list of 50 155.3k 275.0k +77.1%
if / then / else, single object 6.51M 8.53M +31.0%
control: user profile response 652.7k 654.7k +0.3%

The last row is a control: the user profile schema of benchmark/fixtures/api-responses has no anyOf / oneOf / if, so its generated code does not change.

Note:

  • semver-patch. No option or type change. Validator#validate is kept, so standalone files generated by earlier versions keep working.
  • Independent from the objectMechanism: 'native' fix: the two PRs touch different files and can be merged in any order.

@Tony133
Tony133 marked this pull request as ready for review November 10, 2026 17:04

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@gurgunday gurgunday left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@Tony133
Tony133 merged commit 85c54fa into main Nov 11, 2026
20 checks passed
@Tony133
Tony133 deleted the perf/cache-schema-validators branch November 11, 2026 15:27
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.

3 participants