Skip to content

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

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) 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<T> with PaginatedUserResponse = PaginatedTemplate<User> and PaginatedGroupResponse = PaginatedTemplate<Group>. 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:

Running any of these through kiota generate -d <fixture> -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 (Bug: OpenAPI 3.1 $ref siblings ($defs, $dynamicAnchor, $id) are silently dropped when parsing 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> / [T] emission.
  • CommonLanguageRefiner.MoveRequestBuilderPropertiesToBaseType(..., addCurrentTypeAsGenericTypeParameter: true) (src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs:1446-1464) already emits class FooRequestBuilder : BaseRequestBuilder<FooRequestBuilder> 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<T> 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 orval-labs/orval#3353. It emits interface PaginatedTemplate<itemType> + type PaginatedUserResponse = PaginatedTemplate<User>, preserving all 7 fixtures across all 4 OAS versions.

Why this matters

Roadmap

  • Phase 1: Recursive dynamic-scope resolution — schemas with $dynamicAnchor + nested $dynamicRef (e.g. category trees). PR: feat: resolve $dynamicRef against $dynamicAnchor for recursive types #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.
  • Phase 2: Binding-aware $dynamicRef materialization — resolve $dynamicRef bindings supplied by $defs / $dynamicAnchor at the usage site. PR: feat: resolve $dynamicRef generic bindings with per-context classes #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<TItem>.

  • 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: feat: resolve $dynamicRef generic bindings with per-context classes #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<TItem> with bound operation types such as PaginatedTemplate<User> / PaginatedTemplate<Group>.

    This is separate from Phase 2 because it requires cross-language CodeDOM and writer work:

    • Generic type parameter declaration support (class Foo<T>, 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:

    • Phase 4a: CodeDOM generic-parameter metadata (CodeTypeParameter, TypeParameters, IsGeneric). PR: feat: resolve $dynamicRef generic bindings with per-context classes #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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Needs Triage 🔍

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions