feat(reference): dereference dynamic schema refs#5177
Open
aqeelat wants to merge 3 commits into
Open
Conversation
Implement full $dynamicRef/$dynamicAnchor dereferencing in the openapi-3-1 and openapi-3-2 dereference strategy visitors. - Add $dynamicAnchor selectors (isDynamicAnchor, uriToDynamicAnchor, parse, evaluate) with public API exports - Add resolveSchema$dynamicRef method with static target resolution, dynamic scope walk (innermost-first per JSON Schema 2020-12 §7.7.3), and external reference fallback - Support resolve.internal/external flags, continueOnError, circular reference handling (error/replace/ignore), maxDepth, boolean JSON Schemas, allOf discriminator mapping, and ancestor $id base URI - Apply this.constructor pattern for all nested visitor instantiation in both resolveSchema$ref and resolveSchema$dynamicRef - OAS 3.2 visitor carries $selfValue through nested visitors Closes swagger-api#5176
33 new tests covering all code paths in resolveSchema$dynamicRef (1276 total passing). Both OAS 3.1 and OAS 3.2 strategies tested. Integration tests: internal/external $dynamicRef, embedded canonical $id, missing static target, fallback, recursive, nested scope, static anchor, boolean JSON Schema, circular (error/replace/ignore), max depth, allOf+discriminator, resolve flags (internal/external), continueOnError (anchor/missing/circular), external scope override, direct self-reference, ancestor $id base URI, OAS 3.2 $self. Unit tests: $dynamicAnchor selectors (isDynamicAnchor, uriToDynamicAnchor, parse, evaluate) for both strategies.
…cRef resolution Replace the ad-hoc [indirections, ...ancestorsLineage] scope walk in resolveSchema$dynamicRef() with a full explicit DynamicScopeStack that tracks $dynamicAnchor declarations across nested schema scopes per the JSON Schema 2020-12 specification. Key changes: - Add DynamicScopeStack class with push/pop/resolveDynamicRef/clone - Wire dynamicScopeStack into OAS 3.1 and 3.2 dereference visitors - Add toDynamicScopeStack helper to build inherited scope from ancestors - Add hasDynamicScopeOverride guard for schemas with $dynamicAnchor/$defs - Export DynamicScopeStack and related types from package index - Add $dynamicRef-wrapper-defs and $dynamicRef-wrapper-ref-template test fixtures for both OAS 3.1 and OAS 2 - Fix $dynamicAnchor selector test lint issues All 1280 tests passing, type-check and lint clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
$dynamicRef/$dynamicAnchordereferencing to the OpenAPI 3.1 and 3.2 dereference strategies inapidom-reference.ApiDOM already parses these keywords into the element tree (via
apidom-ns-json-schema-2020-12), but the dereference visitors only resolved$ref. Schemas using dynamic references passed through with$dynamicRefunresolved.Closes #5176
Related: #378 (original 2021 request), #306 (JSON Schema 2020-12 dereferencing epic)
Context
The openapi-dynamicref-adoption-tracker tracks
$dynamicRefsupport across the OpenAPI ecosystem. Its initial SDK snapshot confirmed that no tested generator preserves dynamic reference type fidelity — tools either fail to parse specs containing$dynamicAnchor, emitunknown/anyfor dynamic ref slots, or materialize generic templates as duplicate concrete types. This PR adds the parser-level dereferencing that downstream tools need.Test fixtures are sourced from the tracker repo's petstore-dynamicref-showcase.yaml and its focused per-pattern fixtures.
Design decisions
1. Same merge/transclusion semantics as
$ref$dynamicRefschemas are transcluded inline with referencing-element properties merged on top. The$dynamicRefkeyword is removed from the output. This matches the existing$refbehavior exactly.2. Static target resolution before dynamic scope
The resolver first resolves the static target of
$dynamicRefusing the canonical URI selector (uriEvaluate), which matches schemas by$idwithin the current document. If the$dynamicReffragment is a$dynamicAnchortoken, the URI is stripped of its hash to locate the$idresource, then$dynamicAnchorEvaluatefinds the anchor within that resource.Only after the static target is resolved does the resolver walk
ancestorsLineage(innermost-first) to check for a dynamic scope override. This ensures:$idtargets are found without attempting external loads (important forresolve.external: falseand offline use)../missing.json#node) fail before dynamic scope can mask the broken reference with a coincidental local anchor match.If the static target cannot be found in the current document (
EvaluationJsonSchemaUriError), the resolver falls back to loading the URI as an external reference (matching the$refcode path).3. DynamicScopeStack for spec-aligned dynamic scope resolution
The initial implementation walked
ancestorsLineage(innermost-first) looking for a matching$dynamicAnchor. This heuristic was sufficient for simple cases but did not fully model the JSON Schema 2020-12 dynamic scope — particularly for schemas that use$defsto wrap$dynamicAnchordeclarations without themselves containing$dynamicRef.The
DynamicScopeStackreplaces that heuristic with an explicit scope stack:DynamicScopeFrame— captured at each schema boundary that crosses into a new scope (e.g.,$reftargets,$defsentries). Each frame stores the$dynamicAnchordeclarations found in its subtree.collectDynamicAnchors()— traverses an element tree and collects all$dynamicAnchordeclarations (handles both namespace elements with$dynamicAnchoras a direct property and genericObjectElements usingget('$dynamicAnchor')).DynamicScopeStack.resolveDynamicRef()— walks frames from index 0 (outermost) per JSON Schema 2020-12 §8.5.2; first match wins.toDynamicScopeStack()— builds the inherited scope by cloning the parent visitor's stack and adding ancestor frames matching theisDynamicScopeCandidatefilter.hasDynamicScopeOverrideguard — forces traversal of schemas that have$dynamicAnchoror$defs(even without$dynamicRef) so their$dynamicAnchordeclarations enter the scope.The stack is cloned for each nested visitor, so different traversal branches maintain independent scopes — matching the spec's lexical scoping semantics.
4. Fallback to external reference loading
When the static target is not found in the current document, the resolver loads the
$dynamicRefURI as an external reference and searches the returned resource for the anchor (or evaluates the JSON Pointer). This handles the case where$dynamicRefis used without any ancestor override — it behaves like a plain$refto an anchor.5.
$reftakes precedenceIf a schema has both
$refand$dynamicRef, the existing$refhandling runs unchanged.$dynamicRefonly activates when$refis absent. This preserves backward compatibility and avoids changing any existing$refcode paths.6. Full metadata preservation
All dereference metadata is carried through identically to
$ref:ref-fields:$dynamicRefand$dynamicRefBaseURIref-origin: source URI of the resolved element (OAS 3.2 uses$selfvalue when present)ref-referencing-element-id: identity of the referencing schemaancestorsSchemaIdentifiers: used byresolveSchema$dynamicRefFieldto compute the base URI7. Circular reference handling
Circular dynamic references are detected and handled the same way as circular
$ref:referencingElement === referencedElement)dereference.maxDepth)circular: 'error'— throwscircular: 'replace'— inserts aRefElementwith$dynamicRefmetadatacircular: 'ignore'— continues8. External
$dynamicRefsupportCross-file dynamic references are resolved through the existing
toReferencemechanism. The resolver respects:resolve.internal/resolve.externalflagsskipNestedExternaloptiongetNestedVisitorOptionsupdated to check$dynamicRef(not just$ref) for internal reference detection9.
continueOnErrorsupportresolveSchema$dynamicRefuseshandleDereferenceErrorthroughout, enablingcontinueOnError: trueto collect errors from non-existent anchors, missing external files, and circular references without aborting the entire dereference pass.10. Boolean JSON Schema support
Boolean schemas (
true/false) encountered as dynamic ref targets are handled with the same clone-and-annotate approach as$ref.11.
this.constructorpattern for visitor instantiationAll nested visitor creation sites (in both
resolveSchema$refandresolveSchema$dynamicRef) use thethis.constructorpattern instead of directnew OpenAPI3_xDereferenceVisitor(...). This ensures subclass compatibility — if the visitor is extended, nested visitors will also be instances of the subclass.12. Symmetric for OAS 3.1 and 3.2
Identical logic in both strategies. The OAS 3.2 visitor additionally carries
$selfValuethrough nested visitors for external references (matching its existing$refbehavior).Test coverage
1280 tests passing (37 new tests added for
$dynamicRef).Integration tests (both OAS 3.1 and 3.2)
$dynamicRef-internal$dynamicRef→ sibling schema$dynamicAnchorin same document$dynamicRef-external$dynamicRef→$dynamicAnchorin external fileskipNestedExternal$dynamicRef-embedded-canonical-id$dynamicRef→ schema with$id+$dynamicAnchorin same documenturiEvaluatewithout external load, works withresolve.external: false$dynamicRef-missing-static-target$dynamicRef→ non-existent external file$dynamicRef-fallback$dynamicRefwith no ancestor override$dynamicRef-recursive$dynamicRef→$dynamicAnchor$dynamicRef-nested-scope$dynamicAnchorat multiple levels$dynamicAnchorper JSON Schema 2020-12 §7.7.3$dynamicRef-boolean-json-schema$dynamicRef→ boolean JSON Schema$dynamicRef-non-existent-anchor$dynamicRef→ non-existent anchor$dynamicRef-circular-error$dynamicRefwithcircular: 'error'$dynamicRef-circular-replace$dynamicRefwithcircular: 'replace'RefElementplaceholder$dynamicRef-max-depth$dynamicRefwithmaxDepthMaximumDereferenceDepthError$dynamicRef-static-anchor$dynamicReftargeting ordinary$anchor(not$dynamicAnchor)$ref, no dynamic scope$dynamicRef-ignore-internal$dynamicRefwithresolve: { internal: false }$dynamicRef-ignore-external$dynamicRefwithresolve: { external: false }$dynamicRef-continue-on-error-anchor$dynamicRef→ non-existent anchor +continueOnError$dynamicRef-continue-on-error-missing$dynamicRef→ missing external file +continueOnError$dynamicRef-continue-on-error-circular$dynamicRef+circular: 'error'+continueOnError$dynamicRef-external-scope-override$dynamicRefwith root doc$dynamicAnchoroverride$dynamicRef-self-referencing$dynamicAnchor+$dynamicRefon same element$dynamicRef-ancestor-id$dynamicRefreferencing$id-qualified$dynamicAnchorancestorsSchemaIdentifiers$dynamicRef-wrapper-defs$defswith$dynamicAnchorat each level$dynamicRef-wrapper-ref-template$ref-entered template with caller override$dynamicAnchoroverrides template defaultOAS 3.2-only
$dynamicRef-self-deref$dynamicRef+$selfon root OpenAPI documentref-originuses$selfvalueUnit tests (both OAS 3.1 and 3.2)
$dynamicAnchorselector:isDynamicAnchor,uriToDynamicAnchor,parse,evaluate— valid/invalid anchors, URI extraction, error types.Not wired
$dynamicRef-showcase$refresolution that prevent wiringCommit structure
fbb0448feat(reference): add $dynamicRef dereferencing for OAS 3.1 and 3.2— implementation (11 files)83b7430test(reference): add comprehensive $dynamicRef test coverage— tests and fixtures (85 files)a6751cafeat(reference): implement DynamicScopeStack for spec-aligned $dynamicRef resolution— replaces ad-hoc scope walk (13 files)Validation
Future work
$dynamicRef-showcasefixture has pre-existing JSON Pointer errors during$refresolution that prevent wiring. Fixing those is orthogonal to this PR.$dynamicRefvs$refdereferencing can be added in a follow-up.