Summary
OpenAPI 3.1 uses JSON Schema 2020-12 as its schema dialect, including $dynamicRef and $dynamicAnchor for dynamic scope-aware schema resolution.
@hey-api/openapi-ts currently accepts OpenAPI 3.1 specs containing these keywords, but generated TypeScript can lose the dynamic reference semantics and fall back to unknown where the active dynamic scope should preserve a more specific schema type.
This affects recursive schemas and reusable wrapper/template patterns where preserving type fidelity is important for SDK usability.
Reproduction
Validator-backed fixtures:
Additional pagination/generic-wrapper fixtures:
All fixtures pass OpenAPI document validation with Redocly, openapi-spec-validator, Spectral, and swagger-cli. The recursive and nested fixtures pass AJV 2020 runtime validation. The pagination fixtures follow the JSON Schema dynamic reference/generic wrapper pattern and pass Hyperjump runtime validation, while AJV currently disagrees, so those are documented as mixed-validator-support cases.
Expected Behavior
Generated TypeScript should preserve $dynamicRef semantics where they can be resolved statically.
For recursive schemas, dynamic scope should preserve the active recursive type. For example, BaseCategory.children should resolve to the active LocalizedCategory type when used through LocalizedCategory, rather than degrading to unknown[].
For nested schemas, dynamic references should preserve the active folder/resource types instead of producing unknown in unions.
For reusable wrapper/template schemas, the generator should preserve the type relationship between the generic template and the bound schemas. This can be represented as TypeScript generics instead of duplicating fully materialized concrete object types.
For example, given a paginated template schema, generated output like this would preserve the desired type fidelity:
export type PaginatedTemplate<ItemType> = {
items?: Array<ItemType>;
total?: number;
};
export type PaginatedUserResponse = PaginatedTemplate<User>;
export type PaginatedGroupResponse = PaginatedTemplate<Group>;
This is preferable to either:
export type PaginatedUserResponse = PaginatedTemplate;
or:
export type PaginatedUserResponse = {
items?: Array<unknown>;
total?: number;
};
Actual Behavior
| Fixture |
Current @hey-api/openapi-ts output |
Expected type fidelity |
recursive-category-tree |
children: Array<unknown> |
children: Array<LocalizedCategory> or equivalent recursive type |
nested-workspace-resources |
children: Array<Document | unknown>, shortcuts: Array<unknown> |
children: Array<Document | WorkspaceFolder>, shortcuts: Array<WorkspaceResource> |
generic-schema-binding |
items: Array<unknown>, type PaginatedUserResponse = PaginatedTemplate |
PaginatedTemplate<User> / PaginatedTemplate<Group> or equivalent concrete type fidelity |
paginated-response |
route response remains PaginatedTemplate with no item binding |
response type preserves the bound item type, preferably via generic type arguments |
Minimal Pagination Example
components:
schemas:
PaginatedTemplate:
$defs:
itemType:
$dynamicAnchor: itemType
not: {}
type: object
properties:
items:
type: array
items:
$dynamicRef: '#itemType'
total:
type: integer
PaginatedUserResponse:
$defs:
itemType:
$dynamicAnchor: itemType
$ref: '#/components/schemas/User'
$ref: '#/components/schemas/PaginatedTemplate'
Expected generated type fidelity would preserve items: Array<User> for PaginatedUserResponse, either by materializing the object type or by emitting a generic template reference such as:
export type PaginatedTemplate<ItemType> = {
items?: Array<ItemType>;
total?: number;
};
export type PaginatedUserResponse = PaginatedTemplate<User>;
Local Investigation
From a local review, schemaToIrSchema() in packages/shared/src/openApi/3.1.x/parser/schema.ts handles static $ref, enum, allOf, anyOf, oneOf, typed schemas, properties, and contentMediaType.
A schema containing only $dynamicRef does not currently match those branches, so it can fall through to parseUnknown().
Related areas appear to be:
packages/spec-types/src/json-schema/draft-2020-12/spec.ts - the JSON Schema 2020-12 type model appears to include $ref, but not $dynamicRef, $dynamicAnchor, or $defs.
packages/shared/src/openApi/shared/types/schema.ts - parser state currently tracks static $ref context, but not dynamic scope or template type parameters.
packages/shared/src/openApi/3.1.x/parser/schema.ts - likely place to build dynamic scope, resolve $dynamicRef, and detect generic/template bindings before IR reaches plugins.
packages/openapi-ts/src/plugins/@hey-api/typescript - may need to understand generic type parameters and type arguments if the IR preserves template semantics instead of fully materializing every concrete schema.
Possible Implementation Direction
A first implementation could support two related paths:
-
Dynamic scope resolution for recursive/nested schemas:
- Add
$dynamicRef, $dynamicAnchor, and $defs support to the JSON Schema 2020-12 spec types.
- Track dynamic anchors while parsing named schemas.
- Resolve
$dynamicRef through the current dynamic scope where possible.
- Preserve the existing
unknown fallback when a dynamic reference cannot be resolved statically.
-
Generic/template preservation for reusable wrappers:
- Detect template schemas where
$defs entries define $dynamicAnchor placeholders without $ref bindings.
- Represent those placeholders as TypeScript type parameters.
- Detect schemas that bind those anchors via
$defs entries with $ref.
- Emit generic type references such as
PaginatedTemplate<User> instead of losing the bound schema type or duplicating the full object shape.
This keeps recursive cases and generic wrapper cases aligned with JSON Schema 2020-12 semantics while producing idiomatic TypeScript.
Why This Matters
OpenAPI 3.1 adopted JSON Schema 2020-12, and $dynamicRef is the standard mechanism for dynamic scope-aware schema reuse. It enables recursive structures and generic-like wrapper schemas without duplicating schema definitions.
Without support, generated SDK types remain syntactically valid but lose important type information, which affects autocomplete, type checking, and client correctness.
Compatibility Evidence
A cross-generator compatibility matrix and reproducible fixtures are tracked here:
https://github.com/aqeelat/openapi-dynamicref-adoption-tracker
A similar feature request was opened for Orval here:
orval-labs/orval#3352
In the tested matrix, @hey-api/openapi-ts is already ahead of several generators because it parses and generates successfully. The remaining gap is preserving $dynamicRef semantics instead of degrading to unknown.
References
Willing to Contribute
Yes. I have a working implementation plan and can submit a PR.
This issue was drafted with assistance from AI tooling. The submitter is responsible for reviewing and validating the contents before submission.
Update (2026-05-17)
Implementation work is being tracked in PR #3889: #3889
During implementation, the approach changed from generating only materialized concrete schemas to preserving generic/template semantics where appropriate.
For recursive and nested $dynamicRef cases, the parser resolves dynamic references through the active dynamic scope and preserves concrete recursive/resource types.
For reusable wrapper schemas, the implementation now generates idiomatic TypeScript generics. Template schemas with $dynamicAnchor placeholders become generic type aliases, and schemas that bind those anchors become generic references such as PaginatedTemplate<User>.
This better matches the JSON Schema dynamic reference/generic-wrapper pattern, avoids duplicating object shapes, and still preserves the concrete item/resource type information in generated SDK types.
Summary
OpenAPI 3.1 uses JSON Schema 2020-12 as its schema dialect, including
$dynamicRefand$dynamicAnchorfor dynamic scope-aware schema resolution.@hey-api/openapi-tscurrently accepts OpenAPI 3.1 specs containing these keywords, but generated TypeScript can lose the dynamic reference semantics and fall back tounknownwhere the active dynamic scope should preserve a more specific schema type.This affects recursive schemas and reusable wrapper/template patterns where preserving type fidelity is important for SDK usability.
Reproduction
Validator-backed fixtures:
Additional pagination/generic-wrapper fixtures:
All fixtures pass OpenAPI document validation with Redocly, openapi-spec-validator, Spectral, and swagger-cli. The recursive and nested fixtures pass AJV 2020 runtime validation. The pagination fixtures follow the JSON Schema dynamic reference/generic wrapper pattern and pass Hyperjump runtime validation, while AJV currently disagrees, so those are documented as mixed-validator-support cases.
Expected Behavior
Generated TypeScript should preserve
$dynamicRefsemantics where they can be resolved statically.For recursive schemas, dynamic scope should preserve the active recursive type. For example,
BaseCategory.childrenshould resolve to the activeLocalizedCategorytype when used throughLocalizedCategory, rather than degrading tounknown[].For nested schemas, dynamic references should preserve the active folder/resource types instead of producing
unknownin unions.For reusable wrapper/template schemas, the generator should preserve the type relationship between the generic template and the bound schemas. This can be represented as TypeScript generics instead of duplicating fully materialized concrete object types.
For example, given a paginated template schema, generated output like this would preserve the desired type fidelity:
This is preferable to either:
or:
Actual Behavior
@hey-api/openapi-tsoutputrecursive-category-treechildren: Array<unknown>children: Array<LocalizedCategory>or equivalent recursive typenested-workspace-resourceschildren: Array<Document | unknown>,shortcuts: Array<unknown>children: Array<Document | WorkspaceFolder>,shortcuts: Array<WorkspaceResource>generic-schema-bindingitems: Array<unknown>,type PaginatedUserResponse = PaginatedTemplatePaginatedTemplate<User>/PaginatedTemplate<Group>or equivalent concrete type fidelitypaginated-responsePaginatedTemplatewith no item bindingMinimal Pagination Example
Expected generated type fidelity would preserve
items: Array<User>forPaginatedUserResponse, either by materializing the object type or by emitting a generic template reference such as:Local Investigation
From a local review,
schemaToIrSchema()inpackages/shared/src/openApi/3.1.x/parser/schema.tshandles static$ref,enum,allOf,anyOf,oneOf, typed schemas, properties, andcontentMediaType.A schema containing only
$dynamicRefdoes not currently match those branches, so it can fall through toparseUnknown().Related areas appear to be:
packages/spec-types/src/json-schema/draft-2020-12/spec.ts- the JSON Schema 2020-12 type model appears to include$ref, but not$dynamicRef,$dynamicAnchor, or$defs.packages/shared/src/openApi/shared/types/schema.ts- parser state currently tracks static$refcontext, but not dynamic scope or template type parameters.packages/shared/src/openApi/3.1.x/parser/schema.ts- likely place to build dynamic scope, resolve$dynamicRef, and detect generic/template bindings before IR reaches plugins.packages/openapi-ts/src/plugins/@hey-api/typescript- may need to understand generic type parameters and type arguments if the IR preserves template semantics instead of fully materializing every concrete schema.Possible Implementation Direction
A first implementation could support two related paths:
Dynamic scope resolution for recursive/nested schemas:
$dynamicRef,$dynamicAnchor, and$defssupport to the JSON Schema 2020-12 spec types.$dynamicRefthrough the current dynamic scope where possible.unknownfallback when a dynamic reference cannot be resolved statically.Generic/template preservation for reusable wrappers:
$defsentries define$dynamicAnchorplaceholders without$refbindings.$defsentries with$ref.PaginatedTemplate<User>instead of losing the bound schema type or duplicating the full object shape.This keeps recursive cases and generic wrapper cases aligned with JSON Schema 2020-12 semantics while producing idiomatic TypeScript.
Why This Matters
OpenAPI 3.1 adopted JSON Schema 2020-12, and
$dynamicRefis the standard mechanism for dynamic scope-aware schema reuse. It enables recursive structures and generic-like wrapper schemas without duplicating schema definitions.Without support, generated SDK types remain syntactically valid but lose important type information, which affects autocomplete, type checking, and client correctness.
Compatibility Evidence
A cross-generator compatibility matrix and reproducible fixtures are tracked here:
https://github.com/aqeelat/openapi-dynamicref-adoption-tracker
A similar feature request was opened for Orval here:
orval-labs/orval#3352
In the tested matrix,
@hey-api/openapi-tsis already ahead of several generators because it parses and generates successfully. The remaining gap is preserving$dynamicRefsemantics instead of degrading tounknown.References
$dynamicRefWilling to Contribute
Yes. I have a working implementation plan and can submit a PR.
This issue was drafted with assistance from AI tooling. The submitter is responsible for reviewing and validating the contents before submission.
Update (2026-05-17)
Implementation work is being tracked in PR #3889: #3889
During implementation, the approach changed from generating only materialized concrete schemas to preserving generic/template semantics where appropriate.
For recursive and nested
$dynamicRefcases, the parser resolves dynamic references through the active dynamic scope and preserves concrete recursive/resource types.For reusable wrapper schemas, the implementation now generates idiomatic TypeScript generics. Template schemas with
$dynamicAnchorplaceholders become generic type aliases, and schemas that bind those anchors become generic references such asPaginatedTemplate<User>.This better matches the JSON Schema dynamic reference/generic-wrapper pattern, avoids duplicating object shapes, and still preserves the concrete item/resource type information in generated SDK types.