Skip to content

Feature: Resolve $dynamicRef against $dynamicAnchor in the schema reference resolution pipeline #2911

Description

Background

JSON Schema 2020-12 defines $dynamicRef / $dynamicAnchor for recursive and extensible schema resources. This enables patterns where a base schema can recursively refer to the active derived schema:

components:
  schemas:
    BaseCategory:
      $dynamicAnchor: category
      type: object
      properties:
        children:
          type: array
          items:
            $dynamicRef: '#category'
    LocalizedCategory:
      $dynamicAnchor: category
      allOf:
        - $ref: '#/components/schemas/BaseCategory'

When LocalizedCategory is the active schema, children.items should resolve to LocalizedCategory, not BaseCategory.

Current State

After #2896, JSON Schema 2020-12 keywords such as $dynamicRef, $dynamicAnchor, and $defs are parsed, preserved, and serialized, including as siblings on $ref schemas.

However, $dynamicRef is not currently part of schema reference resolution:

  • BaseOpenApiReferenceHolder.Target resolves $ref through OpenApiDocument.ResolveReference.
  • OpenApiWorkspace indexes schemas by component path and $id, but not by $dynamicAnchor.
  • JsonNodeHelper.GetReferencePointer only checks $ref.
  • A schema with $dynamicRef but no $ref is deserialized as a plain OpenApiSchema with DynamicRef set, so it does not enter the OpenApiSchemaReference.Target resolution path.

Design Questions

There are two separate decisions to make.

1. How should bare $dynamicRef schemas participate in resolution?

Options:

  1. Treat $dynamicRef similarly to $ref during schema loading, producing a schema reference type whose target resolution uses $dynamicAnchor.
  2. Keep $dynamicRef as metadata on OpenApiSchema, but add a separate dynamic-ref resolution path outside Target.

Option 1 integrates with the existing reference model but requires Target to resolve context-dependently. Option 2 keeps $dynamicRef resolution explicit and doesn't conflate it with static $ref resolution.

2. What scope should resolution support?

A document-level $dynamicAnchor lookup handles the simple case where only one schema declares an anchor.

Full JSON Schema 2020-12 behavior requires dynamic-scope resolution: when multiple resources declare the same $dynamicAnchor, the outermost active schema resource wins. That requires context that the current Target / Workspace.ResolveReference pipeline does not carry today.

Aspect Document-scoped lookup Dynamic-scope resolution
Simple recursion (single anchor) Works Works
Generic types (multiple anchors, same name) Fails — no context Works
Implementation size Small (index + lookup) Large (scope plumbing + walker changes)
Risk to existing Target semantics Low (additive) Medium-high (new resolution path)
Precedent in repo $id registration pattern LoopDetector, ResolveRecursiveTarget
Order-dependency None Inherent (documented in kiota)
Solves the pure-$dynamicRef blocker? Needs deserializer change Needs deserializer change

Related

Activity

  1. aqeelat commented on Jun 28, 2026

    @aqeelat
    ContributorAuthor

    Update: I opened #2913 implementing the first phase — document-scoped $dynamicRef resolution via OpenApiSchemaReference.

    Bare $dynamicRef schemas (no $ref) now deserialize as OpenApiSchemaReference whose Target resolves through a per-document $dynamicAnchor index in OpenApiWorkspace. Siblings are preserved via ApplySchemaMetadata and surfaced through the existing Reference.X ?? Target?.X property getters.

    The remaining design questions from this issue (full dynamic-scope resolution, inline schema anchor registration) are tracked as future work.

  2. added a commit that references this issue on Jun 29, 2026
    573f8bf
  3. aqeelat commented on Jun 29, 2026

    @aqeelat
    ContributorAuthor

    Updated: #2913 now implements the complete design.

    What is delivered:

    • Bare $dynamicRef → OpenApiSchemaReference with Target resolution
    • Per-document $dynamicAnchor and $anchor registries, populated from the entire document tree (components, inline schemas, all nested subschema locations)
    • $anchor fallback when zero $dynamicAnchor candidates exist (per §8.2.3.2)
    • Two public APIs for multi-candidate resolution: GetDynamicAnchorCandidates and ResolveDynamicAnchorInContext
    • Siblings preserved via ApplySchemaMetadata and surfaced through existing Reference.X ?? Target?.X getters

    Known limitation: when multiple schemas declare the same $dynamicAnchor, Target returns null because the library does not track evaluation context. The spec requires the outermost candidate in the dynamic scope — a deterministic answer, but one that needs to know which schema is the active entry point. Consumers provide this via ResolveDynamicAnchorInContext(entryPointSchema, anchorName). Whether the library should handle this automatically (e.g., AsyncLocal<Stack>) is an open question raised in the PR.

  4. added 3 commits that reference this issue on Jun 30, 2026
    06d3b50
    2ab57de
    7a595ff
  5. aqeelat commented on Jul 3, 2026

    @aqeelat
    ContributorAuthor

    Tracked as sub-task: #2928 (relative URI resolution in $dynamicRef)

  6. added 4 commits that reference this issue on Jul 3, 2026
    702e385
    b976475
    5ea36ad
    4922341
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions