Skip to content

Commit 821053b

Browse files
authored
feat: support relative URI resolution in $dynamicRef (#2928) (#2945)
* feat: support relative URI resolution in $dynamicRef (#2928) Set ExternalResource on non-fragment $dynamicRef references so the workspace loader can discover and fetch external documents. Resolve relative URIs against hostDocument.BaseUri in Target before calling FindDocumentByBaseUri. Tests: - Fragment-only: ExternalResource is null - Absolute URI: ExternalResource is set, resolves across documents - Relative URI: ExternalResource is set, resolves against BaseUri across documents - V31 and V32 coverage * test: mirror V31 dynamic ref tests to V32 and fix workspace fallback Adds 17 missing V32 test mirrors for full parity with V31 coverage. Replaces dead '?? new OpenApiWorkspace()' fallback with null-forgiving '!' in both V31 and V32 cross-document tests. * fix: remove duplicate XML doc and use Uri.TryCreate for relative URI resolution Addresses Copilot review feedback on #2945.
1 parent 9fc177b commit 821053b

5 files changed

Lines changed: 947 additions & 6 deletions

File tree

src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,10 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
455455
{
456456
var nodeLocation = context.GetLocation();
457457
var anchorName = JsonNodeHelper.ExtractDynamicAnchorName(dynamicPointer);
458-
var result = new OpenApiSchemaReference(!string.IsNullOrEmpty(anchorName) ? anchorName! : dynamicPointer, hostDocument);
458+
string? externalResource = JsonNodeHelper.IsFragmentOnlyDynamicRef(dynamicPointer)
459+
? null
460+
: JsonNodeHelper.ExtractDocumentUri(dynamicPointer);
461+
var result = new OpenApiSchemaReference(!string.IsNullOrEmpty(anchorName) ? anchorName! : dynamicPointer, hostDocument, externalResource);
459462
var referenceMetadata = new OpenApiSchema();
460463
jsonObject.ParseMap(referenceMetadata, _openApiSchemaFixedFields, _openApiSchemaPatternFields, hostDocument, context,
461464
static (schema, name, value) =>

src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,10 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
455455
{
456456
var nodeLocation = context.GetLocation();
457457
var anchorName = JsonNodeHelper.ExtractDynamicAnchorName(dynamicPointer);
458-
var result = new OpenApiSchemaReference(!string.IsNullOrEmpty(anchorName) ? anchorName! : dynamicPointer, hostDocument);
458+
string? externalResource = JsonNodeHelper.IsFragmentOnlyDynamicRef(dynamicPointer)
459+
? null
460+
: JsonNodeHelper.ExtractDocumentUri(dynamicPointer);
461+
var result = new OpenApiSchemaReference(!string.IsNullOrEmpty(anchorName) ? anchorName! : dynamicPointer, hostDocument, externalResource);
459462
var referenceMetadata = new OpenApiSchema();
460463
jsonObject.ParseMap(referenceMetadata, _openApiSchemaFixedFields, _openApiSchemaPatternFields, hostDocument, context,
461464
static (schema, name, value) =>

src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,8 @@ private void RegisterAnchor(OpenApiDocument document, string anchorName, IOpenAp
687687
/// <summary>
688688
/// Resolves a $dynamicRef value against the workspace's anchor registries.
689689
/// For fragment-only refs (#node), resolves against the host document.
690-
/// For URI refs (absolute), finds the external document and resolves there.
690+
/// For URI refs (absolute or relative), finds the external document and resolves there.
691+
/// Relative URIs are resolved against the host document's BaseUri.
691692
/// </summary>
692693
internal IOpenApiSchema? ResolveDynamicRef(OpenApiDocument hostDocument, string dynamicRef)
693694
{
@@ -698,7 +699,18 @@ private void RegisterAnchor(OpenApiDocument document, string anchorName, IOpenAp
698699
if (!JsonNodeHelper.IsFragmentOnlyDynamicRef(dynamicRef))
699700
{
700701
var docUri = JsonNodeHelper.ExtractDocumentUri(dynamicRef);
701-
targetDoc = docUri is not null ? FindDocumentByBaseUri(docUri) : null;
702+
if (docUri is not null)
703+
{
704+
if (!Uri.IsWellFormedUriString(docUri, UriKind.Absolute)
705+
&& hostDocument.BaseUri is not null
706+
&& Uri.TryCreate(hostDocument.BaseUri, docUri, out var resolved))
707+
docUri = resolved.AbsoluteUri;
708+
targetDoc = FindDocumentByBaseUri(docUri);
709+
}
710+
else
711+
{
712+
targetDoc = null;
713+
}
702714
}
703715

704716
if (targetDoc is null) return null;

test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDynamicRefTests.cs

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public void BareDynamicRefDeserializesAsSchemaReference()
3838
var reference = Assert.IsType<OpenApiSchemaReference>(result);
3939
Assert.Equal("#category", reference.Reference.DynamicRef);
4040
Assert.True(reference.Reference.IsDynamicRefOnly);
41+
Assert.Null(reference.Reference.ExternalResource);
4142
}
4243

4344
[Fact]
@@ -634,7 +635,7 @@ public async Task AbsoluteDynamicRefResolvesAcrossDocuments()
634635
docA.BaseUri = new("https://example.com/main");
635636
docB.BaseUri = new("https://example.com/external");
636637

637-
var workspace = docA.Workspace ?? new OpenApiWorkspace();
638+
var workspace = docA.Workspace!;
638639
workspace.RegisterComponents(docA);
639640
workspace.RegisterComponents(docB);
640641

@@ -1493,14 +1494,75 @@ public async Task ExternalDynamicRefResolvesToPlainAnchorInExternalDocument()
14931494
docA.BaseUri = new("https://example.com/main");
14941495
docB.BaseUri = new("https://example.com/external");
14951496

1496-
var workspace = docA.Workspace ?? new OpenApiWorkspace();
1497+
var workspace = docA.Workspace!;
14971498
workspace.RegisterComponents(docA);
14981499
workspace.RegisterComponents(docB);
14991500

15001501
var tree = docA.Components.Schemas["Tree"];
15011502
var next = tree.Properties["next"];
15021503
var reference = Assert.IsType<OpenApiSchemaReference>(next);
15031504
Assert.True(reference.Reference.IsDynamicRefOnly);
1505+
Assert.Equal("https://example.com/external", reference.Reference.ExternalResource);
1506+
Assert.NotNull(reference.Target);
1507+
Assert.Same(docB.Components.Schemas["ExternalNode"], reference.Target);
1508+
}
1509+
1510+
[Fact]
1511+
public async Task ExternalDynamicRefWithRelativeUriResolvesAcrossDocuments()
1512+
{
1513+
var yamlA =
1514+
"""
1515+
openapi: 3.1.0
1516+
info:
1517+
title: Doc A
1518+
version: 1.0.0
1519+
paths: {}
1520+
components:
1521+
schemas:
1522+
Tree:
1523+
type: object
1524+
properties:
1525+
next:
1526+
$dynamicRef: 'external.yaml#node'
1527+
""";
1528+
1529+
var yamlB =
1530+
"""
1531+
openapi: 3.1.0
1532+
info:
1533+
title: Doc B
1534+
version: 1.0.0
1535+
paths: {}
1536+
components:
1537+
schemas:
1538+
ExternalNode:
1539+
$dynamicAnchor: node
1540+
type: object
1541+
properties:
1542+
value:
1543+
type: string
1544+
""";
1545+
1546+
using var streamA = new MemoryStream(Encoding.UTF8.GetBytes(yamlA));
1547+
using var streamB = new MemoryStream(Encoding.UTF8.GetBytes(yamlB));
1548+
var resultA = await OpenApiDocument.LoadAsync(streamA, "yaml", SettingsFixture.ReaderSettings);
1549+
var resultB = await OpenApiDocument.LoadAsync(streamB, "yaml", SettingsFixture.ReaderSettings);
1550+
1551+
var docA = resultA.Document;
1552+
var docB = resultB.Document;
1553+
1554+
docA.BaseUri = new("https://example.com/main.yaml");
1555+
docB.BaseUri = new("https://example.com/external.yaml");
1556+
1557+
var workspace = docA.Workspace!;
1558+
workspace.RegisterComponents(docA);
1559+
workspace.RegisterComponents(docB);
1560+
1561+
var tree = docA.Components.Schemas["Tree"];
1562+
var next = tree.Properties["next"];
1563+
var reference = Assert.IsType<OpenApiSchemaReference>(next);
1564+
Assert.True(reference.Reference.IsDynamicRefOnly);
1565+
Assert.Equal("external.yaml", reference.Reference.ExternalResource);
15041566
Assert.NotNull(reference.Target);
15051567
Assert.Same(docB.Components.Schemas["ExternalNode"], reference.Target);
15061568
}

0 commit comments

Comments
 (0)