Feature: Support $dynamicRef / $dynamicAnchor (JSON Schema 2020-12) for OpenAPI 3.1+
- Dominant language
- C#
- Stars
- 3.8k
- Forks
- 333
- Avg merge
- 16h 29m
- Merged PRs (30d)
- 116
Description
# $dynamicRef / $dynamicAnchor Support for OpenAPI 3.1+
## Summary
Kiota silently degrades OpenAPI 3.1+ schemas that use JSON Schema 2020-12 `$dynamicRef` / `$dynamicAnchor` ([spec §7.7](https://json-schema.org/draft/2020-12/json-schema-core#section-7.7)) to `UntypedNode`, which becomes `object` / `any` / `unknown` across all output languages. No error, no warning. The upstream parser (`Microsoft.OpenApi` 3.7.0) already parses and exposes both keywords on `IOpenApiSchema`, but Kiota's code-generation pipeline never reads them.
This affects two real-world uses:
1. **Recursive self-referential types** (validator-backed) — e.g. a category tree where `LocalizedCategory.children` should be `LocalizedCategory[]` but generates as `unknown[]`.
2. **Generic / reusable response wrappers** (mixed validator support) — e.g. `PaginatedTemplate` with `PaginatedUserResponse = PaginatedTemplate` and `PaginatedGroupResponse = PaginatedTemplate`. Kiota currently can't emit the reusable template; consumers get duplicated concrete wrappers or untyped `items`.
## Reproduction
Minimal fixtures (small, single-purpose) are available in the tracker repo:
- Recursive category tree (validator-backed): https://github.com/aqeelat/openapi-dynamicref-adoption-tracker/blob/main/fixtures/recursive-category-tree.yaml
- Nested workspace resources (validator-backed): https://github.com/aqeelat/openapi-dynamicref-adoption-tracker/blob/main/fixtures/nested-workspace-resources.yaml
- Non-identifier schema-key recursion (validator-backed): https://github.com/aqeelat/openapi-dynamicref-adoption-tracker/blob/main/fixtures/non-identifier-schema-key.yaml
- Pagination/generic wrapper, named schemas (mixed validator support): https://github.com/aqeelat/openapi-dynamicref-adoption-tracker/blob/main/fixtures/generic-schema-binding.yaml
- Pagination/generic wrapper, inline response binding (mixed validator support): https://github.com/aqeelat/openapi-dynamicref-adoption-tracker/blob/main/fixtures/paginated-response.yaml
- API envelope, double-wrapped generic: https://github.com/aqeelat/openapi-dynamicref-adoption-tracker/blob/main/fixtures/api-envelope.yaml
- Combined showcase across all patterns: https://github.com/aqeelat/openapi-dynamicref-adoption-tracker/blob/main/petstore-dynamicref-showcase.yaml
Running any of these through `kiota generate -d -l typescript` reproduces the issue.
## Root cause (verified against `main` HEAD `3939d526d`)
- Zero references to `DynamicRef` / `DynamicAnchor` anywhere in the Kiota tree (source + tests).
- `KiotaBuilder.CreateModelDeclarations` (`src/Kiota.Builder/KiotaBuilder.cs:1900`) dispatches on `IsReferencedSchema()` (which only matches `OpenApiSchemaReference`, i.e. `$ref`), inheritance, intersection, union, object, array, primitive — a schema whose only content is `{ $dynamicRef: '#foo' }` matches none of these and falls through to the UntypedNode fallback at `KiotaBuilder.cs:1977`.
- `Microsoft.OpenApi` 3.7.0 exposes `IOpenApiSchema.DynamicRef` / `IOpenApiSchema.DynamicAnchor`, and `OpenApiSchemaReference` delegates both from its `Target`. Phase 1 is unblocked on this front — Kiota just needs to read these properties. Phase 2 has a deeper upstream issue: the 3.1 deserializer short-circuits on `$ref` and drops sibling `$defs` / `$dynamicAnchor` declarations entirely (microsoft/OpenAPI.NET#2895). The binding information never reaches Kiota.
## What's already in place (this is smaller than it looks)
The CodeDOM already supports generics as type arguments:
- `CodeType.GenericTypeParameterValues` (`src/Kiota.Builder/CodeDOM/CodeType.cs:43-55`) is consumed by the C#, TypeScript, Go, Python, and Dart convention services for `` / `[T]` emission.
- `CommonLanguageRefiner.MoveRequestBuilderPropertiesToBaseType(..., addCurrentTypeAsGenericTypeParameter: true)` (`src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs:1446-1464`) already emits `class FooRequestBuilder : BaseRequestBuilder` for every RequestBuilder today — precedent for parameterized base-class emission.
- `RemoveRequestConfigurationClasses` + `GetGenericTypeForRequestConfiguration` is a second precedent.
The narrow gap for full generic support: `ProprietableBlockDeclaration` can carry generic arguments on `Inherits`/`Implements` but cannot declare its own unbound type parameters (`class PaginatedTemplate` where T is the class's parameter rather than a bound argument). Adding a `TypeParameters` collection resolves this.
## Reference implementation
Orval shipped this in May 2026 — PR https://github.com/orval-labs/orval/pull/3353. It emits `interface PaginatedTemplate` + `type PaginatedUserResponse = PaginatedTemplate`, preserving all 7 fixtures across all 4 OAS versions.
## Why this matters
- All Kiota-generated SDKs, including the Microsoft Graph SDK, lose type safety for any API that uses these patterns.
- OpenAPI 3.1.x adopts JSON Schema 2020-12 as its schema dialect; Kiota already claims 3.1 / 3.2 support.
- The pagination-DRY problem was raised internally in #3879 (closed) — `$dynamicRef` is the spec-blessed mechanism for it.
## Roadmap
- [x] **Phase 1: Recursive dynamic-scope resolution** — schemas with `$dynamicAnchor` + nested `$dynamicRef` (e.g. category trees). PR: #7817. Verified across C#, TypeScript, Go, Python, Java via integration tests.
- **Known limitation:** when a base type declaring `$dynamicAnchor` is materialized standalone before any derived type (e.g. it's the response of another endpoint), dynamic-ref-typed properties resolve against the base instead of the derived. The fallback is `UntypedNode` with a build-time warning. Addressed in Phase 3.
- [x] **Phase 2: Binding-aware `$dynamicRef` materialization** — resolve `$dynamicRef` bindings supplied by `$defs` / `$dynamicAnchor` at the usage site. PR: #7978 (merged).
Scope:
- Resolve named and inline binding contexts instead of falling back to `UntypedNode`.
- Support request bodies, responses, error responses, inherited/allOf templates, multi-anchor templates, and root arrays.
- Avoid reusing one generated template model across incompatible bindings.
- Keep generated names deterministic.
- Preserve correct request/response/error deserialization.
This phase preserves type safety by emitting concrete bound models where needed, such as `PaginatedTemplateUser` and `PaginatedTemplateGroup`. This is an interim implementation and fallback path. It does **not** emit reusable generic templates like `PaginatedTemplate`.
- [x] **Phase 3: Multi-derivation dynamic-ref resolution** — when multiple schemas declare the same `$dynamicAnchor` and no single active binding exists, emit a union/wrapper over viable candidates instead of resolving to whichever model materializes first. PR: #7978 (merged).
TypeScript/Python can use native unions; C#/Java/Go/PHP/Dart/Ruby can use the existing `IComposedTypeWrapper` pattern via `CodeUnionType` + `ConvertUnionTypesToWrapper`.
This phase is independent of reusable generic template emission.
- [ ] **Phase 4: Reusable generic template emission** — emit true reusable generic templates for languages that support them, e.g. `PaginatedTemplate` with bound operation types such as `PaginatedTemplate` / `PaginatedTemplate`.
This is separate from Phase 2 because it requires cross-language CodeDOM and writer work:
- Generic type parameter declaration support (`class Foo`, `type Foo[T ...] struct`, etc.).
- Marking `$dynamicAnchor` placeholders as type parameters.
- Generic template model declarations.
- Bound aliases/usages where appropriate.
- A per-language deserialization strategy, likely using generated bound factory helpers or wrappers.
- Concrete bound models as fallback for languages where generic model emission is not practical.
Suggested sub-phases:
- [x] **Phase 4a:** CodeDOM generic-parameter metadata (`CodeTypeParameter`, `TypeParameters`, `IsGeneric`). PR: #7978 (merged).
- [ ] **Phase 4b:** First language implementation: C# generic declaration, bound usages, and deserialization.
- [ ] **Phase 4c:** Go / Java / Dart / Python follow-ups.
- [ ] **Phase 4d:** Fallback and cleanup for non-generic languages.
## Willing to contribute
Phase 1 PR: #7817. Phase 3 to follow; Phase 2 follows once microsoft/OpenAPI.NET#2895 lands.
### Context
I'm doing this as part of my effort to expand dynamicRef across the ecosystem. Progress is tracked in https://github.com/aqeelat/openapi-dynamicref-adoption-tracker
---
*This issue was drafted with assistance from AI tooling. The submitter is responsible for reviewing and validating the contents before submission.*
Contributor guide
Research direction
Start with src/Kiota.Builder/CodeDOM/CodeType.cs and the ProprietableBlockDeclaration gap described in the issue, then inspect C# generation and deserialization paths. Use the linked OpenAPI 3.1 fixtures with `kiota generate` to reproduce current output. Done means C# emits reusable generic declarations, bound usages, and correct deserialization, with coverage for the relevant fixtures.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, dart, go, java, php, python, ruby, typescript
- Domain
- devtools
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100