From a13fbc2f0100177f9f0c7c40aaccd00a269a29b3 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Fri, 30 Oct 2026 00:59:11 +0530 Subject: [PATCH 1/4] fix(v31): recognize external resource in schema ref without fragment (#3148) Fix GetReferenceIdAndExternalResource in OpenApiV31Deserializer and OpenApiV32Deserializer so that references to external documents without a fragment (e.g., schemas/uuid.schema.json or an absolute URL) correctly populate ExternalResource and mark IsExternal = true. Signed-off-by: Hardik Kaurani --- .../Reader/V31/OpenApiV31Deserializer.cs | 2 +- .../Reader/V32/OpenApiV32Deserializer.cs | 2 +- ...OpenApiSchemaReferenceDeserializerTests.cs | 26 +++++++++++++++++++ ...OpenApiSchemaReferenceDeserializerTests.cs | 26 +++++++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs index 617ee9ced..76e1de571 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs @@ -165,7 +165,7 @@ private static (string, string?) GetReferenceIdAndExternalResource(string pointe var isExternalResource = !refSegments[0].StartsWith("#", StringComparison.OrdinalIgnoreCase); string? externalResource = null; - if (isExternalResource && pointer.Contains('#')) + if (isExternalResource) { externalResource = pointer.Split('#')[0].TrimEnd('#'); } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs index 5efed4657..bf43941f2 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs @@ -165,7 +165,7 @@ private static (string, string?) GetReferenceIdAndExternalResource(string pointe var isExternalResource = !refSegments[0].StartsWith("#", StringComparison.OrdinalIgnoreCase); string? externalResource = null; - if (isExternalResource && pointer.Contains('#')) + if (isExternalResource) { externalResource = pointer.Split('#')[0].TrimEnd('#'); } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaReferenceDeserializerTests.cs index 782ae6e45..5b05cc32a 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaReferenceDeserializerTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaReferenceDeserializerTests.cs @@ -57,4 +57,30 @@ public void ShouldDeserializeSchemaReferenceAnnotations() Assert.Equal("example reference value", resultReference.Examples[0]?.ToString()); Assert.NotNull(resultReference.Target); } + + [Theory] + [InlineData("schemas/uuid.schema.json", "schemas/uuid.schema.json", "schemas/uuid.schema.json")] + [InlineData("./schemas/uuid.schema.json", "./schemas/uuid.schema.json", "./schemas/uuid.schema.json")] + [InlineData("uuid.schema.json", "uuid.schema.json", "uuid.schema.json")] + [InlineData("https://example.com/schemas/resource.json", "https://example.com/schemas/resource.json", "https://example.com/schemas/resource.json")] + public void ShouldDeserializeExternalSchemaReferenceWithoutFragment(string refPointer, string expectedId, string expectedExternalResource) + { + var json = $$""" + { + "$ref": "{{refPointer}}" + } + """; + + var hostDocument = new OpenApiDocument(); + var jsonNode = JsonNode.Parse(json); + + var result = OpenApiV31Deserializer.LoadSchema(jsonNode, hostDocument, new ParsingContext(new())); + + Assert.NotNull(result); + var resultReference = Assert.IsType(result); + + Assert.True(resultReference.Reference.IsExternal); + Assert.Equal(expectedId, resultReference.Reference.Id); + Assert.Equal(expectedExternalResource, resultReference.Reference.ExternalResource); + } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaReferenceDeserializerTests.cs index 100e72b4e..fe7b4d5e8 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaReferenceDeserializerTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaReferenceDeserializerTests.cs @@ -57,5 +57,31 @@ public void ShouldDeserializeSchemaReferenceAnnotations() Assert.Equal("example reference value", resultReference.Examples[0]?.ToString()); Assert.NotNull(resultReference.Target); } + + [Theory] + [InlineData("schemas/uuid.schema.json", "schemas/uuid.schema.json", "schemas/uuid.schema.json")] + [InlineData("./schemas/uuid.schema.json", "./schemas/uuid.schema.json", "./schemas/uuid.schema.json")] + [InlineData("uuid.schema.json", "uuid.schema.json", "uuid.schema.json")] + [InlineData("https://example.com/schemas/resource.json", "https://example.com/schemas/resource.json", "https://example.com/schemas/resource.json")] + public void ShouldDeserializeExternalSchemaReferenceWithoutFragment(string refPointer, string expectedId, string expectedExternalResource) + { + var json = $$""" + { + "$ref": "{{refPointer}}" + } + """; + + var hostDocument = new OpenApiDocument(); + var jsonNode = JsonNode.Parse(json); + + var result = OpenApiV32Deserializer.LoadSchema(jsonNode, hostDocument, new ParsingContext(new())); + + Assert.NotNull(result); + var resultReference = Assert.IsType(result); + + Assert.True(resultReference.Reference.IsExternal); + Assert.Equal(expectedId, resultReference.Reference.Id); + Assert.Equal(expectedExternalResource, resultReference.Reference.ExternalResource); + } } From 8003bbf4ece8e89c6bc44369cb0fb783bf6c3464 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Fri, 30 Oct 2026 01:01:56 +0530 Subject: [PATCH 2/4] fix(models): resolve relative reference URIs safely against BaseUri (#3148) Update OpenApiDocument.ResolveReference and OpenApiWorkspace.ToLocationUrl to resolve relative references safely against BaseUri or external document URIs using Uri.TryCreate, preventing unhandled UriFormatException and ArgumentNullException when resolving relative references without fragments. Signed-off-by: Hardik Kaurani --- .../Models/OpenApiDocument.cs | 31 +++++++++----- .../Services/OpenApiWorkspace.cs | 10 ++++- .../V31Tests/RelativeReferenceTests.cs | 34 ++++++++++++++++ .../Models/OpenApiDocumentTests.cs | 40 +++++++++++++++++++ .../Workspaces/OpenApiWorkspaceTests.cs | 15 +++++++ 5 files changed, 117 insertions(+), 13 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index 03086600d..7203ca414 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -630,7 +630,7 @@ private static string ConvertByteArrayToString(byte[] hash) string uriLocation; var id = reference.Id; - if (!string.IsNullOrEmpty(id) && id!.Contains('/')) // this means its a URL reference + if (!string.IsNullOrEmpty(id) && (id!.Contains('/') || (reference.IsExternal && reference.ExternalResource == id))) // this means its a URL or standalone external reference { uriLocation = id!; } @@ -668,17 +668,26 @@ private static string ConvertByteArrayToString(byte[] hash) : BaseUri + relativePath; } - var absoluteUri = -#if NETSTANDARD2_1 || NETCOREAPP || NET5_0_OR_GREATER - uriLocation.StartsWith('#') -#else - uriLocation.StartsWith("#", StringComparison.OrdinalIgnoreCase) -#endif - switch + Uri? resolvedUri = null; + if (Uri.TryCreate(uriLocation, UriKind.Absolute, out var absUri)) { - true => new Uri(BaseUri, uriLocation).AbsoluteUri, - false => new Uri(uriLocation).AbsoluteUri - }; + resolvedUri = absUri; + } + else if (useExternal && reference.ExternalResource is not null && Workspace?.GetDocumentId(reference.ExternalResource) is Uri extDocUri) + { + resolvedUri = extDocUri; + } + else if (BaseUri is not null && Uri.TryCreate(BaseUri, uriLocation, out var relCombinedUri)) + { + resolvedUri = relCombinedUri; + } + + if (resolvedUri is null) + { + return null; + } + + var absoluteUri = resolvedUri.AbsoluteUri; if (reference.Type is ReferenceType.Schema && absoluteUri.Contains('#') && parentSchema is not null) { diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 8872a8983..c9708339e 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -949,10 +949,16 @@ public bool Contains(string location) private Uri? ToLocationUrl(string location) { - if (BaseUrl is not null) + if (Uri.TryCreate(location, UriKind.Absolute, out var absoluteUri)) { - return new(BaseUrl, location); + return absoluteUri; } + + if (BaseUrl is not null && Uri.TryCreate(BaseUrl, location, out var relativeUri)) + { + return relativeUri; + } + return null; } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs index d829c4d5a..2bed9c518 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs @@ -747,5 +747,39 @@ public void ExitsEarlyOnCyclicalReferences() Assert.Equal("#/components/schemas/Category", parentSchemaRef.Reference.ReferenceV3); Assert.NotNull(parentSchemaRef.Target); } + + [Fact] + public void ParseDocumentWithRelativeReferenceWithoutFragmentDoesNotThrowUriFormatException() + { + var openApi = """ + { + "openapi": "3.1.1", + "info": { "title": "Repro", "version": "1" }, + "paths": {}, + "components": { + "schemas": { + "Request": { + "type": "object", + "properties": { "attemptId": { "$ref": "schemas/uuid.schema.json" } } + } + } + } + } + """; + + var reader = new OpenApiStringReader(); + var doc = reader.Read(openApi, out var diagnostic); + + Assert.NotNull(doc); + Assert.Empty(diagnostic.Errors); + + var requestSchema = Assert.IsType(doc.Components.Schemas["Request"]); + var attemptIdRef = Assert.IsType(requestSchema.Properties["attemptId"]); + Assert.True(attemptIdRef.Reference.IsExternal); + Assert.Equal("schemas/uuid.schema.json", attemptIdRef.Reference.ExternalResource); + Assert.Equal("schemas/uuid.schema.json", attemptIdRef.Reference.Id); + + Assert.Null(attemptIdRef.Target); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs index 8b4629664..b9e066c60 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs @@ -2697,5 +2697,45 @@ public void GetOperationById_ThrowsOnNullOrEmptyId() Assert.Throws(() => doc.GetOperationById(null!)); Assert.Throws(() => doc.GetOperationById(string.Empty)); } + + [Theory] + [InlineData("schemas/uuid.schema.json")] + [InlineData("./schemas/uuid.schema.json")] + [InlineData("uuid.schema.json")] + public void ResolveReference_RelativeReferenceWithoutFragmentDoesNotThrowUriFormatException(string referenceId) + { + var doc = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test", Version = "1.0" }, + BaseUri = new Uri("https://example.com/api/openapi.json"), + }; + + var schemaRef = new OpenApiSchemaReference(referenceId, doc, referenceId); + + var result = doc.ResolveReferenceTo(schemaRef.Reference, null); + Assert.Null(result); + Assert.Null(schemaRef.Target); + } + + [Fact] + public void ResolveReference_RelativeReferenceWithoutFragmentResolvesAgainstWorkspace() + { + var workspace = new OpenApiWorkspace(new Uri("https://example.com/api/")); + var targetSchema = new OpenApiSchema { Type = JsonSchemaType.String, Description = "UUID schema" }; + workspace.RegisterComponent("https://example.com/api/schemas/uuid.schema.json", targetSchema); + + var doc = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test", Version = "1.0" }, + BaseUri = new Uri("https://example.com/api/openapi.json"), + Workspace = workspace, + }; + + var schemaRef = new OpenApiSchemaReference("schemas/uuid.schema.json", doc, "schemas/uuid.schema.json"); + + var result = doc.ResolveReferenceTo(schemaRef.Reference, null); + Assert.Same(targetSchema, result); + Assert.Same(targetSchema, schemaRef.Target); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs index 8bb018e9f..8f97f2c5a 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs @@ -172,6 +172,21 @@ private static OpenApiDocument CreateCommonDocument() return doc; } + + [Fact] + public void OpenApiWorkspacesCanResolveAbsoluteUriReferences() + { + var workspace = new OpenApiWorkspace(); + var targetSchema = new OpenApiSchema + { + Type = JsonSchemaType.String, + Description = "Schema at absolute URI" + }; + workspace.RegisterComponent("https://example.com/schemas/uuid.schema.json", targetSchema); + + var resolved = workspace.ResolveReference("https://example.com/schemas/uuid.schema.json"); + Assert.Same(targetSchema, resolved); + } } public static class OpenApiFactoryExtensions From d0eeafadf5b15cc873e09410955f223fe13a9f18 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Sat, 31 Oct 2026 01:46:55 +0530 Subject: [PATCH 3/4] fix(models): resolve review findings for external refs, discriminator mappings, and workspace URLs (#3148) - Preserve component fragments in OpenApiDocument.ResolveReference when external resource and component names collide (e.g. common.json#/components/schemas/common.json). - Preserve authored fragmentless relative pointers in BaseOpenApiReference.SetJsonPointerPath and GetExternalReferenceV3 to maintain exact round trips during serialization. - Preserve bare schema names in OpenApiDiscriminatorDeserializer.LoadMapping so local schemas in discriminator mapping and defaultMapping resolve properly. - Resolve root-relative locations against BaseUrl in OpenApiWorkspace.ToLocationUrl before fallback to absolute URI parsing. - Catch loader errors gracefully in OpenApiWorkspaceLoader to report diagnostics without aborting document resolution. - Add regression unit tests for colliding external references, discriminator bare schema mappings, workspace root-relative URI resolution, and round-trip serialization. Signed-off-by: Hardik Kaurani --- .../Models/BaseOpenApiReference.cs | 10 +++- .../Models/OpenApiDocument.cs | 15 +++-- .../Reader/Services/OpenApiWorkspaceLoader.cs | 27 +++++---- .../V31/OpenApiDiscriminatorDeserializer.cs | 22 +++++++- .../V32/OpenApiDiscriminatorDeserializer.cs | 22 +++++++- .../Services/OpenApiWorkspace.cs | 10 ++-- .../V31Tests/OpenApiComponentsTests.cs | 4 +- .../V31Tests/OpenApiDiscriminatorTests.cs | 56 ++++++++++++++++++- ...OpenApiSchemaReferenceDeserializerTests.cs | 1 + .../V31Tests/RelativeReferenceTests.cs | 13 ++++- .../V32Tests/OpenApiComponentsTests.cs | 2 +- .../V32Tests/OpenApiDiscriminatorTests.cs | 56 ++++++++++++++++++- ...OpenApiSchemaReferenceDeserializerTests.cs | 1 + .../Models/OpenApiDocumentTests.cs | 25 ++++++++- .../Workspaces/OpenApiWorkspaceTests.cs | 17 +++++- 15 files changed, 247 insertions(+), 34 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs b/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs index d32080890..ba1e7135e 100644 --- a/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs +++ b/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs @@ -254,6 +254,11 @@ public virtual void SerializeAsV2(IOpenApiWriter writer) return Id; } + if (string.Equals(ExternalResource, Id, StringComparison.Ordinal) && !Id.Contains("#")) + { + return ExternalResource; + } + return ExternalResource + "#/components/" + Type.GetDisplayName() + "/"+ Id; } @@ -329,9 +334,8 @@ internal void SetJsonPointerPath(string pointer, string nodeLocation) ReferenceV3 = ResolveRelativePointer(nodeLocation, pointer); } - // Absolute reference or anchor (e.g. "#/components/schemas/..." or full URL) - else if ((pointer.Contains('#') || pointer.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - && !string.Equals(ReferenceV3, pointer, StringComparison.OrdinalIgnoreCase)) + // Absolute reference, anchor, or relative pointer (e.g. "#/components/schemas/...", full URL, or "schemas/uuid.schema.json") + else if (!string.Equals(ReferenceV3, pointer, StringComparison.OrdinalIgnoreCase)) { ReferenceV3 = pointer; } diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index 7203ca414..a0213e25b 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -630,14 +630,21 @@ private static string ConvertByteArrayToString(byte[] hash) string uriLocation; var id = reference.Id; - if (!string.IsNullOrEmpty(id) && (id!.Contains('/') || (reference.IsExternal && reference.ExternalResource == id))) // this means its a URL or standalone external reference + var referenceV3 = !string.IsNullOrEmpty(reference.ReferenceV3) ? reference.ReferenceV3! : string.Empty; + var isStandaloneExternal = reference.IsExternal && + (!string.IsNullOrEmpty(referenceV3) ? !referenceV3.Contains("#") : (id is not null && !id.Contains("#"))); + + if (isStandaloneExternal) + { + uriLocation = id ?? reference.ExternalResource ?? string.Empty; + } + else if (!string.IsNullOrEmpty(id) && id!.Contains("/")) { uriLocation = id!; } else { string relativePath; - var referenceV3 = !string.IsNullOrEmpty(reference.ReferenceV3) ? reference.ReferenceV3! : string.Empty; if (reference.Type is ReferenceType.Schema && useExternal && TryGetPlainNameFragment(referenceV3, out var plainNameFragment)) { @@ -689,7 +696,7 @@ private static string ConvertByteArrayToString(byte[] hash) var absoluteUri = resolvedUri.AbsoluteUri; - if (reference.Type is ReferenceType.Schema && absoluteUri.Contains('#') && parentSchema is not null) + if (reference.Type is ReferenceType.Schema && absoluteUri.Contains("#") && parentSchema is not null) { return Workspace?.ResolveJsonSchemaReference(absoluteUri, parentSchema); } diff --git a/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs b/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs index e793aa985..29914ddac 100644 --- a/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs +++ b/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; @@ -45,18 +45,25 @@ internal async Task LoadAsync(BaseOpenApiReference reference, // If not already in workspace, load it and process references if (item.ExternalResource is not null && !_workspace.Contains(item.ExternalResource)) { - var uri = new Uri(item.ExternalResource, UriKind.RelativeOrAbsolute); - var input = await _loader.LoadAsync(item.HostDocument!.BaseUri, uri, cancellationToken).ConfigureAwait(false); - var result = await OpenApiDocument.LoadAsync(input, format, _readerSettings, cancellationToken).ConfigureAwait(false); - // Merge diagnostics - if (result.Diagnostic != null) + try { - diagnostic.AppendDiagnostic(result.Diagnostic, item.ExternalResource); + var uri = new Uri(item.ExternalResource, UriKind.RelativeOrAbsolute); + var input = await _loader.LoadAsync(item.HostDocument!.BaseUri, uri, cancellationToken).ConfigureAwait(false); + var result = await OpenApiDocument.LoadAsync(input, format, _readerSettings, cancellationToken).ConfigureAwait(false); + // Merge diagnostics + if (result.Diagnostic != null) + { + diagnostic.AppendDiagnostic(result.Diagnostic, item.ExternalResource); + } + if (result.Document != null) + { + var loadDiagnostic = await LoadAsync(item, result.Document, format, diagnostic, cancellationToken).ConfigureAwait(false); + diagnostic = loadDiagnostic; + } } - if (result.Document != null) + catch (Exception ex) { - var loadDiagnostic = await LoadAsync(item, result.Document, format, diagnostic, cancellationToken).ConfigureAwait(false); - diagnostic = loadDiagnostic; + diagnostic.Errors.Add(new OpenApiError(string.Empty, ex.Message)); } } } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiDiscriminatorDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiDiscriminatorDeserializer.cs index 38452fda2..8208053ba 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiDiscriminatorDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiDiscriminatorDeserializer.cs @@ -56,8 +56,28 @@ public static OpenApiDiscriminator LoadDiscriminator(JsonNode node, OpenApiDocum public static OpenApiSchemaReference LoadMapping(JsonNode node, OpenApiDocument hostDocument, ParsingContext context) { var pointer = node.GetScalarValue() ?? throw new InvalidOperationException("Could not get a pointer reference"); + if (IsBareSchemaName(pointer)) + { + var schemaRef = new OpenApiSchemaReference(pointer, hostDocument); + schemaRef.Reference.SetJsonPointerPath(pointer, context.GetLocation()); + return schemaRef; + } + var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2); + var result = new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2); + result.Reference.SetJsonPointerPath(pointer, context.GetLocation()); + return result; + } + + private static bool IsBareSchemaName(string pointer) + { + return !pointer.StartsWith("#", StringComparison.OrdinalIgnoreCase) + && !pointer.Contains("/") + && !pointer.Contains("#") + && !pointer.Contains(":") + && !pointer.EndsWith(".json", StringComparison.OrdinalIgnoreCase) + && !pointer.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) + && !pointer.EndsWith(".yml", StringComparison.OrdinalIgnoreCase); } } } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiDiscriminatorDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiDiscriminatorDeserializer.cs index a3aec0c53..0ed9c3bf8 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiDiscriminatorDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiDiscriminatorDeserializer.cs @@ -51,8 +51,28 @@ public static OpenApiDiscriminator LoadDiscriminator(JsonNode node, OpenApiDocum public static OpenApiSchemaReference LoadMapping(JsonNode node, OpenApiDocument hostDocument, ParsingContext context) { var pointer = node.GetScalarValue() ?? throw new InvalidOperationException("Could not get a pointer reference"); + if (IsBareSchemaName(pointer)) + { + var schemaRef = new OpenApiSchemaReference(pointer, hostDocument); + schemaRef.Reference.SetJsonPointerPath(pointer, context.GetLocation()); + return schemaRef; + } + var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2); + var result = new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2); + result.Reference.SetJsonPointerPath(pointer, context.GetLocation()); + return result; + } + + private static bool IsBareSchemaName(string pointer) + { + return !pointer.StartsWith("#", StringComparison.OrdinalIgnoreCase) + && !pointer.Contains("/") + && !pointer.Contains("#") + && !pointer.Contains(":") + && !pointer.EndsWith(".json", StringComparison.OrdinalIgnoreCase) + && !pointer.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) + && !pointer.EndsWith(".yml", StringComparison.OrdinalIgnoreCase); } } } diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index c9708339e..7c58d476a 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -949,14 +949,14 @@ public bool Contains(string location) private Uri? ToLocationUrl(string location) { - if (Uri.TryCreate(location, UriKind.Absolute, out var absoluteUri)) + if (BaseUrl is not null && Uri.TryCreate(BaseUrl, location, out var relativeUri)) { - return absoluteUri; + return relativeUri; } - if (BaseUrl is not null && Uri.TryCreate(BaseUrl, location, out var relativeUri)) + if (Uri.TryCreate(location, UriKind.Absolute, out var absoluteUri)) { - return relativeUri; + return absoluteUri; } return null; diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiComponentsTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiComponentsTests.cs index 01f7d97a8..7c6c7ba6c 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiComponentsTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiComponentsTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using Microsoft.OpenApi.Reader; @@ -14,7 +14,7 @@ public class OpenApiComponentsTests [InlineData("A:\\Dir\\File.json#/components/schemas/ExternalAbsWindowsPathModel", "ExternalAbsWindowsPathModel", "A:\\Dir\\File.json")] [InlineData("/Dir/File.json#/components/schemas/ExternalAbsUnixPathModel", "ExternalAbsUnixPathModel", "/Dir/File.json")] [InlineData("https://host.lan:1234/path/to/file/resource.json#/components/schemas/ExternalHttpsModel", "ExternalHttpsModel", "https://host.lan:1234/path/to/file/resource.json")] - [InlineData("File.json", "File.json", null)] + [InlineData("File.json", "File.json", "File.json")] public void ParseExternalSchemaReferenceShouldSucceed(string reference, string referenceId, string externalResource) { var input = $@"{{ diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDiscriminatorTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDiscriminatorTests.cs index 854afad9e..800bacabc 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDiscriminatorTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDiscriminatorTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System.Collections.Generic; @@ -42,5 +42,59 @@ public async Task ParseBasicDiscriminatorShouldSucceed() DefaultMapping = new OpenApiSchemaReference("Animal", openApiDocument) }, discriminator); } + + [Fact] + public void ParseDiscriminatorWithBareSchemaNameMappingResolvesLocalTarget() + { + var openApi = """ + { + "openapi": "3.1.1", + "info": { "title": "Repro", "version": "1" }, + "paths": {}, + "components": { + "schemas": { + "Dog": { + "type": "object", + "properties": { "bark": { "type": "boolean" } } + }, + "Pet": { + "type": "object", + "required": [ "petType" ], + "properties": { + "petType": { "type": "string" } + }, + "discriminator": { + "propertyName": "petType", + "mapping": { + "dog": "Dog" + }, + "x-oas-default-mapping": "Dog" + } + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + Assert.NotNull(doc); + Assert.Empty(readResult.Diagnostic.Errors); + + var petSchema = Assert.IsType(doc.Components.Schemas["Pet"]); + Assert.NotNull(petSchema.Discriminator); + + var dogMapping = petSchema.Discriminator.Mapping["dog"]; + Assert.False(dogMapping.Reference.IsExternal); + Assert.Null(dogMapping.Reference.ExternalResource); + Assert.Equal("Dog", dogMapping.Reference.Id); + + var dogSchema = Assert.IsType(doc.Components.Schemas["Dog"]); + Assert.Same(dogSchema, dogMapping.Target); + + Assert.NotNull(petSchema.Discriminator.DefaultMapping); + Assert.False(petSchema.Discriminator.DefaultMapping.Reference.IsExternal); + Assert.Same(dogSchema, petSchema.Discriminator.DefaultMapping.Target); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaReferenceDeserializerTests.cs index 5b05cc32a..8010092c4 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaReferenceDeserializerTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaReferenceDeserializerTests.cs @@ -82,5 +82,6 @@ public void ShouldDeserializeExternalSchemaReferenceWithoutFragment(string refPo Assert.True(resultReference.Reference.IsExternal); Assert.Equal(expectedId, resultReference.Reference.Id); Assert.Equal(expectedExternalResource, resultReference.Reference.ExternalResource); + Assert.Equal(refPointer, resultReference.Reference.ReferenceV3); } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs index 2bed9c518..fb6d8bf4a 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Net.Http; @@ -767,8 +767,9 @@ public void ParseDocumentWithRelativeReferenceWithoutFragmentDoesNotThrowUriForm } """; - var reader = new OpenApiStringReader(); - var doc = reader.Read(openApi, out var diagnostic); + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + var diagnostic = readResult.Diagnostic; Assert.NotNull(doc); Assert.Empty(diagnostic.Errors); @@ -780,6 +781,12 @@ public void ParseDocumentWithRelativeReferenceWithoutFragmentDoesNotThrowUriForm Assert.Equal("schemas/uuid.schema.json", attemptIdRef.Reference.Id); Assert.Null(attemptIdRef.Target); + + using var writer = new StringWriter(); + doc.SerializeAsV31(new OpenApiJsonWriter(writer)); + var serialized = writer.ToString(); + Assert.Contains("\"$ref\": \"schemas/uuid.schema.json\"", serialized); + Assert.DoesNotContain("#/components/schemas/schemas/uuid.schema.json", serialized); } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiComponentsTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiComponentsTests.cs index da82e35e7..2128454f9 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiComponentsTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiComponentsTests.cs @@ -14,7 +14,7 @@ public class OpenApiComponentsTests [InlineData("A:\\Dir\\File.json#/components/schemas/ExternalAbsWindowsPathModel", "ExternalAbsWindowsPathModel", "A:\\Dir\\File.json")] [InlineData("/Dir/File.json#/components/schemas/ExternalAbsUnixPathModel", "ExternalAbsUnixPathModel", "/Dir/File.json")] [InlineData("https://host.lan:1234/path/to/file/resource.json#/components/schemas/ExternalHttpsModel", "ExternalHttpsModel", "https://host.lan:1234/path/to/file/resource.json")] - [InlineData("File.json", "File.json", null)] + [InlineData("File.json", "File.json", "File.json")] public void ParseExternalSchemaReferenceShouldSucceed(string reference, string referenceId, string externalResource) { var input = $@"{{ diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDiscriminatorTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDiscriminatorTests.cs index c571b0ce6..7761281a0 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDiscriminatorTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDiscriminatorTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System.Collections.Generic; @@ -42,5 +42,59 @@ public async Task ParseBasicDiscriminatorShouldSucceed() DefaultMapping = new OpenApiSchemaReference("Animal", openApiDocument) }, discriminator); } + + [Fact] + public void ParseDiscriminatorWithBareSchemaNameMappingResolvesLocalTarget() + { + var openApi = """ + { + "openapi": "3.2.0", + "info": { "title": "Repro", "version": "1" }, + "paths": {}, + "components": { + "schemas": { + "Dog": { + "type": "object", + "properties": { "bark": { "type": "boolean" } } + }, + "Pet": { + "type": "object", + "required": [ "petType" ], + "properties": { + "petType": { "type": "string" } + }, + "discriminator": { + "propertyName": "petType", + "mapping": { + "dog": "Dog" + }, + "defaultMapping": "Dog" + } + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + Assert.NotNull(doc); + Assert.Empty(readResult.Diagnostic.Errors); + + var petSchema = Assert.IsType(doc.Components.Schemas["Pet"]); + Assert.NotNull(petSchema.Discriminator); + + var dogMapping = petSchema.Discriminator.Mapping["dog"]; + Assert.False(dogMapping.Reference.IsExternal); + Assert.Null(dogMapping.Reference.ExternalResource); + Assert.Equal("Dog", dogMapping.Reference.Id); + + var dogSchema = Assert.IsType(doc.Components.Schemas["Dog"]); + Assert.Same(dogSchema, dogMapping.Target); + + Assert.NotNull(petSchema.Discriminator.DefaultMapping); + Assert.False(petSchema.Discriminator.DefaultMapping.Reference.IsExternal); + Assert.Same(dogSchema, petSchema.Discriminator.DefaultMapping.Target); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaReferenceDeserializerTests.cs index fe7b4d5e8..63777e626 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaReferenceDeserializerTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaReferenceDeserializerTests.cs @@ -82,6 +82,7 @@ public void ShouldDeserializeExternalSchemaReferenceWithoutFragment(string refPo Assert.True(resultReference.Reference.IsExternal); Assert.Equal(expectedId, resultReference.Reference.Id); Assert.Equal(expectedExternalResource, resultReference.Reference.ExternalResource); + Assert.Equal(refPointer, resultReference.Reference.ReferenceV3); } } diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs index b9e066c60..f332cd8c3 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -2737,5 +2737,28 @@ public void ResolveReference_RelativeReferenceWithoutFragmentResolvesAgainstWork Assert.Same(targetSchema, result); Assert.Same(targetSchema, schemaRef.Target); } + + [Fact] + public void ResolveReference_ExternalComponentCollidingWithDocumentNameResolvesComponent() + { + var workspace = new OpenApiWorkspace(new Uri("https://example.com/api/")); + var targetSchema = new OpenApiSchema { Type = JsonSchemaType.String, Description = "Component named common.json" }; + workspace.AddDocumentId("common.json", new Uri("https://example.com/api/common.json")); + workspace.RegisterComponent("https://example.com/api/common.json#/components/schemas/common.json", targetSchema); + + var doc = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test", Version = "1.0" }, + BaseUri = new Uri("https://example.com/api/openapi.json"), + Workspace = workspace, + }; + + var schemaRef = new OpenApiSchemaReference("common.json", doc, "common.json"); + schemaRef.Reference.SetJsonPointerPath("common.json#/components/schemas/common.json", "#"); + + var result = doc.ResolveReferenceTo(schemaRef.Reference, null); + Assert.Same(targetSchema, result); + Assert.Same(targetSchema, schemaRef.Target); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs index 8f97f2c5a..4b67dc13b 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -187,6 +187,21 @@ public void OpenApiWorkspacesCanResolveAbsoluteUriReferences() var resolved = workspace.ResolveReference("https://example.com/schemas/uuid.schema.json"); Assert.Same(targetSchema, resolved); } + + [Fact] + public void OpenApiWorkspacesResolvesRootRelativeUriAgainstBaseUrl() + { + var workspace = new OpenApiWorkspace(new Uri("https://example.com/base/")); + var targetSchema = new OpenApiSchema + { + Type = JsonSchemaType.String, + Description = "Schema at root-relative URI" + }; + workspace.RegisterComponent("https://example.com/schemas/common.json", targetSchema); + + var resolved = workspace.ResolveReference("/schemas/common.json"); + Assert.Same(targetSchema, resolved); + } } public static class OpenApiFactoryExtensions From fb6b8eb85056344e88bc8695bb3efaf379e55e33 Mon Sep 17 00:00:00 2001 From: Hardik K Date: Sat, 14 Nov 2026 10:28:16 +0530 Subject: [PATCH 4/4] fix(models): retain non-schema reference fragments, handle loader cancellation, and preserve file-suffixed discriminator schemas (#3148) - Exclude OperationCanceledException from OpenApiWorkspaceLoader catch handler to preserve caller cancellation token semantics. - Retain authored component fragments and reference pointers across non-schema reference kinds (responses, parameters, headers, callbacks, examples, links, request bodies, security schemes, media types). - Distinguish standalone references from component references via IsStandalone on BaseOpenApiReference, preventing colliding component names from dropping their fragment. - Preserve local schema resolution in OpenApiDiscriminatorDeserializer for bare schema names including file-suffixed (.json, .yaml, .yml) and forward-declared schemas. - Add regression test coverage for colliding non-schema references, loader cancellation, and file-suffixed/forward-declared discriminator mappings. Signed-off-by: Hardik K --- .../Models/BaseOpenApiReference.cs | 32 +++-- .../Models/OpenApiDocument.cs | 2 +- .../Reader/Services/OpenApiWorkspaceLoader.cs | 2 +- .../Reader/V3/OpenApiCallbackDeserializer.cs | 4 +- .../Reader/V3/OpenApiExampleDeserializer.cs | 4 +- .../Reader/V3/OpenApiHeaderDeserializer.cs | 4 +- .../Reader/V3/OpenApiLinkDeserializer.cs | 4 +- .../Reader/V3/OpenApiParameterDeserializer.cs | 4 +- .../Reader/V3/OpenApiPathItemDeserializer.cs | 4 +- .../V3/OpenApiRequestBodyDeserializer.cs | 4 +- .../Reader/V3/OpenApiResponseDeserializer.cs | 4 +- .../V3/OpenApiSecuritySchemeDeserializer.cs | 4 +- .../Reader/V31/OpenApiCallbackDeserializer.cs | 1 + .../V31/OpenApiDiscriminatorDeserializer.cs | 9 +- .../Reader/V31/OpenApiExampleDeserializer.cs | 1 + .../Reader/V31/OpenApiHeaderDeserializer.cs | 1 + .../Reader/V31/OpenApiLinkDeserializer.cs | 1 + .../V31/OpenApiParameterDeserializer.cs | 1 + .../Reader/V31/OpenApiPathItemDeserializer.cs | 1 + .../V31/OpenApiRequestBodyDeserializer.cs | 1 + .../Reader/V31/OpenApiResponseDeserializer.cs | 1 + .../V31/OpenApiSecuritySchemeDeserializer.cs | 1 + .../Reader/V32/OpenApiCallbackDeserializer.cs | 1 + .../V32/OpenApiDiscriminatorDeserializer.cs | 9 +- .../Reader/V32/OpenApiExampleDeserializer.cs | 1 + .../Reader/V32/OpenApiHeaderDeserializer.cs | 1 + .../Reader/V32/OpenApiLinkDeserializer.cs | 1 + .../V32/OpenApiMediaTypeDeserializer.cs | 1 + .../V32/OpenApiParameterDeserializer.cs | 1 + .../Reader/V32/OpenApiPathItemDeserializer.cs | 1 + .../V32/OpenApiRequestBodyDeserializer.cs | 1 + .../Reader/V32/OpenApiResponseDeserializer.cs | 1 + .../V32/OpenApiSecuritySchemeDeserializer.cs | 1 + .../OpenApiWorkspaceStreamTests.cs | 40 +++++++ .../V31Tests/OpenApiDiscriminatorTests.cs | 111 ++++++++++++++++++ .../V31Tests/RelativeReferenceTests.cs | 52 ++++++++ .../V32Tests/OpenApiDiscriminatorTests.cs | 111 ++++++++++++++++++ .../Models/OpenApiDocumentTests.cs | 34 +++++- 38 files changed, 425 insertions(+), 32 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs b/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs index e3c854442..78295fc88 100644 --- a/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs +++ b/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs @@ -51,6 +51,16 @@ public class BaseOpenApiReference : IOpenApiSerializable /// public bool IsFragment { get; init; } + private bool? _isStandalone; + /// + /// Gets or sets a flag indicating whether this reference points directly to a standalone external resource rather than a component within it. + /// + internal bool IsStandalone + { + get => _isStandalone ?? (Type == ReferenceType.Schema && IsExternal && Id is not null && string.Equals(ExternalResource, Id, StringComparison.Ordinal) && !Id.Contains("#")); + set => _isStandalone = value; + } + private OpenApiDocument? hostDocument; /// /// The OpenApiDocument that is hosting the OpenApiReference instance. This is used to enable dereferencing the reference. @@ -148,6 +158,9 @@ public BaseOpenApiReference(BaseOpenApiReference reference) Type = reference.Type; Id = reference.Id; HostDocument = reference.HostDocument; + IsFragment = reference.IsFragment; + IsStandalone = reference.IsStandalone; + _referenceV3 = reference._referenceV3; } /// @@ -241,6 +254,11 @@ public virtual void SerializeAsV2(IOpenApiWriter writer) private string? GetExternalReferenceV3() { + if (IsStandalone) + { + return ExternalResource; + } + if (Id != null) { if (IsFragment) @@ -254,11 +272,6 @@ public virtual void SerializeAsV2(IOpenApiWriter writer) return Id; } - if (string.Equals(ExternalResource, Id, StringComparison.Ordinal) && !Id.Contains("#")) - { - return ExternalResource; - } - return ExternalResource + "#/components/" + Type.GetDisplayName() + "/"+ Id; } @@ -326,9 +339,9 @@ internal void SetJsonPointerPath(string pointer, ParsingContext context) { // Relative reference to internal JSON schema node/resource (e.g. "#/properties/b") #if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER || NET5_0_OR_GREATER - if (pointer.StartsWith("#/", StringComparison.OrdinalIgnoreCase) && !pointer.Contains("/components/schemas", StringComparison.OrdinalIgnoreCase)) + if (pointer.StartsWith("#/", StringComparison.OrdinalIgnoreCase) && !pointer.Contains("/components/", StringComparison.OrdinalIgnoreCase)) #else - if (pointer.StartsWith("#/", StringComparison.OrdinalIgnoreCase) && !pointer.ToLowerInvariant().Contains("/components/schemas")) + if (pointer.StartsWith("#/", StringComparison.OrdinalIgnoreCase) && !pointer.ToLowerInvariant().Contains("/components/")) #endif { ReferenceV3 = ResolveRelativePointer(context.GetLocation(), pointer); @@ -339,6 +352,11 @@ internal void SetJsonPointerPath(string pointer, ParsingContext context) { ReferenceV3 = pointer; } + + if (IsExternal) + { + IsStandalone = !pointer.Contains('#'); + } } private static string ResolveRelativePointer(string nodeLocation, string relativeRef) diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index a3bc074b2..4e6bcbff6 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -633,7 +633,7 @@ private static string ConvertByteArrayToString(byte[] hash) var id = reference.Id; var referenceV3 = !string.IsNullOrEmpty(reference.ReferenceV3) ? reference.ReferenceV3! : string.Empty; var isStandaloneExternal = reference.IsExternal && - (!string.IsNullOrEmpty(referenceV3) ? !referenceV3.Contains("#") : (id is not null && !id.Contains("#"))); + (reference.IsStandalone || (!string.IsNullOrEmpty(referenceV3) ? !referenceV3.Contains("#") : (reference.Type == ReferenceType.Schema && id is not null && !id.Contains("#") && id.Contains("/")))); if (isStandaloneExternal) { diff --git a/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs b/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs index 29914ddac..a4bfcb9c8 100644 --- a/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs +++ b/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs @@ -61,7 +61,7 @@ internal async Task LoadAsync(BaseOpenApiReference reference, diagnostic = loadDiagnostic; } } - catch (Exception ex) + catch (Exception ex) when (ex is not OperationCanceledException) { diagnostic.Errors.Add(new OpenApiError(string.Empty, ex.Message)); } diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiCallbackDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiCallbackDeserializer.cs index 686968e4c..95b55ceae 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiCallbackDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiCallbackDeserializer.cs @@ -31,7 +31,9 @@ public static IOpenApiCallback LoadCallback(JsonNode node, OpenApiDocument hostD if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiCallbackReference(reference.Item1, hostDocument, reference.Item2); + var callbackReference = new OpenApiCallbackReference(reference.Item1, hostDocument, reference.Item2); + callbackReference.Reference.SetJsonPointerPath(pointer, context); + return callbackReference; } var domainObject = new OpenApiCallback(); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiExampleDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiExampleDeserializer.cs index 03f587f58..ade0e7e06 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiExampleDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiExampleDeserializer.cs @@ -49,7 +49,9 @@ public static IOpenApiExample LoadExample(JsonNode node, OpenApiDocument hostDoc if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiExampleReference(reference.Item1, hostDocument, reference.Item2); + var exampleReference = new OpenApiExampleReference(reference.Item1, hostDocument, reference.Item2); + exampleReference.Reference.SetJsonPointerPath(pointer, context); + return exampleReference; } var example = new OpenApiExample(); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiHeaderDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiHeaderDeserializer.cs index c82b47bd8..6a70bda45 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiHeaderDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiHeaderDeserializer.cs @@ -118,7 +118,9 @@ public static IOpenApiHeader LoadHeader(JsonNode node, OpenApiDocument hostDocum if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiHeaderReference(reference.Item1, hostDocument, reference.Item2); + var headerReference = new OpenApiHeaderReference(reference.Item1, hostDocument, reference.Item2); + headerReference.Reference.SetJsonPointerPath(pointer, context); + return headerReference; } var header = new OpenApiHeader(); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiLinkDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiLinkDeserializer.cs index 37eb476b4..e47aa94d6 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiLinkDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiLinkDeserializer.cs @@ -52,7 +52,9 @@ public static IOpenApiLink LoadLink(JsonNode node, OpenApiDocument hostDocument, if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiLinkReference(reference.Item1, hostDocument, reference.Item2); + var linkReference = new OpenApiLinkReference(reference.Item1, hostDocument, reference.Item2); + linkReference.Reference.SetJsonPointerPath(pointer, context); + return linkReference; } ParseMap(jsonObject, link, _linkFixedFields, _linkPatternFields, hostDocument, context); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiParameterDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiParameterDeserializer.cs index 435515658..d51befb53 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiParameterDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiParameterDeserializer.cs @@ -158,7 +158,9 @@ public static IOpenApiParameter LoadParameter(JsonNode node, OpenApiDocument hos if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiParameterReference(reference.Item1, hostDocument, reference.Item2); + var parameterReference = new OpenApiParameterReference(reference.Item1, hostDocument, reference.Item2); + parameterReference.Reference.SetJsonPointerPath(pointer, context); + return parameterReference; } var parameter = new OpenApiParameter(); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiPathItemDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiPathItemDeserializer.cs index b24619835..7b3a1fce3 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiPathItemDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiPathItemDeserializer.cs @@ -54,7 +54,9 @@ public static IOpenApiPathItem LoadPathItem(JsonNode node, OpenApiDocument hostD if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiPathItemReference(reference.Item1, hostDocument, reference.Item2); + var pathItemReference = new OpenApiPathItemReference(reference.Item1, hostDocument, reference.Item2); + pathItemReference.Reference.SetJsonPointerPath(pointer, context); + return pathItemReference; } var pathItem = new OpenApiPathItem(); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiRequestBodyDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiRequestBodyDeserializer.cs index 8aee2e9a8..bd54de6df 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiRequestBodyDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiRequestBodyDeserializer.cs @@ -51,7 +51,9 @@ public static IOpenApiRequestBody LoadRequestBody(JsonNode node, OpenApiDocument if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiRequestBodyReference(reference.Item1, hostDocument, reference.Item2); + var requestBodyReference = new OpenApiRequestBodyReference(reference.Item1, hostDocument, reference.Item2); + requestBodyReference.Reference.SetJsonPointerPath(pointer, context); + return requestBodyReference; } var requestBody = new OpenApiRequestBody(); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiResponseDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiResponseDeserializer.cs index dba913db4..a5aacc271 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiResponseDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiResponseDeserializer.cs @@ -57,7 +57,9 @@ public static IOpenApiResponse LoadResponse(JsonNode node, OpenApiDocument hostD if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiResponseReference(reference.Item1, hostDocument, reference.Item2); + var responseReference = new OpenApiResponseReference(reference.Item1, hostDocument, reference.Item2); + responseReference.Reference.SetJsonPointerPath(pointer, context); + return responseReference; } var response = new OpenApiResponse(); diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiSecuritySchemeDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiSecuritySchemeDeserializer.cs index 3f9d8be83..322273be6 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiSecuritySchemeDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiSecuritySchemeDeserializer.cs @@ -106,7 +106,9 @@ public static IOpenApiSecurityScheme LoadSecurityScheme(JsonNode node, OpenApiDo if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); - return new OpenApiSecuritySchemeReference(reference.Item1, hostDocument, reference.Item2); + var securitySchemeReference = new OpenApiSecuritySchemeReference(reference.Item1, hostDocument, reference.Item2); + securitySchemeReference.Reference.SetJsonPointerPath(pointer, context); + return securitySchemeReference; } var securityScheme = new OpenApiSecurityScheme(); diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiCallbackDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiCallbackDeserializer.cs index f37f59d2b..5bbd656d1 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiCallbackDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiCallbackDeserializer.cs @@ -28,6 +28,7 @@ public static IOpenApiCallback LoadCallback(JsonNode node, OpenApiDocument hostD var reference = GetReferenceIdAndExternalResource(pointer); var callbackReference = new OpenApiCallbackReference(reference.Item1, hostDocument, reference.Item2); callbackReference.Reference.SetMetadataFromJsonObject(jsonObject); + callbackReference.Reference.SetJsonPointerPath(pointer, context); return callbackReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiDiscriminatorDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiDiscriminatorDeserializer.cs index 8208053ba..9ecd0897b 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiDiscriminatorDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiDiscriminatorDeserializer.cs @@ -59,13 +59,13 @@ public static OpenApiSchemaReference LoadMapping(JsonNode node, OpenApiDocument if (IsBareSchemaName(pointer)) { var schemaRef = new OpenApiSchemaReference(pointer, hostDocument); - schemaRef.Reference.SetJsonPointerPath(pointer, context.GetLocation()); + schemaRef.Reference.SetJsonPointerPath(pointer, context); return schemaRef; } var reference = GetReferenceIdAndExternalResource(pointer); var result = new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2); - result.Reference.SetJsonPointerPath(pointer, context.GetLocation()); + result.Reference.SetJsonPointerPath(pointer, context); return result; } @@ -74,10 +74,7 @@ private static bool IsBareSchemaName(string pointer) return !pointer.StartsWith("#", StringComparison.OrdinalIgnoreCase) && !pointer.Contains("/") && !pointer.Contains("#") - && !pointer.Contains(":") - && !pointer.EndsWith(".json", StringComparison.OrdinalIgnoreCase) - && !pointer.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) - && !pointer.EndsWith(".yml", StringComparison.OrdinalIgnoreCase); + && !pointer.Contains(":"); } } } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiExampleDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiExampleDeserializer.cs index 8c6fef31e..bddf504ba 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiExampleDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiExampleDeserializer.cs @@ -56,6 +56,7 @@ public static IOpenApiExample LoadExample(JsonNode node, OpenApiDocument hostDoc var reference = GetReferenceIdAndExternalResource(pointer); var exampleReference = new OpenApiExampleReference(reference.Item1, hostDocument, reference.Item2); exampleReference.Reference.SetMetadataFromJsonObject(jsonObject); + exampleReference.Reference.SetJsonPointerPath(pointer, context); return exampleReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiHeaderDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiHeaderDeserializer.cs index 10eac3dea..ab56d7402 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiHeaderDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiHeaderDeserializer.cs @@ -123,6 +123,7 @@ public static IOpenApiHeader LoadHeader(JsonNode node, OpenApiDocument hostDocum var reference = GetReferenceIdAndExternalResource(pointer); var headerReference = new OpenApiHeaderReference(reference.Item1, hostDocument, reference.Item2); headerReference.Reference.SetMetadataFromJsonObject(jsonObject); + headerReference.Reference.SetJsonPointerPath(pointer, context); return headerReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiLinkDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiLinkDeserializer.cs index e9f07205d..3ac927a81 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiLinkDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiLinkDeserializer.cs @@ -60,6 +60,7 @@ public static IOpenApiLink LoadLink(JsonNode node, OpenApiDocument hostDocument, var reference = GetReferenceIdAndExternalResource(pointer); var linkReference = new OpenApiLinkReference(reference.Item1, hostDocument, reference.Item2); linkReference.Reference.SetMetadataFromJsonObject(jsonObject); + linkReference.Reference.SetJsonPointerPath(pointer, context); return linkReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiParameterDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiParameterDeserializer.cs index 177342fb4..deec75e4d 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiParameterDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiParameterDeserializer.cs @@ -166,6 +166,7 @@ public static IOpenApiParameter LoadParameter(JsonNode node, OpenApiDocument hos var reference = GetReferenceIdAndExternalResource(pointer); var parameterReference = new OpenApiParameterReference(reference.Item1, hostDocument, reference.Item2); parameterReference.Reference.SetMetadataFromJsonObject(jsonObject); + parameterReference.Reference.SetJsonPointerPath(pointer, context); return parameterReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiPathItemDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiPathItemDeserializer.cs index 101572f5b..dccb5adee 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiPathItemDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiPathItemDeserializer.cs @@ -58,6 +58,7 @@ public static IOpenApiPathItem LoadPathItem(JsonNode node, OpenApiDocument hostD var reference = GetReferenceIdAndExternalResource(pointer); var pathItemReference = new OpenApiPathItemReference(reference.Item1, hostDocument, reference.Item2); pathItemReference.Reference.SetMetadataFromJsonObject(jsonObject); + pathItemReference.Reference.SetJsonPointerPath(pointer, context); return pathItemReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiRequestBodyDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiRequestBodyDeserializer.cs index 1efb61a82..62675bdbd 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiRequestBodyDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiRequestBodyDeserializer.cs @@ -52,6 +52,7 @@ public static IOpenApiRequestBody LoadRequestBody(JsonNode node, OpenApiDocument var reference = GetReferenceIdAndExternalResource(pointer); var requestBodyReference = new OpenApiRequestBodyReference(reference.Item1, hostDocument, reference.Item2); requestBodyReference.Reference.SetMetadataFromJsonObject(jsonObject); + requestBodyReference.Reference.SetJsonPointerPath(pointer, context); return requestBodyReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiResponseDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiResponseDeserializer.cs index b8d64733d..a1992ca97 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiResponseDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiResponseDeserializer.cs @@ -63,6 +63,7 @@ public static IOpenApiResponse LoadResponse(JsonNode node, OpenApiDocument hostD var reference = GetReferenceIdAndExternalResource(pointer); var responseReference = new OpenApiResponseReference(reference.Item1, hostDocument, reference.Item2); responseReference.Reference.SetMetadataFromJsonObject(jsonObject); + responseReference.Reference.SetJsonPointerPath(pointer, context); return responseReference; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSecuritySchemeDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSecuritySchemeDeserializer.cs index fac4b87c1..b7095da47 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSecuritySchemeDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSecuritySchemeDeserializer.cs @@ -116,6 +116,7 @@ public static IOpenApiSecurityScheme LoadSecurityScheme(JsonNode node, OpenApiDo var reference = GetReferenceIdAndExternalResource(pointer); var securitySchemeReference = new OpenApiSecuritySchemeReference(reference.Item1, hostDocument, reference.Item2); securitySchemeReference.Reference.SetMetadataFromJsonObject(jsonObject); + securitySchemeReference.Reference.SetJsonPointerPath(pointer, context); return securitySchemeReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiCallbackDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiCallbackDeserializer.cs index 1c3b9dcab..83fa274bb 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiCallbackDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiCallbackDeserializer.cs @@ -28,6 +28,7 @@ public static IOpenApiCallback LoadCallback(JsonNode node, OpenApiDocument hostD var reference = GetReferenceIdAndExternalResource(pointer); var callbackReference = new OpenApiCallbackReference(reference.Item1, hostDocument, reference.Item2); callbackReference.Reference.SetMetadataFromJsonObject(jsonObject); + callbackReference.Reference.SetJsonPointerPath(pointer, context); return callbackReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiDiscriminatorDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiDiscriminatorDeserializer.cs index 0ed9c3bf8..49abc3ad8 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiDiscriminatorDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiDiscriminatorDeserializer.cs @@ -54,13 +54,13 @@ public static OpenApiSchemaReference LoadMapping(JsonNode node, OpenApiDocument if (IsBareSchemaName(pointer)) { var schemaRef = new OpenApiSchemaReference(pointer, hostDocument); - schemaRef.Reference.SetJsonPointerPath(pointer, context.GetLocation()); + schemaRef.Reference.SetJsonPointerPath(pointer, context); return schemaRef; } var reference = GetReferenceIdAndExternalResource(pointer); var result = new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2); - result.Reference.SetJsonPointerPath(pointer, context.GetLocation()); + result.Reference.SetJsonPointerPath(pointer, context); return result; } @@ -69,10 +69,7 @@ private static bool IsBareSchemaName(string pointer) return !pointer.StartsWith("#", StringComparison.OrdinalIgnoreCase) && !pointer.Contains("/") && !pointer.Contains("#") - && !pointer.Contains(":") - && !pointer.EndsWith(".json", StringComparison.OrdinalIgnoreCase) - && !pointer.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) - && !pointer.EndsWith(".yml", StringComparison.OrdinalIgnoreCase); + && !pointer.Contains(":"); } } } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiExampleDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiExampleDeserializer.cs index b83b25c65..8c038603d 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiExampleDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiExampleDeserializer.cs @@ -66,6 +66,7 @@ public static IOpenApiExample LoadExample(JsonNode node, OpenApiDocument hostDoc var reference = GetReferenceIdAndExternalResource(pointer); var exampleReference = new OpenApiExampleReference(reference.Item1, hostDocument, reference.Item2); exampleReference.Reference.SetMetadataFromJsonObject(jsonObject); + exampleReference.Reference.SetJsonPointerPath(pointer, context); return exampleReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiHeaderDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiHeaderDeserializer.cs index f7cd702d3..639c97f23 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiHeaderDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiHeaderDeserializer.cs @@ -123,6 +123,7 @@ public static IOpenApiHeader LoadHeader(JsonNode node, OpenApiDocument hostDocum var reference = GetReferenceIdAndExternalResource(pointer); var headerReference = new OpenApiHeaderReference(reference.Item1, hostDocument, reference.Item2); headerReference.Reference.SetMetadataFromJsonObject(jsonObject); + headerReference.Reference.SetJsonPointerPath(pointer, context); return headerReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiLinkDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiLinkDeserializer.cs index 5655d397d..912c8cbff 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiLinkDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiLinkDeserializer.cs @@ -60,6 +60,7 @@ public static IOpenApiLink LoadLink(JsonNode node, OpenApiDocument hostDocument, var reference = GetReferenceIdAndExternalResource(pointer); var linkReference = new OpenApiLinkReference(reference.Item1, hostDocument, reference.Item2); linkReference.Reference.SetMetadataFromJsonObject(jsonObject); + linkReference.Reference.SetJsonPointerPath(pointer, context); return linkReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiMediaTypeDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiMediaTypeDeserializer.cs index 7bff0f942..c09ec61b9 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiMediaTypeDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiMediaTypeDeserializer.cs @@ -97,6 +97,7 @@ public static IOpenApiMediaType LoadMediaType(JsonNode node, OpenApiDocument hos var reference = GetReferenceIdAndExternalResource(pointer); var mediaTypeReference = new OpenApiMediaTypeReference(reference.Item1, hostDocument, reference.Item2); mediaTypeReference.Reference.SetMetadataFromJsonObject(jsonObject); + mediaTypeReference.Reference.SetJsonPointerPath(pointer, context); return mediaTypeReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiParameterDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiParameterDeserializer.cs index 4a8300766..213376ae3 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiParameterDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiParameterDeserializer.cs @@ -166,6 +166,7 @@ public static IOpenApiParameter LoadParameter(JsonNode node, OpenApiDocument hos var reference = GetReferenceIdAndExternalResource(pointer); var parameterReference = new OpenApiParameterReference(reference.Item1, hostDocument, reference.Item2); parameterReference.Reference.SetMetadataFromJsonObject(jsonObject); + parameterReference.Reference.SetJsonPointerPath(pointer, context); return parameterReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiPathItemDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiPathItemDeserializer.cs index 3cc9bab03..b89dc80e0 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiPathItemDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiPathItemDeserializer.cs @@ -82,6 +82,7 @@ public static IOpenApiPathItem LoadPathItem(JsonNode node, OpenApiDocument hostD var reference = GetReferenceIdAndExternalResource(pointer); var pathItemReference = new OpenApiPathItemReference(reference.Item1, hostDocument, reference.Item2); pathItemReference.Reference.SetMetadataFromJsonObject(jsonObject); + pathItemReference.Reference.SetJsonPointerPath(pointer, context); return pathItemReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiRequestBodyDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiRequestBodyDeserializer.cs index a3ccbfcda..63ce237ef 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiRequestBodyDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiRequestBodyDeserializer.cs @@ -52,6 +52,7 @@ public static IOpenApiRequestBody LoadRequestBody(JsonNode node, OpenApiDocument var reference = GetReferenceIdAndExternalResource(pointer); var requestBodyReference = new OpenApiRequestBodyReference(reference.Item1, hostDocument, reference.Item2); requestBodyReference.Reference.SetMetadataFromJsonObject(jsonObject); + requestBodyReference.Reference.SetJsonPointerPath(pointer, context); return requestBodyReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiResponseDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiResponseDeserializer.cs index a0e36d017..c2f9f890d 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiResponseDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiResponseDeserializer.cs @@ -59,6 +59,7 @@ public static IOpenApiResponse LoadResponse(JsonNode node, OpenApiDocument hostD var reference = GetReferenceIdAndExternalResource(pointer); var responseReference = new OpenApiResponseReference(reference.Item1, hostDocument, reference.Item2); responseReference.Reference.SetMetadataFromJsonObject(jsonObject); + responseReference.Reference.SetJsonPointerPath(pointer, context); return responseReference; } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiSecuritySchemeDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiSecuritySchemeDeserializer.cs index ca5ad4e48..745614d92 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiSecuritySchemeDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiSecuritySchemeDeserializer.cs @@ -114,6 +114,7 @@ public static IOpenApiSecurityScheme LoadSecurityScheme(JsonNode node, OpenApiDo var reference = GetReferenceIdAndExternalResource(pointer); var securitySchemeReference = new OpenApiSecuritySchemeReference(reference.Item1, hostDocument, reference.Item2); securitySchemeReference.Reference.SetMetadataFromJsonObject(jsonObject); + securitySchemeReference.Reference.SetJsonPointerPath(pointer, context); return securitySchemeReference; } diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs index 34f4f2d45..f7a9906b9 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs @@ -124,6 +124,46 @@ public async Task LoadDocumentWithExternalReferencesInSubDirectories() } }, petSchemaTarget); } + + [Fact] + public async Task LoadDocumentAsync_WhenLoaderThrowsOperationCanceledException_PropagatesCancellation() + { + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + CustomExternalLoader = new CancellingStreamLoader(), + BaseUrl = new("file://c:\\"), + }; + settings.AddYamlReader(); + + await Assert.ThrowsAnyAsync(async () => + await OpenApiDocument.LoadAsync("V3Tests/Samples/OpenApiWorkspace/TodoMain.yaml", settings, token: TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task LoadDocumentAsync_WhenCancellationTokenCanceled_PropagatesCancellation() + { + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + CustomExternalLoader = new ResourceLoader(), + BaseUrl = new("file://c:\\"), + }; + settings.AddYamlReader(); + + await Assert.ThrowsAnyAsync(async () => + await OpenApiDocument.LoadAsync("V3Tests/Samples/OpenApiWorkspace/TodoMain.yaml", settings, token: cts.Token)); + } + } + + public class CancellingStreamLoader : IStreamLoader + { + public Stream Load(Uri uri) => throw new OperationCanceledException(); + public Task LoadAsync(Uri baseUrl, Uri uri, CancellationToken cancellationToken = default) + => Task.FromException(new OperationCanceledException(cancellationToken)); } public class MockLoader : IStreamLoader diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDiscriminatorTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDiscriminatorTests.cs index 800bacabc..42dbbd429 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDiscriminatorTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDiscriminatorTests.cs @@ -96,5 +96,116 @@ public void ParseDiscriminatorWithBareSchemaNameMappingResolvesLocalTarget() Assert.False(petSchema.Discriminator.DefaultMapping.Reference.IsExternal); Assert.Same(dogSchema, petSchema.Discriminator.DefaultMapping.Target); } + + [Theory] + [InlineData("Dog.json")] + [InlineData("Dog.yaml")] + [InlineData("Dog.yml")] + public void ParseDiscriminatorWithFileSuffixedBareSchemaNameResolvesLocalTarget(string schemaName) + { + var openApi = $$""" + { + "openapi": "3.1.1", + "info": { "title": "Repro", "version": "1" }, + "paths": {}, + "components": { + "schemas": { + "{{schemaName}}": { + "type": "object", + "properties": { "bark": { "type": "boolean" } } + }, + "Pet": { + "type": "object", + "required": [ "petType" ], + "properties": { + "petType": { "type": "string" } + }, + "discriminator": { + "propertyName": "petType", + "mapping": { + "dog": "{{schemaName}}" + }, + "x-oas-default-mapping": "{{schemaName}}" + } + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + Assert.NotNull(doc); + Assert.Empty(readResult.Diagnostic.Errors); + + var petSchema = Assert.IsType(doc.Components.Schemas["Pet"]); + Assert.NotNull(petSchema.Discriminator); + + var dogMapping = petSchema.Discriminator.Mapping["dog"]; + Assert.False(dogMapping.Reference.IsExternal); + Assert.Null(dogMapping.Reference.ExternalResource); + Assert.Equal(schemaName, dogMapping.Reference.Id); + + var dogSchema = Assert.IsType(doc.Components.Schemas[schemaName]); + Assert.Same(dogSchema, dogMapping.Target); + + Assert.NotNull(petSchema.Discriminator.DefaultMapping); + Assert.False(petSchema.Discriminator.DefaultMapping.Reference.IsExternal); + Assert.Same(dogSchema, petSchema.Discriminator.DefaultMapping.Target); + } + + [Fact] + public void ParseDiscriminatorWithForwardDeclaredSchemaMappingResolvesLocalTarget() + { + var openApi = """ + { + "openapi": "3.1.1", + "info": { "title": "Repro", "version": "1" }, + "paths": {}, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": [ "petType" ], + "properties": { + "petType": { "type": "string" } + }, + "discriminator": { + "propertyName": "petType", + "mapping": { + "dog": "Dog.json" + }, + "x-oas-default-mapping": "Dog.json" + } + }, + "Dog.json": { + "type": "object", + "properties": { "bark": { "type": "boolean" } } + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + Assert.NotNull(doc); + Assert.Empty(readResult.Diagnostic.Errors); + + var petSchema = Assert.IsType(doc.Components.Schemas["Pet"]); + Assert.NotNull(petSchema.Discriminator); + + var dogMapping = petSchema.Discriminator.Mapping["dog"]; + Assert.False(dogMapping.Reference.IsExternal); + Assert.Null(dogMapping.Reference.ExternalResource); + Assert.Equal("Dog.json", dogMapping.Reference.Id); + + var dogSchema = Assert.IsType(doc.Components.Schemas["Dog.json"]); + Assert.Same(dogSchema, dogMapping.Target); + + Assert.NotNull(petSchema.Discriminator.DefaultMapping); + Assert.False(petSchema.Discriminator.DefaultMapping.Reference.IsExternal); + Assert.Same(dogSchema, petSchema.Discriminator.DefaultMapping.Target); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs index fb6d8bf4a..10107301c 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs @@ -788,5 +788,57 @@ public void ParseDocumentWithRelativeReferenceWithoutFragmentDoesNotThrowUriForm Assert.Contains("\"$ref\": \"schemas/uuid.schema.json\"", serialized); Assert.DoesNotContain("#/components/schemas/schemas/uuid.schema.json", serialized); } + + [Fact] + public void ParseDocumentWithExternalNonSchemaReferenceCollidingWithDocumentNamePreservesComponentFragment() + { + var openApi = """ + { + "openapi": "3.1.1", + "info": { "title": "Repro", "version": "1" }, + "paths": { + "/items": { + "get": { + "parameters": [ + { "$ref": "common.json#/components/parameters/common.json" } + ], + "responses": { + "200": { "$ref": "common.json#/components/responses/common.json" } + } + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + var diagnostic = readResult.Diagnostic; + + Assert.NotNull(doc); + Assert.Empty(diagnostic.Errors); + + var pathItem = doc.Paths["/items"]; + var operation = pathItem.Operations[HttpMethod.Get]; + + var paramRef = Assert.IsType(operation.Parameters[0]); + Assert.True(paramRef.Reference.IsExternal); + Assert.Equal("common.json", paramRef.Reference.ExternalResource); + Assert.Equal("common.json", paramRef.Reference.Id); + Assert.Equal("common.json#/components/parameters/common.json", paramRef.Reference.ReferenceV3); + + var responseRef = Assert.IsType(operation.Responses["200"]); + Assert.True(responseRef.Reference.IsExternal); + Assert.Equal("common.json", responseRef.Reference.ExternalResource); + Assert.Equal("common.json", responseRef.Reference.Id); + Assert.Equal("common.json#/components/responses/common.json", responseRef.Reference.ReferenceV3); + + using var writer = new StringWriter(); + doc.SerializeAsV31(new OpenApiJsonWriter(writer)); + var serialized = writer.ToString(); + Assert.Contains("\"$ref\": \"common.json#/components/parameters/common.json\"", serialized); + Assert.Contains("\"$ref\": \"common.json#/components/responses/common.json\"", serialized); + Assert.DoesNotContain("\"$ref\": \"common.json\"", serialized); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDiscriminatorTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDiscriminatorTests.cs index 7761281a0..fffdc7c52 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDiscriminatorTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDiscriminatorTests.cs @@ -96,5 +96,116 @@ public void ParseDiscriminatorWithBareSchemaNameMappingResolvesLocalTarget() Assert.False(petSchema.Discriminator.DefaultMapping.Reference.IsExternal); Assert.Same(dogSchema, petSchema.Discriminator.DefaultMapping.Target); } + + [Theory] + [InlineData("Dog.json")] + [InlineData("Dog.yaml")] + [InlineData("Dog.yml")] + public void ParseDiscriminatorWithFileSuffixedBareSchemaNameResolvesLocalTarget(string schemaName) + { + var openApi = $$""" + { + "openapi": "3.2.0", + "info": { "title": "Repro", "version": "1" }, + "paths": {}, + "components": { + "schemas": { + "{{schemaName}}": { + "type": "object", + "properties": { "bark": { "type": "boolean" } } + }, + "Pet": { + "type": "object", + "required": [ "petType" ], + "properties": { + "petType": { "type": "string" } + }, + "discriminator": { + "propertyName": "petType", + "mapping": { + "dog": "{{schemaName}}" + }, + "defaultMapping": "{{schemaName}}" + } + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + Assert.NotNull(doc); + Assert.Empty(readResult.Diagnostic.Errors); + + var petSchema = Assert.IsType(doc.Components.Schemas["Pet"]); + Assert.NotNull(petSchema.Discriminator); + + var dogMapping = petSchema.Discriminator.Mapping["dog"]; + Assert.False(dogMapping.Reference.IsExternal); + Assert.Null(dogMapping.Reference.ExternalResource); + Assert.Equal(schemaName, dogMapping.Reference.Id); + + var dogSchema = Assert.IsType(doc.Components.Schemas[schemaName]); + Assert.Same(dogSchema, dogMapping.Target); + + Assert.NotNull(petSchema.Discriminator.DefaultMapping); + Assert.False(petSchema.Discriminator.DefaultMapping.Reference.IsExternal); + Assert.Same(dogSchema, petSchema.Discriminator.DefaultMapping.Target); + } + + [Fact] + public void ParseDiscriminatorWithForwardDeclaredSchemaMappingResolvesLocalTarget() + { + var openApi = """ + { + "openapi": "3.2.0", + "info": { "title": "Repro", "version": "1" }, + "paths": {}, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": [ "petType" ], + "properties": { + "petType": { "type": "string" } + }, + "discriminator": { + "propertyName": "petType", + "mapping": { + "dog": "Dog.json" + }, + "defaultMapping": "Dog.json" + } + }, + "Dog.json": { + "type": "object", + "properties": { "bark": { "type": "boolean" } } + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(openApi, "json"); + var doc = readResult.Document; + Assert.NotNull(doc); + Assert.Empty(readResult.Diagnostic.Errors); + + var petSchema = Assert.IsType(doc.Components.Schemas["Pet"]); + Assert.NotNull(petSchema.Discriminator); + + var dogMapping = petSchema.Discriminator.Mapping["dog"]; + Assert.False(dogMapping.Reference.IsExternal); + Assert.Null(dogMapping.Reference.ExternalResource); + Assert.Equal("Dog.json", dogMapping.Reference.Id); + + var dogSchema = Assert.IsType(doc.Components.Schemas["Dog.json"]); + Assert.Same(dogSchema, dogMapping.Target); + + Assert.NotNull(petSchema.Discriminator.DefaultMapping); + Assert.False(petSchema.Discriminator.DefaultMapping.Reference.IsExternal); + Assert.Same(dogSchema, petSchema.Discriminator.DefaultMapping.Target); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs index 6769350d5..f3322a396 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs @@ -2810,13 +2810,44 @@ public void ResolveReference_ExternalComponentCollidingWithDocumentNameResolvesC }; var schemaRef = new OpenApiSchemaReference("common.json", doc, "common.json"); - schemaRef.Reference.SetJsonPointerPath("common.json#/components/schemas/common.json", "#"); + schemaRef.Reference.SetJsonPointerPath("common.json#/components/schemas/common.json", new Microsoft.OpenApi.Reader.ParsingContext(new())); var result = doc.ResolveReferenceTo(schemaRef.Reference, null); Assert.Same(targetSchema, result); Assert.Same(targetSchema, schemaRef.Target); } + [Fact] + public void ResolveReference_ExternalNonSchemaComponentCollidingWithDocumentNameResolvesComponent() + { + var workspace = new OpenApiWorkspace(new Uri("https://example.com/api/")); + var targetResponse = new OpenApiResponse { Description = "Response named common.json" }; + var targetParam = new OpenApiParameter { Name = "common.json", In = ParameterLocation.Query }; + workspace.AddDocumentId("common.json", new Uri("https://example.com/api/common.json")); + workspace.RegisterComponent("https://example.com/api/common.json#/components/responses/common.json", targetResponse); + workspace.RegisterComponent("https://example.com/api/common.json#/components/parameters/common.json", targetParam); + + var doc = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test", Version = "1.0" }, + BaseUri = new Uri("https://example.com/api/openapi.json"), + Workspace = workspace, + }; + + // Test publicly constructed non-schema reference + var responseRef = new OpenApiResponseReference("common.json", doc, "common.json"); + Assert.Equal("common.json#/components/responses/common.json", responseRef.Reference.ReferenceV3); + var resultResponse = doc.ResolveReferenceTo(responseRef.Reference, null); + Assert.Same(targetResponse, resultResponse); + Assert.Same(targetResponse, responseRef.Target); + + var paramRef = new OpenApiParameterReference("common.json", doc, "common.json"); + Assert.Equal("common.json#/components/parameters/common.json", paramRef.Reference.ReferenceV3); + var resultParam = doc.ResolveReferenceTo(paramRef.Reference, null); + Assert.Same(targetParam, resultParam); + Assert.Same(targetParam, paramRef.Target); + } + [Fact] public void OpenApiDocumentCopyConstructorPreservesPathsExtensions() { @@ -2848,6 +2879,5 @@ public void OpenApiDocumentCopyConstructorPreservesPathsExtensions() Assert.Single(document.Paths.Extensions); Assert.False(document.Paths.Extensions.ContainsKey("x-isolated")); } - } } }