diff --git a/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs b/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs index 86377b8b2..675031fde 100644 --- a/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs +++ b/src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs @@ -300,7 +300,11 @@ protected virtual void SetAdditional31MetadataFromMapNode(JsonObject jsonObject) internal void SetJsonPointerPath(string pointer, string nodeLocation) { // Relative reference to internal JSON schema node/resource (e.g. "#/properties/b") - if (pointer.StartsWith("#/", StringComparison.OrdinalIgnoreCase) && !pointer.Contains("/components/schemas")) +#if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER || NET5_0_OR_GREATER + if (pointer.StartsWith("#/", StringComparison.OrdinalIgnoreCase) && !pointer.Contains("/components/schemas", StringComparison.OrdinalIgnoreCase)) +#else + if (pointer.StartsWith("#/", StringComparison.OrdinalIgnoreCase) && !pointer.ToLowerInvariant().Contains("/components/schemas")) +#endif { ReferenceV3 = ResolveRelativePointer(nodeLocation, pointer); } @@ -316,23 +320,28 @@ internal void SetJsonPointerPath(string pointer, string nodeLocation) private static string ResolveRelativePointer(string nodeLocation, string relativeRef) { // Convert nodeLocation to path segments - var segments = nodeLocation.TrimStart('#').Split(['/'], StringSplitOptions.RemoveEmptyEntries).ToList(); + var nodeLocationSegments = nodeLocation.TrimStart('#').Split(['/'], StringSplitOptions.RemoveEmptyEntries).ToList(); // Convert relativeRef to dynamic segments var relativeSegments = relativeRef.TrimStart('#').Split(['/'], StringSplitOptions.RemoveEmptyEntries); // Locate the first occurrence of relativeRef segments in the full path - for (int i = 0; i <= segments.Count - relativeSegments.Length; i++) + for (int i = 0; i <= nodeLocationSegments.Count - relativeSegments.Length; i++) { - if (relativeSegments.SequenceEqual(segments.Skip(i).Take(relativeSegments.Length))) + if (relativeSegments.SequenceEqual(nodeLocationSegments.Skip(i).Take(relativeSegments.Length), StringComparer.Ordinal) && + nodeLocationSegments.Take(i + relativeSegments.Length).ToArray() is {Length: > 0} matchingSegments) { // Trim to include just the matching segment chain - segments = [.. segments.Take(i + relativeSegments.Length)]; - break; + return $"#/{string.Join("/", matchingSegments)}"; } } - return $"#/{string.Join("/", segments)}"; + // Fallback on building a full path +#if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER || NET5_0_OR_GREATER + return $"#/{string.Join("/", nodeLocationSegments.SkipLast(relativeSegments.Length).Union(relativeSegments))}"; +#else + return $"#/{string.Join("/", nodeLocationSegments.Take(nodeLocationSegments.Count - relativeSegments.Length).Union(relativeSegments))}"; +#endif } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/relativeSubschemaReference.json b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/relativeSubschemaReference.json new file mode 100644 index 000000000..6883dd5a6 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/relativeSubschemaReference.json @@ -0,0 +1,58 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Relative reference in a subschema of an component schema", + "version": "1.0.0" + }, + "paths": { + "/items": { + "get": { + "responses": { + "200": { + "description": "ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Foo" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Foo": { + "type": "object", + "properties": { + "seq1": { + "type": [ + "array", + "null" + ], + "items": { + "type": "array", + "items": { + "type": "string", + "format": null, + "x-schema-id": null + } + } + }, + "seq2": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/properties/seq1/items" + } + } + }, + "x-schema-id": "ContainerType" + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs index c64b9594f..455904ce8 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs @@ -279,5 +279,22 @@ public void ResolveSubSchema_ShouldRecurseIntoAllOfComposition() Assert.NotNull(result); Assert.Equal(JsonSchemaType.Integer, result!.Type); } + [Fact] + public async Task SHouldResolveRelativeSubReference() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "relativeSubschemaReference.json"); + + // Act + var (actual, _) = await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings); + + var fooComponentSchema = actual.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); + } } }