feat(schema): resolve bare $dynamicRef via $dynamicAnchor index#2913
Conversation
e4c3bd2 to
96e2bcc
Compare
96e2bcc to
573f8bf
Compare
|
@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. |
|
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! |
There was a problem hiding this comment.
Pull request overview
This PR adds document-scoped $dynamicRef resolution for OpenAPI 3.1/3.2 JSON Schema 2020-12 by indexing $dynamicAnchor/$anchor declarations per OpenApiDocument in OpenApiWorkspace, and by deserializing bare $dynamicRef schemas as OpenApiSchemaReference so they participate in the existing reference-resolution pipeline.
Changes:
- Index
$dynamicAnchorand$anchordeclarations per document inOpenApiWorkspaceand expose APIs for candidate lookup + context-aware resolution. - Update V31/V32 schema deserializers to create an
OpenApiSchemaReferencefor bare$dynamicRef(no$ref) and preserve siblings viaApplySchemaMetadata. - Add extensive V31/V32 unit tests covering registration, resolution precedence, ambiguity behavior, serialization round-trips, and tricky schema-bearing locations (responses/headers/callback cycles/etc.).
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDynamicRefTests.cs | Adds OpenAPI 3.2 test coverage for bare $dynamicRef deserialization, resolution, anchor registration across OAS locations, and serialization behavior. |
| test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDynamicRefTests.cs | Adds OpenAPI 3.1 test coverage for anchor indexing across subschema locations, ambiguity behavior, context-aware resolution, and $ref regression scenarios. |
| src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs | Implements per-document $dynamicAnchor/$anchor registries and recursive anchor discovery across components + inline paths/webhooks/callback graphs. |
| src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs | Deserializes bare $dynamicRef schemas into OpenApiSchemaReference and applies schema metadata so siblings are preserved. |
| src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs | Same as V32, for OpenAPI 3.1 schema deserialization. |
| src/Microsoft.OpenApi/Reader/JsonNodeHelper.cs | Adds $dynamicRef pointer detection and helper methods for extracting anchor names + determining fragment-only refs. |
| src/Microsoft.OpenApi/PublicAPI.Unshipped.txt | Records new/changed public API surface for serialization overrides, Target override, and new workspace APIs. |
| src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs | Overrides Target to resolve bare $dynamicRef via workspace $dynamicAnchor index with $anchor fallback when appropriate. |
| src/Microsoft.OpenApi/Models/JsonSchemaReference.cs | Adds IsDynamicRefOnly and custom serialization behavior to emit $dynamicRef (without $ref) for dynamic-ref-only references, plus interface updates for anchor walking. |
baywet
left a comment
There was a problem hiding this comment.
Thanks for the contribution!
In addition to the comments I've left, I think we have good coverage of the deserialization scenarios, and some coverage of the serialization scenarios (at least for basic properties).
In think that one key aspect that is missing from the tests is: people using the object model to build documents. For "regular refs" components registration is required. I'd like to see more tests demonstrating that a document built from OM can resolve dynamic refs without requiring a re-parsing.
573f8bf to
06d3b50
Compare
|
@baywet CodeQL is suggesting we convert the |
Let's convert for consistency (we have patterns like those throughout the codebase). Allocation wise, the costliest part is the DOM itself and associated strings, the iterators are short lived and negligeable in comparison. And we've made significant performance improvements over the past year or so to have a better margin here. |
06d3b50 to
2ab57de
Compare
|
Done. Converted all All review comments should be addressed now. Do you think this can be merged today? |
|
@copilot fix failing test and address the other review items |
@aqueelat, I believe this doesn't work on forks. (I had the same problem with aspnetcore recently) |
2ab57de to
7a595ff
Compare
cc6b964 to
de6aa5f
Compare
|
@baywet Conflicts resolved — rebased on One observation from the merge: I left them as separate calls for now. Merging into one pass would require:
Happy to do that if you prefer a single walk, or leave as-is since the double walk is O(2n) and doesn't show up in benchmarks. |
de6aa5f to
7ebc8b5
Compare
|
@aqeelat thanks for handling the conflicts, and for pointing this out. Let's merge the crawling, it should reduce the amount of code we have and improve the performance. |
|
@baywet can we do it in a different PR? |
…ntifiers Combines the two separate schema tree walks (RegisterSchemaIdentifiers for $id/$anchor component registration, RegisterAnchors for $dynamicAnchor anchor registration) into a single walk. Per @baywet's request in microsoft#2913.
|
@baywet I forgot to mention that I already did the merged crawl in I have another PR to work on to address #2913 (comment). I'll stack it on top of this one. The kiota changes are ready locally but require we release a new version with this PR. |
00a4980 to
8d0accc
Compare
…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.). Anchor registration is merged into RegisterSchemaIdentifiers (microsoft#2918) so the schema tree is walked once instead of twice. IsDynamicRefOnly is computed from whether $dynamicRef is set and ReferenceV3 does not point to a component path — no field to forget when building from the object model. ResolveDynamicAnchorInContext special-cases OpenApiSchemaReference to read from Reference.DynamicAnchor/Definitions directly, never falling through to Target. Reference holder guards prevent the structural walk from crossing document boundaries through OpenApiParameterReference, OpenApiResponseReference, etc. Test snapshots use a custom JsonConverter<IOpenApiSchema> that routes schema serialization through the native OpenAPI writer, avoiding reflection cycles from recursive $dynamicRef. Public APIs for consumers tracking dynamic scope: - GetDynamicAnchorCandidates(doc, anchorName): returns all candidate schemas - ResolveDynamicAnchorInContext(contextSchema, anchorName): resolves against a specific entry-point schema's $defs without crossing reference boundaries Known limitation: relative URI resolution in $dynamicRef is not yet implemented (tracked as microsoft#2928). Cross-document resolution works for absolute URIs only.
8d0accc to
6e3e271
Compare
baywet
left a comment
There was a problem hiding this comment.
Thank you for making the changes!
…icAnchorInContext $defs entries that are themselves $refs no longer fall through to the referenced target's $dynamicAnchor. OpenApiSchemaReference.DynamicAnchor delegates to Target when the authored sibling is empty, which violated the method's context-bound intent. Adds AuthoredDynamicAnchor helper that reads Reference.DynamicAnchor for OpenApiSchemaReference and the plain property otherwise. Both $defs lookups (non-reference context branch and reference-holder branch) now use it. Tests cover both branches in V31 and V32: a Container schema with an aliased $ref def whose Target declares $dynamicAnchor: node, plus a reference holder whose Reference.Definitions carries the same aliased entry.
|
@baywet I just pushed the copilot review fixes. I also noticed there are a couple of tests not mirrored between V31 and V32. I will include them in the next PR. |
Pull Request
Description
Implements document-scoped
$dynamicRefresolution per JSON Schema 2020-12 §8.2.3.2. Bare$dynamicRefschemas (no$ref) now deserialize asOpenApiSchemaReferencewhoseTargetresolves via per-document$dynamicAnchorand$anchorregistries inOpenApiWorkspace.Resolution order in
Target$dynamicAnchorindex — single candidate resolves automatically$anchorfallback — when zero$dynamicAnchorcandidates exist (per §8.2.3.2)null— when ambiguous (multiple candidates need dynamic-scope tracking)Type of Change
Public APIs for consumers tracking dynamic scope
GetDynamicAnchorCandidates(doc, anchorName)— returns all candidate schemasResolveDynamicAnchorInContext(contextSchema, anchorName)— resolves against a specific schema's$defs$refsibling behaviorThis PR relies on the existing OpenAPI.NET 3.1/3.2 behavior where
$refschema siblings are preserved onJsonSchemaReferenceand surfaced throughOpenApiSchemaReference's existing reference-first property getters.This PR does not introduce that behavior. It extends the same storage model to bare
$dynamicRefschemas so sibling keywords are preserved while the schema participates in reference resolution.See #2919 for a broader discussion on whether the reference-first getter model should be revised for assertion keywords.
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,Targetreturnsnull(ambiguous). The spec requires the outermost candidate in the dynamic evaluation scope. Automatic resolution would require threading evaluation context throughTarget(e.g.,AsyncLocal<Stack>). Currently consumers handle this viaGetDynamicAnchorCandidates+ResolveDynamicAnchorInContext. Feedback welcome on whether the library should handle this automatically or leave it to consumers.Testing
Serialize_DoesNotMutateDomfailures on clean main)46 dynamic-ref tests across V31 and V32 covering: bare
$dynamicRefdeserialization, resolution to anchors in all schema locations, ambiguity handling,$anchorfallback,$dynamicAnchorprecedence, round-trip serialization, sibling preservation, context-aware resolution, OM-built document, and$refregression.Microsoft.OpenApi.Tests: 1149 passed.Microsoft.OpenApi.Readers.Tests: 535 passed, 7 pre-existing failures.Checklist
Versions applicability
Related
$refschemas)