Skip to content

feat(schema): resolve bare $dynamicRef via $dynamicAnchor index#2913

Open
aqeelat wants to merge 1 commit into
microsoft:mainfrom
aqeelat:feat/dynamicref-resolution
Open

feat(schema): resolve bare $dynamicRef via $dynamicAnchor index#2913
aqeelat wants to merge 1 commit into
microsoft:mainfrom
aqeelat:feat/dynamicref-resolution

Conversation

@aqeelat

@aqeelat aqeelat commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Description

Implements document-scoped $dynamicRef resolution per JSON Schema 2020-12 §8.2.3.2. Bare $dynamicRef schemas (no $ref) now deserialize as OpenApiSchemaReference whose Target resolves via per-document $dynamicAnchor and $anchor registries in OpenApiWorkspace.

Resolution order in Target

  1. $dynamicAnchor index — single candidate resolves automatically
  2. $anchor fallback — when zero $dynamicAnchor candidates exist (per §8.2.3.2)
  3. null — when ambiguous (multiple candidates need dynamic-scope tracking)

Type of Change

  • New feature (non-breaking change which adds functionality)

Public APIs for consumers tracking dynamic scope

  • GetDynamicAnchorCandidates(doc, anchorName) — returns all candidate schemas
  • ResolveDynamicAnchorInContext(contextSchema, anchorName) — resolves against a specific schemas $defs

Behavior change: $ref siblings now take precedence

The $ref deserializer path now uses ApplySchemaMetadata (which populates JsonSchemaReference properties) instead of SetMetadataFromJsonObject. Structural siblings on $ref schemas (type, maxProperties, pattern, allOf, etc.) are now captured and take precedence over target values via the existing Reference.X ?? Target?.X property getters. Previously these siblings were stored but not surfaced. This affects all $ref schemas with siblings in 3.1+ documents, not just $dynamicRef.

Anchor index coverage

Registries are populated by recursively walking the entire document tree: component schemas, reusable component definitions (parameters, responses, request bodies, headers, callbacks, path items, media types), inline schemas (paths, operations, webhooks), and all nested subschema locations ($defs, properties, items, allOf, if/then/else, etc.).

Open question

When multiple schemas declare the same $dynamicAnchor, Target returns null (ambiguous). The spec requires the outermost candidate in the dynamic evaluation scope. Automatic resolution would require threading evaluation context through Target (e.g., AsyncLocal<Stack>). Currently consumers handle this via GetDynamicAnchorCandidates + ResolveDynamicAnchorInContext. Feedback welcome on whether the library should handle this automatically or leave it to consumers.

Testing

  • Unit tests added/updated
  • All existing tests pass (except 7 pre-existing Serialize_DoesNotMutateDom failures on clean main)

46 dynamic-ref tests across V31 and V32 covering: bare $dynamicRef deserialization, resolution to anchors in all schema locations, ambiguity handling, $anchor fallback, $dynamicAnchor precedence, round-trip serialization, sibling preservation, context-aware resolution, and $ref regression.

Microsoft.OpenApi.Tests: 1149 passed. Microsoft.OpenApi.Readers.Tests: 535 passed, 7 pre-existing failures.

Checklist

  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Versions applicability

  • My change applies to the version 3.X of the library, if so PR link: (this PR)

Related

@aqeelat aqeelat requested a review from a team as a code owner June 27, 2026 14:42
@aqeelat aqeelat force-pushed the feat/dynamicref-resolution branch 3 times, most recently from e4c3bd2 to 96e2bcc Compare June 28, 2026 12:23
@aqeelat aqeelat marked this pull request as draft June 29, 2026 00:22
…chemaReference

Implements document-scoped $dynamicRef resolution per JSON Schema
2020-12 §8.2.3.2. Bare $dynamicRef schemas (no $ref) now deserialize
as OpenApiSchemaReference whose Target resolves via per-document
$dynamicAnchor and $anchor registries in OpenApiWorkspace.

Resolution order in Target:
1. $dynamicAnchor index (single candidate → resolved automatically)
2. $anchor fallback when zero $dynamicAnchor candidates exist (per §8.2.3.2)
3. null when ambiguous (multiple candidates need dynamic-scope tracking)

Anchor registries are populated by recursively walking the entire
document tree: component schemas, reusable component definitions
(parameters, responses, request bodies, headers, callbacks, path items,
media types), inline schemas (paths, operations, webhooks), and all
nested subschema locations ($defs, properties, items, allOf, if/then/else,
etc.).

Public APIs for consumers tracking dynamic scope:
- GetDynamicAnchorCandidates(doc, anchorName): returns all candidate schemas
- ResolveDynamicAnchorInContext(contextSchema, anchorName): resolves against
  a specific schema's $defs for context-dependent resolution

Other changes:
- Deserializer (V31/V32): detect bare $dynamicRef, create
  OpenApiSchemaReference with IsDynamicRefOnly, parse siblings via
  ApplySchemaMetadata
- JsonSchemaReference: add IsDynamicRefOnly flag, implement
  IOpenApiSchemaMissingProperties, override SerializeAsV31/V32 for
  dynamic-only refs
- JsonNodeHelper: GetDynamicReferencePointer, ExtractDynamicAnchorName,
  IsFragmentOnlyDynamicRef
- Siblings preserved via ApplySchemaMetadata and surfaced through
  existing Reference-first property getters

Fixes microsoft#2911.
@aqeelat aqeelat force-pushed the feat/dynamicref-resolution branch from 96e2bcc to 573f8bf Compare June 29, 2026 09:20
@aqeelat aqeelat marked this pull request as ready for review June 29, 2026 09:38
@aqeelat

aqeelat commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

@baywet sorry for the mega PR. Most of the changes are tests and mechanical changes. However, if you want, I can split it into multiple PRs if that will make it easier for you to review.

@baywet

baywet commented Jun 29, 2026

Copy link
Copy Markdown
Member

Thanks for the contribution!

No worries, I'll take the time to review during the week.

Looking back at the reference mechanisms that are implemented throughout the library, I realized we had a specification/documentation page that never got published on release. Took the time to clean it up a little and it's now available here.

https://learn.microsoft.com/en-us/openapi/openapi.net/references-openapi

Let us know what you think!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants