Skip to content

fix: recursive relative reference resolution #2393

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 1 commit into from
Jun 12, 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
8 changes: 6 additions & 2 deletions src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
/// <summary>
/// The OpenApiDocument that is hosting the OpenApiReference instance. This is used to enable dereferencing the reference.
/// </summary>
public OpenApiDocument? HostDocument { get => hostDocument; init => hostDocument = value; }

Check warning on line 58 in src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs

View workflow job for this annotation

GitHub Actions / Build

Make this an auto-implemented property and remove its backing field. (https://rules.sonarsource.com/csharp/RSPEC-2292)

Check warning on line 58 in src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs

View workflow job for this annotation

GitHub Actions / Build

Make this an auto-implemented property and remove its backing field. (https://rules.sonarsource.com/csharp/RSPEC-2292)

Check warning on line 58 in src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs

View workflow job for this annotation

GitHub Actions / Build

Make this an auto-implemented property and remove its backing field. (https://rules.sonarsource.com/csharp/RSPEC-2292)

private string? _referenceV3;
/// <summary>
Expand Down Expand Up @@ -84,8 +84,8 @@
{
return Id;
}
if (!string.IsNullOrEmpty(Id) && Id is not null && Id.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||

Check warning on line 87 in src/Microsoft.OpenApi/Models/BaseOpenApiReference.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)

Check warning on line 87 in src/Microsoft.OpenApi/Models/BaseOpenApiReference.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)
!string.IsNullOrEmpty(Id) && Id is not null && Id.StartsWith("https://", StringComparison.OrdinalIgnoreCase))

Check warning on line 88 in src/Microsoft.OpenApi/Models/BaseOpenApiReference.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)

Check warning on line 88 in src/Microsoft.OpenApi/Models/BaseOpenApiReference.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)
{
return Id;
}
Expand Down Expand Up @@ -337,10 +337,14 @@
}

// Fallback on building a full path
if (nodeLocation.StartsWith("#/components/schemas/", StringComparison.OrdinalIgnoreCase))
{ // If the nodeLocation is a schema, we only want to keep the first three segments which are components/schemas/{schemaName}
return $"#/{string.Join("/", nodeLocationSegments.Take(3).Concat(relativeSegments))}";
}
#if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER || NET5_0_OR_GREATER
return $"#/{string.Join("/", nodeLocationSegments.SkipLast(relativeSegments.Length).Union(relativeSegments))}";
return $"#/{string.Join("/", nodeLocationSegments.SkipLast(relativeSegments.Length).Concat(relativeSegments))}";
#else
return $"#/{string.Join("/", nodeLocationSegments.Take(nodeLocationSegments.Count - relativeSegments.Length).Union(relativeSegments))}";
return $"#/{string.Join("/", nodeLocationSegments.Take(nodeLocationSegments.Count - relativeSegments.Length).Concat(relativeSegments))}";
#endif
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{
"openapi": "3.1.0",
"info": {
"title": "Recursive 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": {
"name": {
"type": [
"string",
"null"
],
"format": null,
"x-schema-id": null
},
"parent": {
"type": [
"object",
"null"
],
"properties": {
"name": {
"type": [
"string",
"null"
],
"format": null,
"x-schema-id": null
},
"parent": {
"$ref": "#/properties/parent",
"x-schema-id": "Category"
},
"tags": {
"type": [
"array",
"null"
],
"items": {
"type": "object",
"properties": {
"name": {
"type": [
"string",
"null"
],
"format": null,
"x-schema-id": null
}
},
"required": [
"name"
],
"x-schema-id": "Tag"
}
}
},
"required": [
"name"
],
"x-schema-id": "Category"
},
"tags": {
"$ref": "#/properties/parent/properties/tags"
}
},
"required": [
"name"
],
"x-schema-id": "Category"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ public void ResolveSubSchema_ShouldRecurseIntoAllOfComposition()
Assert.Equal(JsonSchemaType.Integer, result!.Type);
}
[Fact]
public async Task SHouldResolveRelativeSubReference()
public async Task ShouldResolveRelativeSubReference()
{
// Arrange
var filePath = Path.Combine(SampleFolderPath, "relativeSubschemaReference.json");
Expand All @@ -296,5 +296,27 @@ public async Task SHouldResolveRelativeSubReference()
Assert.Equal(JsonSchemaType.Array, seq2Property.Items.Type);
Assert.Equal(JsonSchemaType.String, seq2Property.Items.Items.Type);
}
[Fact]
public async Task ShouldResolveRecursiveRelativeSubReference()
{
// Arrange
var filePath = Path.Combine(SampleFolderPath, "recursiveRelativeSubschemaReference.json");

// Act
var (actual, _) = await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings);

var fooComponentSchema = actual.Components.Schemas["Foo"];
var fooSchemaParentProperty = fooComponentSchema.Properties["parent"];
Assert.NotNull(fooSchemaParentProperty);
var fooSchemaParentPropertyTagsProperty = fooSchemaParentProperty.Properties["tags"];
Assert.NotNull(fooSchemaParentPropertyTagsProperty);
Assert.Equal(JsonSchemaType.Array | JsonSchemaType.Null, fooSchemaParentPropertyTagsProperty.Type);
Assert.Equal(JsonSchemaType.Object, fooSchemaParentPropertyTagsProperty.Items.Type);

var fooSchemaTagsProperty = fooComponentSchema.Properties["tags"];
Assert.NotNull(fooSchemaTagsProperty);
Assert.Equal(JsonSchemaType.Array | JsonSchemaType.Null, fooSchemaTagsProperty.Type);
Assert.Equal(JsonSchemaType.Object, fooSchemaTagsProperty.Items.Type);
}
}
}
Loading