diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs
index e1300935f..d09df2101 100644
--- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs
+++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs
@@ -455,7 +455,10 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
{
var nodeLocation = context.GetLocation();
var anchorName = JsonNodeHelper.ExtractDynamicAnchorName(dynamicPointer);
- var result = new OpenApiSchemaReference(!string.IsNullOrEmpty(anchorName) ? anchorName! : dynamicPointer, hostDocument);
+ string? externalResource = JsonNodeHelper.IsFragmentOnlyDynamicRef(dynamicPointer)
+ ? null
+ : JsonNodeHelper.ExtractDocumentUri(dynamicPointer);
+ var result = new OpenApiSchemaReference(!string.IsNullOrEmpty(anchorName) ? anchorName! : dynamicPointer, hostDocument, externalResource);
var referenceMetadata = new OpenApiSchema();
jsonObject.ParseMap(referenceMetadata, _openApiSchemaFixedFields, _openApiSchemaPatternFields, hostDocument, context,
static (schema, name, value) =>
diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs
index ff26d92c0..723562a74 100644
--- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs
+++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs
@@ -671,7 +671,8 @@ private void RegisterAnchor(OpenApiDocument document, string anchorName, IOpenAp
///
/// Resolves a $dynamicRef value against the workspace's anchor registries.
/// For fragment-only refs (#node), resolves against the host document.
- /// For URI refs (absolute), finds the external document and resolves there.
+ /// For URI refs (absolute or relative), finds the external document and resolves there.
+ /// Relative URIs are resolved against the host document's BaseUri.
///
internal IOpenApiSchema? ResolveDynamicRef(OpenApiDocument hostDocument, string dynamicRef)
{
@@ -682,7 +683,18 @@ private void RegisterAnchor(OpenApiDocument document, string anchorName, IOpenAp
if (!JsonNodeHelper.IsFragmentOnlyDynamicRef(dynamicRef))
{
var docUri = JsonNodeHelper.ExtractDocumentUri(dynamicRef);
- targetDoc = docUri is not null ? FindDocumentByBaseUri(docUri) : null;
+ if (docUri is not null)
+ {
+ if (!Uri.IsWellFormedUriString(docUri, UriKind.Absolute)
+ && hostDocument.BaseUri is not null
+ && Uri.TryCreate(hostDocument.BaseUri, docUri, out var resolved))
+ docUri = resolved.AbsoluteUri;
+ targetDoc = FindDocumentByBaseUri(docUri);
+ }
+ else
+ {
+ targetDoc = null;
+ }
}
if (targetDoc is null) return null;
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDynamicRefTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDynamicRefTests.cs
index 4b8f7580a..49921034c 100644
--- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDynamicRefTests.cs
+++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDynamicRefTests.cs
@@ -38,6 +38,7 @@ public void BareDynamicRefDeserializesAsSchemaReference()
var reference = Assert.IsType(result);
Assert.Equal("#category", reference.Reference.DynamicRef);
Assert.True(reference.Reference.IsDynamicRefOnly);
+ Assert.Null(reference.Reference.ExternalResource);
}
[Fact]
@@ -634,7 +635,7 @@ public async Task AbsoluteDynamicRefResolvesAcrossDocuments()
docA.BaseUri = new("https://example.com/main");
docB.BaseUri = new("https://example.com/external");
- var workspace = docA.Workspace ?? new OpenApiWorkspace();
+ var workspace = docA.Workspace!;
workspace.RegisterComponents(docA);
workspace.RegisterComponents(docB);
@@ -1493,7 +1494,7 @@ public async Task ExternalDynamicRefResolvesToPlainAnchorInExternalDocument()
docA.BaseUri = new("https://example.com/main");
docB.BaseUri = new("https://example.com/external");
- var workspace = docA.Workspace ?? new OpenApiWorkspace();
+ var workspace = docA.Workspace!;
workspace.RegisterComponents(docA);
workspace.RegisterComponents(docB);
@@ -1501,6 +1502,67 @@ public async Task ExternalDynamicRefResolvesToPlainAnchorInExternalDocument()
var next = tree.Properties["next"];
var reference = Assert.IsType(next);
Assert.True(reference.Reference.IsDynamicRefOnly);
+ Assert.Equal("https://example.com/external", reference.Reference.ExternalResource);
+ Assert.NotNull(reference.Target);
+ Assert.Same(docB.Components.Schemas["ExternalNode"], reference.Target);
+ }
+
+ [Fact]
+ public async Task ExternalDynamicRefWithRelativeUriResolvesAcrossDocuments()
+ {
+ var yamlA =
+ """
+ openapi: 3.1.0
+ info:
+ title: Doc A
+ version: 1.0.0
+ paths: {}
+ components:
+ schemas:
+ Tree:
+ type: object
+ properties:
+ next:
+ $dynamicRef: 'external.yaml#node'
+ """;
+
+ var yamlB =
+ """
+ openapi: 3.1.0
+ info:
+ title: Doc B
+ version: 1.0.0
+ paths: {}
+ components:
+ schemas:
+ ExternalNode:
+ $dynamicAnchor: node
+ type: object
+ properties:
+ value:
+ type: string
+ """;
+
+ using var streamA = new MemoryStream(Encoding.UTF8.GetBytes(yamlA));
+ using var streamB = new MemoryStream(Encoding.UTF8.GetBytes(yamlB));
+ var resultA = await OpenApiDocument.LoadAsync(streamA, "yaml", SettingsFixture.ReaderSettings);
+ var resultB = await OpenApiDocument.LoadAsync(streamB, "yaml", SettingsFixture.ReaderSettings);
+
+ var docA = resultA.Document;
+ var docB = resultB.Document;
+
+ docA.BaseUri = new("https://example.com/main.yaml");
+ docB.BaseUri = new("https://example.com/external.yaml");
+
+ var workspace = docA.Workspace!;
+ workspace.RegisterComponents(docA);
+ workspace.RegisterComponents(docB);
+
+ var tree = docA.Components.Schemas["Tree"];
+ var next = tree.Properties["next"];
+ var reference = Assert.IsType(next);
+ Assert.True(reference.Reference.IsDynamicRefOnly);
+ Assert.Equal("external.yaml", reference.Reference.ExternalResource);
Assert.NotNull(reference.Target);
Assert.Same(docB.Components.Schemas["ExternalNode"], reference.Target);
}
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithConstNull.json b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithConstNull.json
deleted file mode 100644
index 586d2f726..000000000
--- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithConstNull.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
- "properties": {
- "status": {
- "type": "string",
- "const": null
- },
- "user": {
- "type": "object",
- "properties": {
- "role": {
- "type": "string",
- "const": null
- }
- },
- "required": [ "role" ]
- }
- },
- "required": [ "status" ]
-}
-
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSecurityScheme/mutualTlsSecurityScheme.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSecurityScheme/mutualTlsSecurityScheme.yaml
deleted file mode 100644
index 72b4e9ae8..000000000
--- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSecurityScheme/mutualTlsSecurityScheme.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-type: mutualTLS
-description: Sample Description