Skip to content

fix: scope AJV instances per lintDocument call to fix concurrent validation#2774

Open
vlindhol wants to merge 1 commit intoRedocly:mainfrom
vlindhol:fix/ajv-concurrent-lint-resolve
Open

fix: scope AJV instances per lintDocument call to fix concurrent validation#2774
vlindhol wants to merge 1 commit intoRedocly:mainfrom
vlindhol:fix/ajv-concurrent-lint-resolve

Conversation

@vlindhol
Copy link
Copy Markdown

@vlindhol vlindhol commented Apr 24, 2026

Summary

The global AJV singleton in packages/core/src/rules/ajv.ts captured a resolve closure from whichever document first created the instance. When consumers like openapi-typescript process multiple APIs concurrently via Promise.all (using @redocly/openapi-core's lintDocument), the second+ document's resolve was silently discarded. This produced spurious warnings:

⚠ Example validation errored: can't resolve reference #/components/schemas/Foo
  from id .../spec.yaml#/paths/~1items/post/requestBody/content/application~1json/schema.

These warnings appeared for valid same-document $refs that have an example/examples sibling, but only when:

  • Multiple APIs are processed concurrently (e.g. Promise.all over lintDocument)
  • The specs contain cross-file $refs (triggering AJV's loadSchemaSync)

The redocly lint CLI was unaffected because it processes APIs sequentially.

I did a similar PR (#2135) last year to fix the same thing but it got stale, and this time I managed to create a minimal test case to prove the bug exists.

Fix

  • Replace the global singleton with an AjvValidator class that encapsulates AJV instance caching
  • Each lintDocument call creates its own AjvValidator instance, threaded through WalkContextUserContext
  • No more global state, no more stale closures

Changes

  • packages/core/src/rules/ajv.ts — Refactored from free functions + global singleton to an AjvValidator class with a validate() method and private getAjv()/getValidator() methods
  • packages/core/src/walk.ts — Added ajvValidator: AjvValidator to WalkContext and UserContext, threaded through all visitor context construction sites
  • packages/core/src/lint.ts — Each lintDocument/lintConfig call creates new AjvValidator() on the context (also removes the old releaseAjvInstance() call and its associated FIXME)
  • packages/core/src/rules/utils.tsvalidateExample calls ctx.ajvValidator.validate() instead of the old free function

Test plan

  • Added integration test: concurrent lint() via Promise.all on two specs with same-doc $ref + example + cross-file $ref — confirmed it fails before the fix and passes after
  • All 853 existing tests pass (127 test files)
  • Pre-commit hooks (lint + format) pass

@vlindhol vlindhol requested a review from a team as a code owner April 24, 2026 12:23
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 24, 2026

🦋 Changeset detected

Latest commit: 1157188

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@redocly/openapi-core Patch
@redocly/cli Patch
@redocly/respect-core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vlindhol vlindhol force-pushed the fix/ajv-concurrent-lint-resolve branch from 75353fe to 4572e82 Compare April 24, 2026 12:45
@vlindhol vlindhol force-pushed the fix/ajv-concurrent-lint-resolve branch 2 times, most recently from a0f6003 to 18bfbff Compare May 4, 2026 09:11
@vlindhol vlindhol requested a review from a team as a code owner May 4, 2026 09:11
…dation

The global AJV singleton in `ajv.ts` captured a `resolve` closure from
whichever document first created the instance. When consumers like
`openapi-typescript` process multiple APIs concurrently via
`Promise.all`, the second document's `resolve` was silently discarded,
producing spurious "Example validation errored: can't resolve reference"
warnings for valid same-document `$ref`s.

Replace the global singleton with an `AjvValidator` class instantiated
per `lintDocument` call and threaded through WalkContext/UserContext.
@vlindhol vlindhol force-pushed the fix/ajv-concurrent-lint-resolve branch from 18bfbff to 1157188 Compare May 4, 2026 09:13
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.

1 participant