microsoft / microsoft/kiota

Feature: Support $dynamicRef / $dynamicAnchor (JSON Schema 2020-12) for OpenAPI 3.1+

Open
#7,815 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.