Uh oh!
There was an error while loading. Please reload this page.
feat/external anchors and relative ids - #2918
Conversation
Code Coverage OverviewLanguages: C# C# / code-coverage/dotnetThe overall coverage in the Show a code coverage summary of the most impacted files.
Updated |
Vincent Biret (baywet)
commented
Jun 29, 2026
Copilot update the performance reports |
Done — benchmark reports updated in commit chore(benchmark): update performance reports. |
feat: adds support for relative ids Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
c62d21f to
6e9f76bCompareThere was a problem hiding this comment.
Pull request overview
This PR extends reference resolution for JSON Schema usage in OpenAPI 3.1/3.2 by supporting external plain-name fragment anchors (#anchor) and by registering $id-based schema identifiers for nested subschemas so they can be targeted by absolute/relative IDs.
Changes:
- Add reader tests for external
$anchorfragment resolution and nested$id-based subschema resolution (v3.1 + v3.2). - Register
$anchorand$ididentifiers for component schemas and their nested subschemas inOpenApiWorkspace. - Adjust schema reference URI construction to preserve external plain-name fragments (e.g.,
OAS-schemas.yaml#address) rather than forcing JSON Pointer fragments.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/Microsoft.OpenApi.Readers.Tests/V32Tests/RelativeReferenceTests.cs | Adds v3.2 regression tests for external $anchor refs and nested $id refs. |
| test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs | Adds v3.1 regression tests for external $anchor refs and nested $id refs. |
| src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs | Registers $anchor/$id schema identifiers for nested subschemas and resolves non-JSON-pointer fragments via the workspace registry. |
| src/Microsoft.OpenApi/Models/OpenApiDocument.cs | Preserves external plain-name fragments when constructing schema reference URIs for workspace lookup. |
| performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.EmptyModels-report.html | Updated benchmark artifact output. |
| performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.EmptyModels-report.csv | Updated benchmark artifact output. |
| performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.EmptyModels-report-github.md | Updated benchmark artifact output. |
| performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.json | Updated benchmark artifact output. |
| performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html | Updated benchmark artifact output. |
| performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv | Updated benchmark artifact output. |
| performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md | Updated benchmark artifact output. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Youssef Fahmy (Youssef1313)
left a comment
There was a problem hiding this comment.
I'm not familiar on everything but giving a sign-off (thought the approval won't be counting anyways as I'm not a maintainer in this repo)
Uh oh!
There was an error while loading. Please reload this page.
Addresses CodeQL feedback. Also resolves merge conflict with microsoft#2918 (RegisterSchemaIdentifiers + RegisterAnchors called together).
…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.
…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.
…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.
* feat(schema): resolve $dynamicRef against $dynamicAnchor via OpenApiSchemaReference 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 (#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 #2928). Cross-document resolution works for absolute URIs only. * fix(workspace): only consider authored $dynamicAnchor in ResolveDynamicAnchorInContext $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.
…osoft#2913) * feat(schema): resolve $dynamicRef against $dynamicAnchor via OpenApiSchemaReference 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. * fix(workspace): only consider authored $dynamicAnchor in ResolveDynamicAnchorInContext $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.



related MicrosoftDocs/openapi-docs#167 and #2913