Background
Issue #378 requested $dynamicRef / $dynamicAnchor dereference support for the OpenAPI 3.1 namespace in 2021. It was closed in November 2025 without a dedicated implementation. Issue #306 is the parent JSON Schema 2020-12 dereferencing epic.
ApiDOM already parses $dynamicRef and $dynamicAnchor into the element tree, but the OpenAPI 3.1 and 3.2 dereference strategies only dereference $ref. Schemas that rely on dynamic references currently pass through unresolved.
Ecosystem context
The openapi-dynamicref-adoption-tracker repository tracks $dynamicRef / $dynamicAnchor support across the OpenAPI tooling ecosystem (SDK generators, parsers, validators). It provides:
- Validator-backed fixtures for recursive trees, nested generics, and non-identifier schema keys
- A compatibility matrix covering 9 SDK generators across the TypeScript ecosystem
- A state-of-the-union snapshot with per-fixture AJV / Hyperjump validation results
- An implementation guide for adding
$dynamicRef support to generators and parsers
- A combined showcase fixture (
petstore-dynamicref-showcase.yaml) exercising all $dynamicRef patterns in a realistic API
The initial SDK snapshot confirmed: no tested tool preserves $dynamicRef type fidelity. Generators either fail to parse specs containing $dynamicAnchor, emit unknown/any/Object for dynamic ref slots, or materialize generic/template fixtures as duplicate concrete types. This PR adds the parser-level dereferencing that downstream tools need.
Motivation
Downstream tools that consume ApiDOM dereferenced output should be able to work with OpenAPI 3.1/3.2 schemas that use JSON Schema 2020-12 dynamic references for recursive schemas and generic schema templates.
Proposed stages
Each stage corresponds to a single commit in PR #5177. They can be split into separate PRs if the maintainer prefers smaller reviews.
Stage 1: Add $dynamicAnchor selectors and error types
Closed by 175cccd
- Add
selectors/$dynamicAnchor.ts to both openapi-3-1 and openapi-3-2 dereference strategies
isDynamicAnchor — validates anchor syntax (^[A-Za-z_][A-Za-z_0-9.-]*$)
uriToDynamicAnchor — extracts anchor token from URI fragment
evaluate — finds the schema element with a matching $dynamicAnchor value via apidom-core's find
parse — validates anchor format, throws InvalidJsonSchema$dynamicAnchorError on invalid input
- Add error class hierarchy:
JsonSchema$dynamicAnchorError (base, extends ApiDOMError)
EvaluationJsonSchema$dynamicAnchorError (evaluation failure)
InvalidJsonSchema$dynamicAnchorError (syntax validation failure)
Stage 2: Add $dynamicRef dereferencing in both visitors
Closed by 1052435
- Add
resolveSchema$dynamicRefField to util.ts in both strategies — computes the base URI for a $dynamicRef by walking ancestorsSchemaIdentifiers, matching the existing resolveSchema$refField pattern
- Add
resolveSchema$dynamicRef method to both OpenAPI3_1DereferenceVisitor and OpenAPI3_2DereferenceVisitor
- Walks the dynamic scope (ancestor lineage, innermost-first) looking for a schema with a matching
$dynamicAnchor
- Falls back to document-level resolution when no ancestor in dynamic scope has a matching anchor — resolves the URI, loads the target resource, searches for the anchor or evaluates the JSON Pointer
- Uses the same merge/transclusion semantics as
$ref: referencing-element properties are merged on top of the transcluded schema, $dynamicRef keyword is removed from output
- Preserves all dereference metadata:
ref-fields ($dynamicRef, $dynamicRefBaseURI), ref-origin, ref-referencing-element-id
- Handles circular references identically to
$ref: max-depth checks, circular: 'error' | 'replace' | 'ignore'
- Supports boolean JSON schemas
- Supports external
$dynamicRef — cross-file resolution with resolve.internal/resolve.external and skipNestedExternal options
$ref takes precedence — if a schema has both $ref and $dynamicRef, existing $ref handling runs; $dynamicRef only activates when $ref is absent
- Update
SchemaElement visitor entry point to dispatch to resolveSchema$dynamicRef when the schema has $dynamicRef but not $ref
- Update
getNestedVisitorOptions to handle $dynamicRef in skipNestedExternal logic
Stage 3: Add test coverage for internal, external, fallback, and recursive $dynamicRef
Closed by f085ce8
- Add test fixtures and assertions for both OAS 3.1 and 3.2:
$dynamicRef-internal: $dynamicRef points to a $dynamicAnchor in a sibling schema within the same document — verifies transclusion
$dynamicRef-external: $dynamicRef points to a $dynamicAnchor in an external file — verifies cross-file resolution (with skipNestedExternal)
$dynamicRef-fallback: $dynamicRef has no ancestor override — verifies fallback to document-level $dynamicAnchor search
$dynamicRef-recursive: $dynamicRef creates a self-referential tree — verifies circularity detection (child items are the same element reference as parent)
Stage 4: Add petstore dynamic-ref showcase fixture
Closed by a7efa29
- Add the full petstore dynamic-ref showcase fixture from the openapi-dynamicref-adoption-tracker repo as a test fixture for both OAS 3.1 and 3.2
- Exercises all
$dynamicRef patterns in a realistic API:
- Generic response envelope (
ApiEnvelopeTemplate with $dynamicAnchor: dataType)
- Generic pagination (
PaginatedTemplate with $dynamicAnchor: itemType)
- Concrete pagination bindings (
PaginatedPetItems, PaginatedOwnerItems)
- Recursive category tree (
BaseSpeciesCategory / LocalizedSpeciesCategory with $dynamicAnchor: speciesCategory)
- Multi-parameter generic template (
ShelterFolderTemplate with folderType + resourceType)
- Typed request/response bodies
Expected outcome
Dereferencing should:
- Resolve
$dynamicRef by walking the dynamic scope (ancestor lineage, innermost-first) for a matching $dynamicAnchor
- Fall back to document-level anchor search when no dynamic scope match is found
- Remove the
$dynamicRef keyword from the output
- Transclude the matching schema using the same merge semantics as
$ref
- Detect and handle circular dynamic references identically to circular
$ref
- Apply to both OpenAPI 3.1 and 3.2 dereference strategies
Background
Issue #378 requested
$dynamicRef/$dynamicAnchordereference support for the OpenAPI 3.1 namespace in 2021. It was closed in November 2025 without a dedicated implementation. Issue #306 is the parent JSON Schema 2020-12 dereferencing epic.ApiDOM already parses
$dynamicRefand$dynamicAnchorinto the element tree, but the OpenAPI 3.1 and 3.2 dereference strategies only dereference$ref. Schemas that rely on dynamic references currently pass through unresolved.Ecosystem context
The openapi-dynamicref-adoption-tracker repository tracks
$dynamicRef/$dynamicAnchorsupport across the OpenAPI tooling ecosystem (SDK generators, parsers, validators). It provides:$dynamicRefsupport to generators and parserspetstore-dynamicref-showcase.yaml) exercising all$dynamicRefpatterns in a realistic APIThe initial SDK snapshot confirmed: no tested tool preserves
$dynamicReftype fidelity. Generators either fail to parse specs containing$dynamicAnchor, emitunknown/any/Objectfor dynamic ref slots, or materialize generic/template fixtures as duplicate concrete types. This PR adds the parser-level dereferencing that downstream tools need.Motivation
Downstream tools that consume ApiDOM dereferenced output should be able to work with OpenAPI 3.1/3.2 schemas that use JSON Schema 2020-12 dynamic references for recursive schemas and generic schema templates.
Proposed stages
Each stage corresponds to a single commit in PR #5177. They can be split into separate PRs if the maintainer prefers smaller reviews.
Stage 1: Add
$dynamicAnchorselectors and error typesClosed by
175cccdselectors/$dynamicAnchor.tsto bothopenapi-3-1andopenapi-3-2dereference strategiesisDynamicAnchor— validates anchor syntax (^[A-Za-z_][A-Za-z_0-9.-]*$)uriToDynamicAnchor— extracts anchor token from URI fragmentevaluate— finds the schema element with a matching$dynamicAnchorvalue viaapidom-core'sfindparse— validates anchor format, throwsInvalidJsonSchema$dynamicAnchorErroron invalid inputJsonSchema$dynamicAnchorError(base, extendsApiDOMError)EvaluationJsonSchema$dynamicAnchorError(evaluation failure)InvalidJsonSchema$dynamicAnchorError(syntax validation failure)Stage 2: Add
$dynamicRefdereferencing in both visitorsClosed by
1052435resolveSchema$dynamicRefFieldtoutil.tsin both strategies — computes the base URI for a$dynamicRefby walkingancestorsSchemaIdentifiers, matching the existingresolveSchema$refFieldpatternresolveSchema$dynamicRefmethod to bothOpenAPI3_1DereferenceVisitorandOpenAPI3_2DereferenceVisitor$dynamicAnchor$ref: referencing-element properties are merged on top of the transcluded schema,$dynamicRefkeyword is removed from outputref-fields($dynamicRef,$dynamicRefBaseURI),ref-origin,ref-referencing-element-id$ref: max-depth checks,circular: 'error' | 'replace' | 'ignore'$dynamicRef— cross-file resolution withresolve.internal/resolve.externalandskipNestedExternaloptions$reftakes precedence — if a schema has both$refand$dynamicRef, existing$refhandling runs;$dynamicRefonly activates when$refis absentSchemaElementvisitor entry point to dispatch toresolveSchema$dynamicRefwhen the schema has$dynamicRefbut not$refgetNestedVisitorOptionsto handle$dynamicRefinskipNestedExternallogicStage 3: Add test coverage for internal, external, fallback, and recursive
$dynamicRefClosed by
f085ce8$dynamicRef-internal:$dynamicRefpoints to a$dynamicAnchorin a sibling schema within the same document — verifies transclusion$dynamicRef-external:$dynamicRefpoints to a$dynamicAnchorin an external file — verifies cross-file resolution (withskipNestedExternal)$dynamicRef-fallback:$dynamicRefhas no ancestor override — verifies fallback to document-level$dynamicAnchorsearch$dynamicRef-recursive:$dynamicRefcreates a self-referential tree — verifies circularity detection (child items are the same element reference as parent)Stage 4: Add petstore dynamic-ref showcase fixture
Closed by
a7efa29$dynamicRefpatterns in a realistic API:ApiEnvelopeTemplatewith$dynamicAnchor: dataType)PaginatedTemplatewith$dynamicAnchor: itemType)PaginatedPetItems,PaginatedOwnerItems)BaseSpeciesCategory/LocalizedSpeciesCategorywith$dynamicAnchor: speciesCategory)ShelterFolderTemplatewithfolderType+resourceType)Expected outcome
Dereferencing should:
$dynamicRefby walking the dynamic scope (ancestor lineage, innermost-first) for a matching$dynamicAnchor$dynamicRefkeyword from the output$ref$ref