From a01f21d4f05df8d0511d54730dff92e2f292062c Mon Sep 17 00:00:00 2001 From: Maggiekimani1 Date: Wed, 7 May 2025 17:30:53 +0300 Subject: [PATCH 01/10] feat: register schemas with an identifier --- src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs | 10 ++++++++++ .../Reader/V31/OpenApiSchemaDeserializer.cs | 9 ++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index ae7adefa6..9dd081b17 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -156,6 +156,16 @@ public override string GetRaw() return refNode?.GetScalarValue(); } + public string? GetJsonSchemaIdentifier() + { + if (!_node.TryGetPropertyValue("$id", out JsonNode? idNode)) + { + return null; + } + + return idNode?.GetScalarValue(); + } + public string? GetSummaryValue() { if (!_node.TryGetPropertyValue("summary", out JsonNode? summaryNode)) diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs index 71265aa8c..21d95e715 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.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.Extensions; @@ -363,6 +363,7 @@ public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocu var mapNode = node.CheckMapNode(OpenApiConstants.Schema); var pointer = mapNode.GetReferencePointer(); + var identifier = mapNode.GetJsonSchemaIdentifier(); if (pointer != null) { @@ -397,6 +398,12 @@ public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocu schema.Extensions.Remove(OpenApiConstants.NullableExtension); } + if (identifier is not null && hostDocument.Workspace is not null) + { + // register the schema in our registry using the identifer's URL + hostDocument.Workspace.RegisterComponentForDocument(hostDocument, schema, identifier); + } + return schema; } } From e59fbe6964c7a0efbb43d72f436f07225b386f81 Mon Sep 17 00:00:00 2001 From: Maggiekimani1 Date: Tue, 13 May 2025 11:15:21 +0300 Subject: [PATCH 02/10] chore: add test cases for references in OpenApi docs --- .../ReferenceSamples/OAS-schemas.yaml | 18 ++ .../componentExternalReference.yaml | 13 ++ .../ReferenceSamples/customApiKey.yaml | 11 + .../V31Tests/ReferenceSamples/examples.yaml | 11 + .../externalComponentSubschemaReference.yaml | 14 ++ .../inlineExternalReference.yaml | 15 ++ .../inlineLocalReference.yaml | 14 ++ .../internalComponentReferenceUsingId.yaml | 29 +++ .../internalComponentsSubschemaReference.yaml | 37 ++++ .../localReferenceToJsonSchemaResource.yaml | 23 ++ .../rootComponentSchemaReference.yaml | 24 ++ .../rootInlineSchemaReference.yaml | 18 ++ .../subschemaComponentSchemaReference.yaml | 22 ++ .../subschemaInlineSchemaReference.yaml | 20 ++ .../V31Tests/RelativeReferenceTests.cs | 205 ++++++++++++++++++ 15 files changed, 474 insertions(+) create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/OAS-schemas.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/componentExternalReference.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/customApiKey.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/examples.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/externalComponentSubschemaReference.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/inlineExternalReference.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/inlineLocalReference.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentReferenceUsingId.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentsSubschemaReference.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/localReferenceToJsonSchemaResource.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootComponentSchemaReference.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootInlineSchemaReference.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/subschemaComponentSchemaReference.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/subschemaInlineSchemaReference.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/OAS-schemas.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/OAS-schemas.yaml new file mode 100644 index 000000000..dcaabab1c --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/OAS-schemas.yaml @@ -0,0 +1,18 @@ +openapi: 3.1.0 +info: + title: OpenAPI document containing reusable components + version: 1.0.0 +components: + schemas: + person: + type: object + properties: + name: + type: string + address: + type: object + properties: + street: + type: string + city: + type: string diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/componentExternalReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/componentExternalReference.yaml new file mode 100644 index 000000000..407927cee --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/componentExternalReference.yaml @@ -0,0 +1,13 @@ +openapi: 3.1.0 +info: + title: Example of reference object in a component object + version: 1.0.0 +paths: + /item: + get: + security: + - customapikey: [] +components: + securitySchemes: + customapikey: + $ref: ./customApiKey.yaml#/components/securityschemes/customapikey \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/customApiKey.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/customApiKey.yaml new file mode 100644 index 000000000..4c2ab5107 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/customApiKey.yaml @@ -0,0 +1,11 @@ +openapi: 3.1.0 +info: + title: Example of reference object pointing to a parameter + version: 1.0.0 +paths: {} +components: + securitySchemes: + customapikey: + type: apiKey + name: x-api-key + in: header \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/examples.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/examples.yaml new file mode 100644 index 000000000..831bccdf2 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/examples.yaml @@ -0,0 +1,11 @@ +# file for examples (examples.yaml) +openapi: 3.1.0 +info: + title: OpenAPI document containing examples for reuse + version: 1.0.0 +components: + examples: + item-list: + value: + - name: thing + description: a thing \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/externalComponentSubschemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/externalComponentSubschemaReference.yaml new file mode 100644 index 000000000..3627afcdb --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/externalComponentSubschemaReference.yaml @@ -0,0 +1,14 @@ +openapi: 3.1.0 +info: + title: Reference to an external OpenApi document component + version: 1.0.0 +paths: + /person/{id}: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: 'OAS-schemas.yaml#/components/schemas/person/properties/address' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/inlineExternalReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/inlineExternalReference.yaml new file mode 100644 index 000000000..d05adb724 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/inlineExternalReference.yaml @@ -0,0 +1,15 @@ +openapi: 3.1.0 +info: + title: Example of reference object pointing to an example object in an OpenAPI document + version: 1.0.0 +paths: + /items: + get: + responses: + '200': + description: sample description + content: + application/json: + examples: + item-list: + $ref: './examples.yaml#/components/examples/item-list' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/inlineLocalReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/inlineLocalReference.yaml new file mode 100644 index 000000000..25241d100 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/inlineLocalReference.yaml @@ -0,0 +1,14 @@ +openapi: 3.1.0 +info: + title: Example of reference object pointing to a parameter + version: 1.0.0 +paths: + /item: + get: + parameters: + - $ref: '#/components/parameters/size' +components: + parameters: + size: + schema: + type: number \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentReferenceUsingId.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentReferenceUsingId.yaml new file mode 100644 index 000000000..ed2e4f91c --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentReferenceUsingId.yaml @@ -0,0 +1,29 @@ +openapi: 3.1.0 +info: + title: Reference an internal component using id + version: 1.0.0 +paths: + /person/{id}: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: 'https://schemas.acme.org/person' +components: + schemas: + person: + $id: 'https://schemas.acme.org/person' + type: object + properties: + name: + type: string + address: + type: object + properties: + street: + type: string + city: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentsSubschemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentsSubschemaReference.yaml new file mode 100644 index 000000000..a18a1b54f --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentsSubschemaReference.yaml @@ -0,0 +1,37 @@ +openapi: 3.1.0 +info: + title: Reference to an internal component + version: 1.0.0 +paths: + /person/{id}: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/person' + /person/{id}/address: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/person/properties/address' +components: + schemas: + person: + type: object + properties: + name: + type: string + address: + type: object + properties: + street: + type: string + city: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/localReferenceToJsonSchemaResource.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/localReferenceToJsonSchemaResource.yaml new file mode 100644 index 000000000..bef88ee90 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/localReferenceToJsonSchemaResource.yaml @@ -0,0 +1,23 @@ +openapi: 3.1.0 +info: + title: OpenAPI document containing examples for reuse + version: 1.0.0 +components: + schemas: + a: + type: + - object + - 'null' + properties: + b: + type: + - object + - 'null' + properties: + c: + type: + - object + - 'null' + properties: + b: + $ref: '#/properties/b' \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootComponentSchemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootComponentSchemaReference.yaml new file mode 100644 index 000000000..789b1abb2 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootComponentSchemaReference.yaml @@ -0,0 +1,24 @@ +openapi: 3.1.0 +info: + title: Reference at the root of a component schema + version: 1.0.0 +paths: + /items: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/specialitem' +components: + schemas: + specialitem: # Use the item type but provide a different title for the type + title: Special Item + $ref: "#/components/schemas/item" + item: + title: Item + type: object \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootInlineSchemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootInlineSchemaReference.yaml new file mode 100644 index 000000000..cd2843bb2 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootInlineSchemaReference.yaml @@ -0,0 +1,18 @@ +openapi: 3.1.0 +info: + title: Reference in at the root of an inline schema + version: 1.0.0 +paths: + /item: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/item' +components: + schemas: + item: + type: object \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/subschemaComponentSchemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/subschemaComponentSchemaReference.yaml new file mode 100644 index 000000000..80dbc2f34 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/subschemaComponentSchemaReference.yaml @@ -0,0 +1,22 @@ +openapi: 3.1.0 +info: + title: 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/items' +components: + schemas: + items: + type: array + items: + $ref: '#/components/schemas/item' + item: + type: object \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/subschemaInlineSchemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/subschemaInlineSchemaReference.yaml new file mode 100644 index 000000000..cb72b0c66 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/subschemaInlineSchemaReference.yaml @@ -0,0 +1,20 @@ +openapi: 3.1.0 +info: + title: Reference in at the root of an inline schema + version: 1.0.0 +paths: + /items: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/item' +components: + schemas: + item: + type: object \ 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 new file mode 100644 index 000000000..daab61773 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs @@ -0,0 +1,205 @@ +using System.IO; +using System.Net.Http; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Writers; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V31Tests +{ + public class RelativeReferenceTests + { + private const string SampleFolderPath = "V31Tests/ReferenceSamples"; + + [Fact] + public async Task ParseInlineLocalReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "inlineLocalReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schemaType = actual.Paths["/item"].Operations[HttpMethod.Get].Parameters[0].Schema.Type; + + // Assert + Assert.Equal(JsonSchemaType.Number, schemaType); + } + + [Fact] + public async Task ParseInlineExternalReferenceWorks() + { + // Arrange + var expected = new JsonArray + { + new JsonObject + { + ["name"] = "thing", + ["description"] = "a thing" + } + }; + + var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath); + + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + BaseUrl = new(path), + }; + settings.AddYamlReader(); + + // Act + var actual = (await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "inlineExternalReference.yaml"), settings)).Document; + var exampleValue = actual.Paths["/items"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Examples["item-list"].Value; + + // Assert + Assert.NotNull(exampleValue); + Assert.IsType(exampleValue); + Assert.Equal(expected.ToJsonString(), exampleValue.ToJsonString()); + } + + [Fact] + public async Task ParseComponentExternalReferenceWorks() + { + // Arrange + var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath); + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + BaseUrl = new(path), + }; + settings.AddYamlReader(); + + // Act + var actual = (await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "componentExternalReference.yaml"), settings)).Document; + var securitySchemeValue = actual.Components.SecuritySchemes["customapikey"]; + + // Assert + Assert.Equal("x-api-key", securitySchemeValue.Name); + } + + [Fact] + public async Task ParseRootInlineJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "rootInlineSchemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Paths["/item"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseSubschemaInlineJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "subschemaInlineSchemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Paths["/items"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema.Items; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseRootComponentJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "rootComponentSchemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Components.Schemas["specialitem"]; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + Assert.Equal("Item", schema.Title); + } + + [Fact] + public async Task ParseSubschemaComponentJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "subschemaComponentSchemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Components.Schemas["items"].Items; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseInternalComponentSubschemaJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "internalComponentsSubschemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Paths["/person/{id}/address"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseExternalComponentSubschemaJsonSchemaReferenceWorks() + { + // Arrange + var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath); + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + BaseUrl = new(path), + }; + settings.AddYamlReader(); + + // Act + var actual = (await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "externalComponentSubschemaReference.yaml"), settings)).Document; + var schema = actual.Paths["/person/{id}"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseReferenceToInternalComponentUsingDollarIdWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "internalComponentReferenceUsingId.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Paths["/person/{id}"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseLocalReferenceToJsonSchemaResourceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "localReferenceToJsonSchemaResource.yaml"); + var stringWriter = new StringWriter(); + var writer = new OpenApiYamlWriter(stringWriter); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Components.Schemas["a"].Properties["b"].Properties["c"].Properties["b"]; + schema.SerializeAsV31(writer); + var content = stringWriter.ToString(); + + // Assert + Assert.Equal(JsonSchemaType.Object | JsonSchemaType.Null, schema.Type); + } + } +} From 0212484412c1941880489c5c593f7672051e3c7a Mon Sep 17 00:00:00 2001 From: Maggiekimani1 Date: Wed, 14 May 2025 11:15:25 +0300 Subject: [PATCH 03/10] chore: remove unimplemented test cases --- .../V31Tests/RelativeReferenceTests.cs | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs index daab61773..ebfb8d766 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs @@ -136,40 +136,6 @@ public async Task ParseSubschemaComponentJsonSchemaReferenceWorks() Assert.Equal(JsonSchemaType.Object, schema.Type); } - [Fact] - public async Task ParseInternalComponentSubschemaJsonSchemaReferenceWorks() - { - // Arrange - var filePath = Path.Combine(SampleFolderPath, "internalComponentsSubschemaReference.yaml"); - - // Act - var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; - var schema = actual.Paths["/person/{id}/address"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; - - // Assert - Assert.Equal(JsonSchemaType.Object, schema.Type); - } - - [Fact] - public async Task ParseExternalComponentSubschemaJsonSchemaReferenceWorks() - { - // Arrange - var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath); - var settings = new OpenApiReaderSettings - { - LoadExternalRefs = true, - BaseUrl = new(path), - }; - settings.AddYamlReader(); - - // Act - var actual = (await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "externalComponentSubschemaReference.yaml"), settings)).Document; - var schema = actual.Paths["/person/{id}"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; - - // Assert - Assert.Equal(JsonSchemaType.Object, schema.Type); - } - [Fact] public async Task ParseReferenceToInternalComponentUsingDollarIdWorks() { @@ -183,23 +149,5 @@ public async Task ParseReferenceToInternalComponentUsingDollarIdWorks() // Assert Assert.Equal(JsonSchemaType.Object, schema.Type); } - - [Fact] - public async Task ParseLocalReferenceToJsonSchemaResourceWorks() - { - // Arrange - var filePath = Path.Combine(SampleFolderPath, "localReferenceToJsonSchemaResource.yaml"); - var stringWriter = new StringWriter(); - var writer = new OpenApiYamlWriter(stringWriter); - - // Act - var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; - var schema = actual.Components.Schemas["a"].Properties["b"].Properties["c"].Properties["b"]; - schema.SerializeAsV31(writer); - var content = stringWriter.ToString(); - - // Assert - Assert.Equal(JsonSchemaType.Object | JsonSchemaType.Null, schema.Type); - } } } From 486ee2886888ec13c158f2c5370eded4c9c71559 Mon Sep 17 00:00:00 2001 From: Maggiekimani1 Date: Thu, 15 May 2025 18:53:30 +0300 Subject: [PATCH 04/10] chore: adds recursion for resolving nested subschemas --- .../Models/OpenApiDocument.cs | 44 +++++++++- .../Models/OpenApiReference.cs | 27 +++++- .../Reader/V31/OpenApiSchemaDeserializer.cs | 7 +- .../Services/OpenApiWorkspace.cs | 85 +++++++++++++++++++ .../internalComponentsSubschemaReference.yaml | 20 ++++- .../V31Tests/RelativeReferenceTests.cs | 54 ++++++++++++ .../PublicApi/PublicApi.approved.txt | 3 +- 7 files changed, 232 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index c5ac5f759..afd8da8d2 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -587,7 +587,26 @@ private static string ConvertByteArrayToString(byte[] hash) } else { - string relativePath = $"#{OpenApiConstants.ComponentsSegment}{reference.Type.GetDisplayName()}/{id}"; + string relativePath; + + if (!string.IsNullOrEmpty(reference.ReferenceV3) && IsSubComponent(reference.ReferenceV3!)) + { + // Enables setting the complete JSON path for nested subschemas e.g. #/components/schemas/person/properties/address + if (useExternal) + { + var relPathSegment = reference.ReferenceV3!.Split('#')[1]; + relativePath = $"#{relPathSegment}"; + } + else + { + relativePath = reference.ReferenceV3!; + } + } + else + { + relativePath = $"#{OpenApiConstants.ComponentsSegment}{reference.Type.GetDisplayName()}/{id}"; + } + Uri? externalResourceUri = useExternal ? Workspace?.GetDocumentId(reference.ExternalResource) : null; uriLocation = useExternal && externalResourceUri is not null @@ -595,9 +614,32 @@ private static string ConvertByteArrayToString(byte[] hash) : BaseUri + relativePath; } + if (reference.Type is ReferenceType.Schema && !uriLocation.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return Workspace?.ResolveJsonSchemaReference(new Uri(uriLocation).AbsoluteUri); + } + return Workspace?.ResolveReference(new Uri(uriLocation).AbsoluteUri); } + private static bool IsSubComponent(string reference) + { + // Normalize fragment part only + var parts = reference.Split('#'); + var fragment = parts.Length > 1 ? parts[1] : string.Empty; + + if (fragment.StartsWith("/components/schemas/", StringComparison.OrdinalIgnoreCase)) + { + var segments = fragment.Split('/'); + + // Expect exactly 4 segments for root-level schema: ["", "components", "schemas", "person"] + // Anything longer means it's a subcomponent. + return segments.Length > 4; + } + + return false; + } + /// /// Reads the stream input and parses it into an Open API document. /// diff --git a/src/Microsoft.OpenApi/Models/OpenApiReference.cs b/src/Microsoft.OpenApi/Models/OpenApiReference.cs index 0e05ec89d..0385ac151 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiReference.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiReference.cs @@ -73,6 +73,7 @@ public class OpenApiReference : IOpenApiSerializable, IOpenApiDescribedElement, /// public OpenApiDocument? HostDocument { get => hostDocument; init => hostDocument = value; } + private string? _referenceV3; /// /// Gets the full reference string for v3.0. /// @@ -80,9 +81,14 @@ public string? ReferenceV3 { get { + if (!string.IsNullOrEmpty(_referenceV3)) + { + return _referenceV3; + } + if (IsExternal) { - return GetExternalReferenceV3(); + return _referenceV3 = GetExternalReferenceV3(); } if (Type == ReferenceType.Tag) @@ -100,7 +106,14 @@ public string? ReferenceV3 return Id; } - return "#/components/" + Type.GetDisplayName() + "/" + Id; + return _referenceV3 = "#/components/" + Type.GetDisplayName() + "/" + Id; + } + set + { + if (value is not null) + { + _referenceV3 = value; + } } } @@ -299,5 +312,15 @@ internal void SetSummaryAndDescriptionFromMapNode(MapNode mapNode) Summary = summary; } } + + internal void SetJsonPointerPath(string pointer) + { + // Eg of an internal subcomponent's JSONPath: #/components/schemas/person/properties/address + if ((pointer.Contains("#") || pointer.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + && !string.IsNullOrEmpty(ReferenceV3) && !ReferenceV3!.Equals(pointer, StringComparison.OrdinalIgnoreCase)) + { + ReferenceV3 = pointer; + } + } } } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs index 21d95e715..02f4f14c1 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.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.Extensions; @@ -370,8 +370,9 @@ public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocu var reference = GetReferenceIdAndExternalResource(pointer); var result = new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2); result.Reference.SetSummaryAndDescriptionFromMapNode(mapNode); + result.Reference.SetJsonPointerPath(pointer); return result; - } + } var schema = new OpenApiSchema(); @@ -400,7 +401,7 @@ public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocu if (identifier is not null && hostDocument.Workspace is not null) { - // register the schema in our registry using the identifer's URL + // register the schema in our registry using the identifier's URL hostDocument.Workspace.RegisterComponentForDocument(hostDocument, schema, identifier); } diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 5519696d9..697a4cda9 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; @@ -330,6 +331,90 @@ public bool Contains(string location) return default; } + /// + /// Recursively resolves a schema from a URI fragment. + /// + /// + /// + public IOpenApiSchema? ResolveJsonSchemaReference(string location) + { + /* Enables resolving references for nested subschemas + * Examples: + * #/components/schemas/person/properties/address" + * #/components/schemas/human/allOf/0 + */ + + if (string.IsNullOrEmpty(location)) return default; + + var uri = ToLocationUrl(location); + string[] pathSegments; + + if (uri is not null) + { + 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 + + if (_IOpenApiReferenceableRegistry.TryGetValue(uriBuilder.Uri, out var schema) && schema is OpenApiSchema targetSchema) + { + // traverse remaining segments after fetching the base schema + var remainingSegments = pathSegments.Skip(4).ToArray(); + return ResolveSubSchema(targetSchema, remainingSegments); + } + } + + return default; + } + + private static IOpenApiSchema? ResolveSubSchema(IOpenApiSchema schema, string[] pathSegments) + { + // Traverse schema object to resolve subschemas + if (pathSegments.Length == 0) + { + return schema; + } + var currentSegment = pathSegments[0]; + pathSegments = [.. pathSegments.Skip(1)]; // skip one segment for the next recursive call + + switch (currentSegment) + { + case OpenApiConstants.Properties: + var propName = pathSegments[0]; + if (schema.Properties != null && schema.Properties.TryGetValue(propName, out var propSchema)) + return ResolveSubSchema(propSchema, [.. pathSegments.Skip(1)]); + break; + case OpenApiConstants.Items: + return schema.Items is OpenApiSchema itemsSchema ? ResolveSubSchema(itemsSchema, pathSegments) : null; + + case OpenApiConstants.AdditionalProperties: + return schema.AdditionalProperties is OpenApiSchema additionalSchema ? ResolveSubSchema(additionalSchema, pathSegments) : null; + case OpenApiConstants.AllOf: + case OpenApiConstants.AnyOf: + case OpenApiConstants.OneOf: + if (!int.TryParse(pathSegments[0], out var index)) return null; + + var list = currentSegment switch + { + OpenApiConstants.AllOf => schema.AllOf, + OpenApiConstants.AnyOf => schema.AnyOf, + OpenApiConstants.OneOf => schema.OneOf, + _ => null + }; + + // recurse into the indexed subschema if valid + if (list != null && index < list.Count) + return ResolveSubSchema(list[index], [.. pathSegments.Skip(1)]); + break; + } + + return null; + } + private Uri? ToLocationUrl(string location) { if (BaseUrl is not null) diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentsSubschemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentsSubschemaReference.yaml index a18a1b54f..fef558ab7 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentsSubschemaReference.yaml +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/internalComponentsSubschemaReference.yaml @@ -21,8 +21,24 @@ paths: application/json: schema: $ref: '#/components/schemas/person/properties/address' + /human: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/human/allOf/0' components: schemas: + human: + allOf: + - $ref: '#/components/schemas/person/items' + - type: object + properties: + name: + type: string person: type: object properties: @@ -34,4 +50,6 @@ components: street: type: string city: - type: string \ No newline at end of file + type: string + items: + type: integer \ 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 ebfb8d766..dcfebf660 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs @@ -136,6 +136,42 @@ public async Task ParseSubschemaComponentJsonSchemaReferenceWorks() Assert.Equal(JsonSchemaType.Object, schema.Type); } + [Fact] + public async Task ParseInternalComponentSubschemaJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "internalComponentsSubschemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var addressSchema = actual.Paths["/person/{id}/address"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + var itemsSchema = actual.Paths["/human"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + Assert.Equal(JsonSchemaType.Object, addressSchema.Type); + Assert.Equal(JsonSchemaType.Integer, itemsSchema.Type); + } + + [Fact] + public async Task ParseExternalComponentSubschemaJsonSchemaReferenceWorks() + { + // Arrange + var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath); + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + BaseUrl = new(path), + }; + settings.AddYamlReader(); + + // Act + var actual = (await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "externalComponentSubschemaReference.yaml"), settings)).Document; + var schema = actual.Paths["/person/{id}"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + [Fact] public async Task ParseReferenceToInternalComponentUsingDollarIdWorks() { @@ -149,5 +185,23 @@ public async Task ParseReferenceToInternalComponentUsingDollarIdWorks() // Assert Assert.Equal(JsonSchemaType.Object, schema.Type); } + + [Fact] + public async Task ParseLocalReferenceToJsonSchemaResourceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "localReferenceToJsonSchemaResource.yaml"); + var stringWriter = new StringWriter(); + var writer = new OpenApiYamlWriter(stringWriter); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Components.Schemas["a"].Properties["b"].Properties["c"].Properties["b"]; + schema.SerializeAsV31(writer); + var content = stringWriter.ToString(); + + // Assert + Assert.Equal(JsonSchemaType.Object | JsonSchemaType.Null, schema.Type); + } } } diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 1e3c88ec0..55bc8dc0c 100644 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -965,7 +965,7 @@ namespace Microsoft.OpenApi.Models public bool IsFragment { get; init; } public bool IsLocal { get; } public string? ReferenceV2 { get; } - public string? ReferenceV3 { get; } + public string? ReferenceV3 { get; set; } public string? Summary { get; set; } public Microsoft.OpenApi.Models.ReferenceType Type { get; init; } public void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } @@ -1650,6 +1650,7 @@ namespace Microsoft.OpenApi.Services public System.Uri? GetDocumentId(string? key) { } public bool RegisterComponentForDocument(Microsoft.OpenApi.Models.OpenApiDocument openApiDocument, T componentToRegister, string id) { } public void RegisterComponents(Microsoft.OpenApi.Models.OpenApiDocument document) { } + public Microsoft.OpenApi.Models.Interfaces.IOpenApiSchema? ResolveJsonSchemaReference(string location) { } public T? ResolveReference(string location) { } } public class OperationSearch : Microsoft.OpenApi.Services.OpenApiVisitorBase From 42a5a2d8310c1cb5f58fe882eaf34603d87a54ca Mon Sep 17 00:00:00 2001 From: Maggiekimani1 Date: Thu, 15 May 2025 18:58:15 +0300 Subject: [PATCH 05/10] chore: remove unused variable --- .../V31Tests/RelativeReferenceTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs index dcfebf660..053094f50 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs @@ -198,7 +198,6 @@ public async Task ParseLocalReferenceToJsonSchemaResourceWorks() var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; var schema = actual.Components.Schemas["a"].Properties["b"].Properties["c"].Properties["b"]; schema.SerializeAsV31(writer); - var content = stringWriter.ToString(); // Assert Assert.Equal(JsonSchemaType.Object | JsonSchemaType.Null, schema.Type); From cd4d41125bbc95e5ce83bbeeda594b095bf6be8f Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Mon, 19 May 2025 12:43:32 +0300 Subject: [PATCH 06/10] Update src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs Co-authored-by: Vincent Biret --- src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs index 02f4f14c1..3653fd54c 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs @@ -399,7 +399,7 @@ public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocu schema.Extensions.Remove(OpenApiConstants.NullableExtension); } - if (identifier is not null && hostDocument.Workspace is not null) + if (!string.IsNullOrEmpty(identifier) && hostDocument.Workspace is not null) { // register the schema in our registry using the identifier's URL hostDocument.Workspace.RegisterComponentForDocument(hostDocument, schema, identifier); From a6e254602904221c3753d4ee45b9506e5906b4cf Mon Sep 17 00:00:00 2001 From: Maggiekimani1 Date: Mon, 19 May 2025 15:47:18 +0300 Subject: [PATCH 07/10] chore: implement PR feedback --- src/Microsoft.OpenApi/Models/OpenApiDocument.cs | 15 ++++++++------- src/Microsoft.OpenApi/Models/OpenApiReference.cs | 8 ++++---- .../Services/OpenApiWorkspace.cs | 4 ++-- .../rootComponentSchemaReference.yaml | 2 +- .../PublicApi/PublicApi.approved.txt | 3 +-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index afd8da8d2..3bdec0146 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -588,18 +588,19 @@ private static string ConvertByteArrayToString(byte[] hash) else { string relativePath; + var referenceV3 = !string.IsNullOrEmpty(reference.ReferenceV3) ? reference.ReferenceV3! : string.Empty; - if (!string.IsNullOrEmpty(reference.ReferenceV3) && IsSubComponent(reference.ReferenceV3!)) + if (!string.IsNullOrEmpty(referenceV3) && IsSubComponent(referenceV3)) { // Enables setting the complete JSON path for nested subschemas e.g. #/components/schemas/person/properties/address if (useExternal) { - var relPathSegment = reference.ReferenceV3!.Split('#')[1]; + var relPathSegment = referenceV3.Split(['#'], StringSplitOptions.RemoveEmptyEntries)[1]; relativePath = $"#{relPathSegment}"; } else { - relativePath = reference.ReferenceV3!; + relativePath = referenceV3; } } else @@ -614,7 +615,7 @@ private static string ConvertByteArrayToString(byte[] hash) : BaseUri + relativePath; } - if (reference.Type is ReferenceType.Schema && !uriLocation.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + if (reference.Type is ReferenceType.Schema && uriLocation.Contains('#')) { return Workspace?.ResolveJsonSchemaReference(new Uri(uriLocation).AbsoluteUri); } @@ -630,11 +631,11 @@ private static bool IsSubComponent(string reference) if (fragment.StartsWith("/components/schemas/", StringComparison.OrdinalIgnoreCase)) { - var segments = fragment.Split('/'); + var segments = fragment.Split(['/'], StringSplitOptions.RemoveEmptyEntries); - // Expect exactly 4 segments for root-level schema: ["", "components", "schemas", "person"] + // Expect exactly 3 segments for root-level schema: ["components", "schemas", "person"] // Anything longer means it's a subcomponent. - return segments.Length > 4; + return segments.Length > 3; } return false; diff --git a/src/Microsoft.OpenApi/Models/OpenApiReference.cs b/src/Microsoft.OpenApi/Models/OpenApiReference.cs index 0385ac151..accf6624c 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiReference.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiReference.cs @@ -106,9 +106,9 @@ public string? ReferenceV3 return Id; } - return _referenceV3 = "#/components/" + Type.GetDisplayName() + "/" + Id; + return _referenceV3 = $"#/components/{Type.GetDisplayName()}/{Id}"; } - set + private set { if (value is not null) { @@ -139,7 +139,7 @@ public string? ReferenceV2 return Id; } - return "#/" + GetReferenceTypeNameAsV2(Type) + "/" + Id; + return $"#/{GetReferenceTypeNameAsV2(Type)}/{Id}"; } } @@ -316,7 +316,7 @@ internal void SetSummaryAndDescriptionFromMapNode(MapNode mapNode) internal void SetJsonPointerPath(string pointer) { // Eg of an internal subcomponent's JSONPath: #/components/schemas/person/properties/address - if ((pointer.Contains("#") || pointer.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + if ((pointer.Contains('#') || pointer.StartsWith("http", StringComparison.OrdinalIgnoreCase)) && !string.IsNullOrEmpty(ReferenceV3) && !ReferenceV3!.Equals(pointer, StringComparison.OrdinalIgnoreCase)) { ReferenceV3 = pointer; diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 697a4cda9..0345d7b63 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -336,7 +336,7 @@ public bool Contains(string location) /// /// /// - public IOpenApiSchema? ResolveJsonSchemaReference(string location) + internal IOpenApiSchema? ResolveJsonSchemaReference(string location) { /* Enables resolving references for nested subschemas * Examples: @@ -360,7 +360,7 @@ public bool Contains(string location) Fragment = fragment }; // to avoid escaping the # character in the resulting Uri - if (_IOpenApiReferenceableRegistry.TryGetValue(uriBuilder.Uri, out var schema) && schema is OpenApiSchema targetSchema) + 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(); diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootComponentSchemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootComponentSchemaReference.yaml index 789b1abb2..9eb4ca38a 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootComponentSchemaReference.yaml +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootComponentSchemaReference.yaml @@ -16,7 +16,7 @@ paths: $ref: '#/components/schemas/specialitem' components: schemas: - specialitem: # Use the item type but provide a different title for the type + specialitem: title: Special Item $ref: "#/components/schemas/item" item: diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 55bc8dc0c..1e3c88ec0 100644 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -965,7 +965,7 @@ namespace Microsoft.OpenApi.Models public bool IsFragment { get; init; } public bool IsLocal { get; } public string? ReferenceV2 { get; } - public string? ReferenceV3 { get; set; } + public string? ReferenceV3 { get; } public string? Summary { get; set; } public Microsoft.OpenApi.Models.ReferenceType Type { get; init; } public void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } @@ -1650,7 +1650,6 @@ namespace Microsoft.OpenApi.Services public System.Uri? GetDocumentId(string? key) { } public bool RegisterComponentForDocument(Microsoft.OpenApi.Models.OpenApiDocument openApiDocument, T componentToRegister, string id) { } public void RegisterComponents(Microsoft.OpenApi.Models.OpenApiDocument document) { } - public Microsoft.OpenApi.Models.Interfaces.IOpenApiSchema? ResolveJsonSchemaReference(string location) { } public T? ResolveReference(string location) { } } public class OperationSearch : Microsoft.OpenApi.Services.OpenApiVisitorBase From d27a4fcb9fdc43aa63f92d8b08f3562ee91a3963 Mon Sep 17 00:00:00 2001 From: Maggiekimani1 Date: Wed, 21 May 2025 16:32:42 +0300 Subject: [PATCH 08/10] feat: resolve JSON pointer path for a relative reference --- .../Models/OpenApiReference.cs | 36 ++++++++++++++++--- .../Reader/V31/OpenApiSchemaDeserializer.cs | 3 +- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiReference.cs b/src/Microsoft.OpenApi/Models/OpenApiReference.cs index a79c92081..6ebf75342 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiReference.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiReference.cs @@ -314,14 +314,42 @@ internal void SetSummaryAndDescriptionFromMapNode(MapNode mapNode) } } - internal void SetJsonPointerPath(string pointer) + internal void SetJsonPointerPath(string pointer, string nodeLocation) { - // Eg of an internal subcomponent's JSONPath: #/components/schemas/person/properties/address - if ((pointer.Contains('#') || pointer.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - && !string.IsNullOrEmpty(ReferenceV3) && !ReferenceV3!.Equals(pointer, StringComparison.OrdinalIgnoreCase)) + // Relative reference to internal JSON schema node/resource (e.g. "#/properties/b") + if (pointer.StartsWith("#/", StringComparison.OrdinalIgnoreCase) && !pointer.Contains("/components/schemas")) + { + 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)) { ReferenceV3 = pointer; + } + } + + private static string ResolveRelativePointer(string nodeLocation, string relativeRef) + { + // Convert nodeLocation to path segments + var segments = 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++) + { + if (relativeSegments.SequenceEqual(segments.Skip(i).Take(relativeSegments.Length))) + { + // Trim to include just the matching segment chain + segments = [.. segments.Take(i + relativeSegments.Length)]; + break; + } } + + return $"#/{string.Join("/", segments)}"; } } } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs index 3499992ad..e02d33287 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs @@ -367,13 +367,14 @@ public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocu var pointer = mapNode.GetReferencePointer(); var identifier = mapNode.GetJsonSchemaIdentifier(); + var nodeLocation = node.Context.GetLocation(); if (pointer != null) { var reference = GetReferenceIdAndExternalResource(pointer); var result = new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2); result.Reference.SetSummaryAndDescriptionFromMapNode(mapNode); - result.Reference.SetJsonPointerPath(pointer); + result.Reference.SetJsonPointerPath(pointer, nodeLocation); return result; } From e0e3095d6049650f3ad5d722153783ba04e1e895 Mon Sep 17 00:00:00 2001 From: Maggiekimani1 Date: Fri, 23 May 2025 11:51:15 +0300 Subject: [PATCH 09/10] chore: implement review feedback --- .../Models/OpenApiReference.cs | 4 +- .../Services/OpenApiWorkspace.cs | 2 +- .../rootComponentSchemaReference.yaml | 1 - .../V31Tests/RelativeReferenceTests.cs | 83 ++++++++++++++++++- 4 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiReference.cs b/src/Microsoft.OpenApi/Models/OpenApiReference.cs index 6ebf75342..d3810801b 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiReference.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiReference.cs @@ -89,7 +89,7 @@ public string? ReferenceV3 if (IsExternal) { - return _referenceV3 = GetExternalReferenceV3(); + return GetExternalReferenceV3(); } if (Type == ReferenceType.Tag) @@ -107,7 +107,7 @@ public string? ReferenceV3 return Id; } - return _referenceV3 = $"#/components/{Type.GetDisplayName()}/{Id}"; + return $"#/components/{Type.GetDisplayName()}/{Id}"; } private set { diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 1396b14dd..548d3a178 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -371,7 +371,7 @@ public bool Contains(string location) return default; } - private static IOpenApiSchema? ResolveSubSchema(IOpenApiSchema schema, string[] pathSegments) + internal static IOpenApiSchema? ResolveSubSchema(IOpenApiSchema schema, string[] pathSegments) { // Traverse schema object to resolve subschemas if (pathSegments.Length == 0) diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootComponentSchemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootComponentSchemaReference.yaml index 9eb4ca38a..b7fdfdbb0 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootComponentSchemaReference.yaml +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/ReferenceSamples/rootComponentSchemaReference.yaml @@ -17,7 +17,6 @@ paths: components: schemas: specialitem: - title: Special Item $ref: "#/components/schemas/item" item: title: Item diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs index 053094f50..4547aa60a 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs @@ -1,9 +1,12 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; using System.Net.Http; using System.Text.Json.Nodes; using System.Threading.Tasks; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models.Interfaces; using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Services; using Microsoft.OpenApi.Writers; using Xunit; @@ -202,5 +205,83 @@ public async Task ParseLocalReferenceToJsonSchemaResourceWorks() // Assert Assert.Equal(JsonSchemaType.Object | JsonSchemaType.Null, schema.Type); } + + [Fact] + public void ResolveSubSchema_ShouldTraverseKnownKeywords() + { + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary + { + ["a"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["b"] = new OpenApiSchema { Type = JsonSchemaType.String } + } + } + } + }; + + var path = new[] { "properties", "a", "properties", "b" }; + + var result = OpenApiWorkspace.ResolveSubSchema(schema, path); + + Assert.NotNull(result); + Assert.Equal(JsonSchemaType.String, result!.Type); + } + + public static IEnumerable SubSchemaKeywordPropertyPaths => + [ + [new[] { "properties", "properties" }], + [new[] { "properties", "allOf" }] + ]; + + + [Theory] + [MemberData(nameof(SubSchemaKeywordPropertyPaths))] + public void ResolveSubSchema_ShouldHandleUserDefinedKeywordNamedProperty(string[] pathSegments) + { + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary + { + ["properties"] = new OpenApiSchema { Type = JsonSchemaType.String }, + ["allOf"] = new OpenApiSchema { Type = JsonSchemaType.String } + } + }; + + var result = OpenApiWorkspace.ResolveSubSchema(schema, pathSegments); + + Assert.NotNull(result); + Assert.Equal(JsonSchemaType.String, result!.Type); + } + + [Fact] + public void ResolveSubSchema_ShouldRecurseIntoAllOfComposition() + { + var schema = new OpenApiSchema + { + AllOf = + [ + new OpenApiSchema + { + Properties = new Dictionary + { + ["x"] = new OpenApiSchema { Type = JsonSchemaType.Integer } + } + } + ] + }; + + var path = new[] { "allOf", "0", "properties", "x" }; + + var result = OpenApiWorkspace.ResolveSubSchema(schema, path); + + Assert.NotNull(result); + Assert.Equal(JsonSchemaType.Integer, result!.Type); + } } } From e39eeccf9f8a047d49c31309af0fc71e125ae6c4 Mon Sep 17 00:00:00 2001 From: Maggiekimani1 Date: Wed, 28 May 2025 02:24:38 +0300 Subject: [PATCH 10/10] chore: add and clean up deprecated namespaces --- src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs | 1 + .../V31Tests/RelativeReferenceTests.cs | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index c53236898..8fd0d439e 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; namespace Microsoft.OpenApi { diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs index 4547aa60a..c64b9594f 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs @@ -3,11 +3,7 @@ using System.Net.Http; using System.Text.Json.Nodes; using System.Threading.Tasks; -using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Models.Interfaces; using Microsoft.OpenApi.Reader; -using Microsoft.OpenApi.Services; -using Microsoft.OpenApi.Writers; using Xunit; namespace Microsoft.OpenApi.Readers.Tests.V31Tests