Skip to content

feat: add $dynamicRef and $dynamicAnchor support (OpenAPI 3.1 / JSON Schema 2020-12)#2824

Open
aqeelat wants to merge 2 commits into
openapi-ts:mainfrom
aqeelat:feat/dynamicref-support
Open

feat: add $dynamicRef and $dynamicAnchor support (OpenAPI 3.1 / JSON Schema 2020-12)#2824
aqeelat wants to merge 2 commits into
openapi-ts:mainfrom
aqeelat:feat/dynamicref-support

Conversation

@aqeelat
Copy link
Copy Markdown

@aqeelat aqeelat commented May 31, 2026

Closes #2029

Summary

Implements $dynamicRef and $dynamicAnchor support (JSON Schema Draft 2020-12 / OpenAPI 3.1), enabling generic/template schema patterns like PaginatedResponse<T> and recursive type hierarchies.

What it does

When a schema uses $ref to a template with $dynamicRef placeholders, and the caller provides $defs with $dynamicAnchor overrides, the generated TypeScript types are concretized — each override produces a distinct type with the correct substituted types instead of unknown.

Example input:

PaginatedTemplate:
  type: object
  properties:
    items:
      type: array
      items: { $dynamicRef: "#itemType" }
  $defs:
    itemType: { $dynamicAnchor: "itemType", not: {} }

PaginatedPetItems:
  $ref: "#/components/schemas/PaginatedTemplate"
  $defs:
    itemType: { $dynamicAnchor: "itemType", $ref: "#/components/schemas/Pet" }

Output:

PaginatedTemplate: { items?: unknown[]; ... }        // standalone: no override → unknown
PaginatedPetItems: { items?: components["schemas"]["Pet"][]; ... }  // concretized with Pet

Supported patterns

Pattern Example
Generic/template schemas via $ref + $defs PaginatedResponse<T>
Recursive self-reference via $dynamicAnchor on schema BaseCategory { children: Self[] }
Child override of parent anchor LocalizedCategory overrides children: LocalizedCategory[]
Multiple dynamic anchors ShelterFolder with folderType + resourceType
$dynamicRef inside allOf/oneOf/anyOf Nested composition
Chained templates ApiEnvelope > PaginatedResponse > Pet

Implementation

  • SchemaObject — Added $dynamicRef?: string and $dynamicAnchor?: string
  • TransformNodeOptions — Added dynamicAnchors for scope propagation through recursive transforms
  • collectDynamicAnchors() — Collects $dynamicAnchor entries from $defs, stripping the anchor keyword
  • resolveDynamicAnchor() — Resolves anchor name: checks parent scope first (override), then local $defs (fallback)
  • schemaContainsDynamicRef() — Guards $ref concretization to only inline when the target actually uses $dynamicRef
  • $ref handler — When $defs provide dynamic anchors AND the resolved target contains $dynamicRef, concretizes inline with merged anchors
  • $dynamicRef handler — Resolves to override type, returns oapiRef() for $ref overrides (prevents infinite recursion), transforms inline for schema overrides
  • $dynamicAnchor handler — Self-registers schema as anchor for recursive type generation
  • Parent $defs propagation — Non-$ref schemas with $defs anchors pass them to child transforms

Testing

  • 336 tests total (43 new)
    • 27 unit tests in dynamic-ref.test.ts covering every branch in the transform logic
    • 16 direct utility tests in utils.test.ts for schemaContainsDynamicRef, collectDynamicAnchors, resolveDynamicAnchor
    • 3 integration tests in index.test.ts with full OpenAPI documents (paginated response, recursive allOf, multiple anchors with oneOf)
  • All pre-existing tests continue to pass (293 → 336)
  • Lint clean, TypeScript type-check clean

Files changed

File Change
src/types.ts Add $dynamicRef, $dynamicAnchor to SchemaObject; add dynamicAnchors to TransformNodeOptions
src/lib/utils.ts Add collectDynamicAnchors(), resolveDynamicAnchor(), schemaContainsDynamicRef()
src/transform/schema-object.ts Add $dynamicRef handler, $dynamicAnchor self-registration, $ref concretization with dynamic anchors, parent $defs propagation
test/transform/schema-object/dynamic-ref.test.ts 27 new unit tests (new file)
test/lib/utils.test.ts 16 new direct utility tests
test/index.test.ts 3 new integration tests

Implements JSON Schema Draft 2020-12 / OpenAPI 3.1 $dynamicRef and
$dynamicAnchor keywords, enabling generic/template schema patterns
like PaginatedResponse<T> and recursive type hierarchies.

Closes openapi-ts#2029

Changes:
- Add $dynamicRef and $dynamicAnchor to SchemaObject type
- Add dynamicAnchors to TransformNodeOptions for scope propagation
- Add collectDynamicAnchors(), resolveDynamicAnchor(),
  schemaContainsDynamicRef() utilities
- Handle $dynamicRef resolution with parent scope override semantics
- Handle $dynamicAnchor self-registration for recursive types
- Handle $ref concretization when resolved target contains $dynamicRef
- Parent $defs anchors propagate to child $dynamicRef in non-$ref schemas
- 336 tests passing (43 new: 27 unit, 16 utility direct, 3 integration)
@aqeelat aqeelat requested a review from a team as a code owner May 31, 2026 01:04
@aqeelat aqeelat requested a review from gzm0 May 31, 2026 01:04
@netlify
Copy link
Copy Markdown

netlify Bot commented May 31, 2026

👷 Deploy request for openapi-ts pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 5c312ff

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 31, 2026

🦋 Changeset detected

Latest commit: 5c312ff

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

This PR includes changesets to release 1 package
Name Type
openapi-typescript Minor

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

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.

implement OAS3.1 dynamic references

1 participant