diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index dcbcde3cb..abf6a0748 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -575,9 +575,9 @@ 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('/')) // this means its a URL reference { - uriLocation = id; + uriLocation = id!; } else { @@ -609,12 +609,24 @@ private static string ConvertByteArrayToString(byte[] hash) : BaseUri + relativePath; } - if (reference.Type is ReferenceType.Schema && uriLocation.Contains('#')) + var absoluteUri = +#if NETSTANDARD2_1 || NETCOREAPP || NET5_0_OR_GREATER + uriLocation.StartsWith('#') +#else + uriLocation.StartsWith("#", StringComparison.OrdinalIgnoreCase) +#endif + switch + { + true => new Uri(BaseUri, uriLocation).AbsoluteUri, + false => new Uri(uriLocation).AbsoluteUri + }; + + if (reference.Type is ReferenceType.Schema && absoluteUri.Contains('#')) { - return Workspace?.ResolveJsonSchemaReference(new Uri(uriLocation).AbsoluteUri); + return Workspace?.ResolveJsonSchemaReference(absoluteUri); } - return Workspace?.ResolveReference(new Uri(uriLocation).AbsoluteUri); + return Workspace?.ResolveReference(absoluteUri); } private static bool IsSubComponent(string reference) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 8fd0d439e..65fccb15a 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -340,29 +340,30 @@ public bool Contains(string location) * #/components/schemas/human/allOf/0 */ - if (string.IsNullOrEmpty(location)) return default; + if (string.IsNullOrEmpty(location) || ToLocationUrl(location) is not Uri uri) return default; - var uri = ToLocationUrl(location); - string[] pathSegments; +#if NETSTANDARD2_1 || NETCOREAPP || NET5_0_OR_GREATER + if (!location.Contains("#/components/schemas/", StringComparison.OrdinalIgnoreCase)) +#else + if (!location.Contains("#/components/schemas/")) +#endif + throw new ArgumentException($"Invalid schema reference location: {location}. It should contain '#/components/schemas/'"); - if (uri is not null) - { - pathSegments = uri.Fragment.Split(['/'], StringSplitOptions.RemoveEmptyEntries); + var pathSegments = uri.Fragment.Split(['/'], StringSplitOptions.RemoveEmptyEntries); - // Build the base path for the root schema: "#/components/schemas/person" - var fragment = OpenApiConstants.ComponentsSegment + ReferenceType.Schema.GetDisplayName() + ComponentSegmentSeparator + pathSegments[3]; - var uriBuilder = new UriBuilder(uri) - { - Fragment = fragment - }; // to avoid escaping the # character in the resulting Uri + // Build the base path for the root schema: "#/components/schemas/person" + var fragment = OpenApiConstants.ComponentsSegment + ReferenceType.Schema.GetDisplayName() + ComponentSegmentSeparator + pathSegments[3]; + var uriBuilder = new UriBuilder(uri) + { + Fragment = fragment + }; // to avoid escaping the # character in the resulting Uri - if (_IOpenApiReferenceableRegistry.TryGetValue(uriBuilder.Uri, out var schema) && schema is IOpenApiSchema targetSchema) - { - // traverse remaining segments after fetching the base schema - var remainingSegments = pathSegments.Skip(4).ToArray(); - return ResolveSubSchema(targetSchema, remainingSegments); - } - } + if (_IOpenApiReferenceableRegistry.TryGetValue(uriBuilder.Uri, out var schema) && schema is IOpenApiSchema targetSchema) + { + // traverse remaining segments after fetching the base schema + var remainingSegments = pathSegments.Skip(4).ToArray(); + return ResolveSubSchema(targetSchema, remainingSegments); + } return default; } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs index 83c89737c..fd4980b1a 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Text.Json.Nodes; @@ -297,6 +298,99 @@ public async Task ShouldResolveRelativeSubReference() Assert.Equal(JsonSchemaType.String, seq2Property.Items.Items.Type); } [Fact] + public async Task ShouldResolveRelativeSubReferenceUsingParsingContext() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "relativeSubschemaReference.json"); + using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); + var jsonNode = await JsonNode.ParseAsync(fs); + var schemaJsonNode = jsonNode["components"]?["schemas"]?["Foo"]; + Assert.NotNull(schemaJsonNode); + var diagnostic = new OpenApiDiagnostic(); + var parsingContext = new ParsingContext(diagnostic); + parsingContext.StartObject("components"); + parsingContext.StartObject("schemas"); + parsingContext.StartObject("Foo"); + var document = new OpenApiDocument(); + + // Act + var fooComponentSchema = parsingContext.ParseFragment(schemaJsonNode, OpenApiSpecVersion.OpenApi3_1, document); + document.AddComponent("Foo", fooComponentSchema); + var seq1Property = fooComponentSchema.Properties["seq1"]; + Assert.NotNull(seq1Property); + var seq2Property = fooComponentSchema.Properties["seq2"]; + Assert.NotNull(seq2Property); + Assert.Equal(JsonSchemaType.Array, seq2Property.Items.Type); + Assert.Equal(JsonSchemaType.String, seq2Property.Items.Items.Type); + } + [Fact] + public void ShouldFailToResolveRelativeSubReferenceFromTheObjectModel() + { + var document = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" }, + }; + document.Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["Foo"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["seq1"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchema { Type = JsonSchemaType.Array, Items = new OpenApiSchema { Type = JsonSchemaType.String } } }, + ["seq2"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchemaReference("#/properties/seq1/items", document) } + } + } + } + }; + document.RegisterComponents(); + + var fooComponentSchema = document.Components.Schemas["Foo"]; + var seq1Property = fooComponentSchema.Properties["seq1"]; + Assert.NotNull(seq1Property); + var seq2Property = fooComponentSchema.Properties["seq2"]; + Assert.NotNull(seq2Property); + Assert.Throws(() => seq2Property.Items.Type); + // it's impossible to resolve relative references from the object model only because we don't have a way to get to + // the parent object to build the full path for the reference. + + + // #/properties/seq1/items + // #/components/schemas/Foo/properties/seq1/items + } + [Fact] + public void ShouldResolveAbsoluteSubReferenceFromTheObjectModel() + { + var document = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" }, + }; + document.Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["Foo"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["seq1"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchema { Type = JsonSchemaType.Array, Items = new OpenApiSchema { Type = JsonSchemaType.String } } }, + ["seq2"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchemaReference("#/components/schemas/Foo/properties/seq1/items", document) } + } + } + } + }; + document.RegisterComponents(); + + var fooComponentSchema = document.Components.Schemas["Foo"]; + var seq1Property = fooComponentSchema.Properties["seq1"]; + Assert.NotNull(seq1Property); + var seq2Property = fooComponentSchema.Properties["seq2"]; + Assert.NotNull(seq2Property); + Assert.Equal(JsonSchemaType.Array, seq2Property.Items.Type); + Assert.Equal(JsonSchemaType.String, seq2Property.Items.Items.Type); + } + [Fact] public async Task ShouldResolveRecursiveRelativeSubReference() { // Arrange