Skip to content

fix: relative uri in json schema references would not parse appropriately or provide feedback to the user #2397

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@
/// <summary>
/// Serialize <see cref="OpenApiDocument"/> to OpenAPI object V2.0.
/// </summary>
public void SerializeAsV2(IOpenApiWriter writer)

Check warning on line 262 in src/Microsoft.OpenApi/Models/OpenApiDocument.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 31 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
Utils.CheckArgumentNull(writer);

Expand Down Expand Up @@ -408,7 +408,7 @@
return server.ReplaceServerUrlVariables([]);
}

private static void WriteHostInfoV2(IOpenApiWriter writer, IList<OpenApiServer>? servers)

Check warning on line 411 in src/Microsoft.OpenApi/Models/OpenApiDocument.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
if (servers == null || !servers.Any())
{
Expand Down Expand Up @@ -477,7 +477,7 @@
// schemes
writer.WriteOptionalCollection(OpenApiConstants.Schemes, schemes, (w, s) =>
{
if(!string.IsNullOrEmpty(s) && s is not null)

Check warning on line 480 in src/Microsoft.OpenApi/Models/OpenApiDocument.cs

View workflow job for this annotation

GitHub Actions / Build

Change this condition so that it does not always evaluate to 'True'. (https://rules.sonarsource.com/csharp/RSPEC-2589)
{
w.WriteValue(s);
}
Expand Down Expand Up @@ -538,7 +538,7 @@

return ConvertByteArrayToString(hash ?? []);

async Task WriteDocumentAsync(TextWriter writer, CancellationToken token)

Check warning on line 541 in src/Microsoft.OpenApi/Models/OpenApiDocument.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this unused method parameter 'token'. (https://rules.sonarsource.com/csharp/RSPEC-1172)
{
var openApiJsonWriter = new OpenApiJsonWriter(writer, new() { Terse = true });
SerializeAsV31(openApiJsonWriter);
Expand Down Expand Up @@ -566,7 +566,7 @@
/// <summary>
/// Load the referenced <see cref="IOpenApiReferenceable"/> object from a <see cref="BaseOpenApiReference"/> object
/// </summary>
internal IOpenApiReferenceable? ResolveReference(BaseOpenApiReference? reference, bool useExternal)

Check warning on line 569 in src/Microsoft.OpenApi/Models/OpenApiDocument.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
if (reference == null)
{
Expand All @@ -575,9 +575,9 @@

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
{
Expand Down Expand Up @@ -609,12 +609,24 @@
: 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<IOpenApiReferenceable>(new Uri(uriLocation).AbsoluteUri);
return Workspace?.ResolveReference<IOpenApiReferenceable>(absoluteUri);
}

private static bool IsSubComponent(string reference)
Expand Down
39 changes: 20 additions & 19 deletions src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
/// Registers a document's components into the workspace
/// </summary>
/// <param name="document"></param>
public void RegisterComponents(OpenApiDocument document)

Check warning on line 84 in src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 31 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 84 in src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 31 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
if (document?.Components == null) return;

Expand Down Expand Up @@ -268,7 +268,7 @@
/// <param name="value"></param>
public void AddDocumentId(string? key, Uri? value)
{
if (!string.IsNullOrEmpty(key) && key is not null && value is not null && !_documentsIdRegistry.ContainsKey(key))

Check warning on line 271 in src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

View workflow job for this annotation

GitHub Actions / Build

Change this condition so that it does not always evaluate to 'True'. (https://rules.sonarsource.com/csharp/RSPEC-2589)
{
_documentsIdRegistry[key] = value;
}
Expand Down Expand Up @@ -340,34 +340,35 @@
* #/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;
}

internal static IOpenApiSchema? ResolveSubSchema(IOpenApiSchema schema, string[] pathSegments)

Check warning on line 371 in src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
// Traverse schema object to resolve subschemas
if (pathSegments.Length == 0)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<OpenApiSchema>(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<string, IOpenApiSchema>
{
["Foo"] = new OpenApiSchema
{
Properties = new Dictionary<string, IOpenApiSchema>
{
["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<ArgumentException>(() => 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<string, IOpenApiSchema>
{
["Foo"] = new OpenApiSchema
{
Properties = new Dictionary<string, IOpenApiSchema>
{
["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
Expand Down
Loading