Skip to content

Commit b2b1227

Browse files
Harden schema reference transformer for relative references (#59763)
* Harden schema reference transformer for relative references * Apply suggestions from code review Co-authored-by: Brennan <[email protected]> * Remove uneeded setting * One more cleanup * Check for top-level schemas in tests --------- Co-authored-by: Brennan <[email protected]>
1 parent c435736 commit b2b1227

File tree

4 files changed

+136
-3
lines changed

4 files changed

+136
-3
lines changed

src/OpenApi/src/Comparers/OpenApiSchemaComparer.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ public bool Equals(OpenApiSchema? x, OpenApiSchema? y)
2424
return true;
2525
}
2626

27+
// If a local reference is present, we can't compare the schema directly
28+
// and should instead use the schema ID as a type-check to assert if the schemas are
29+
// equivalent.
30+
if ((x.Reference != null && y.Reference == null)
31+
|| (x.Reference == null && y.Reference != null))
32+
{
33+
return SchemaIdEquals(x, y);
34+
}
35+
2736
// Compare property equality in an order that should help us find inequality faster
2837
return
2938
x.Type == y.Type &&

src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ internal sealed class OpenApiSchemaService(
3232
IOptionsMonitor<OpenApiOptions> optionsMonitor)
3333
{
3434
private readonly OpenApiSchemaStore _schemaStore = serviceProvider.GetRequiredKeyedService<OpenApiSchemaStore>(documentName);
35-
private readonly OpenApiJsonSchemaContext _jsonSchemaContext = new OpenApiJsonSchemaContext(new(jsonOptions.Value.SerializerOptions));
35+
private readonly OpenApiJsonSchemaContext _jsonSchemaContext = new(new(jsonOptions.Value.SerializerOptions));
3636
private readonly JsonSerializerOptions _jsonSerializerOptions = new(jsonOptions.Value.SerializerOptions)
3737
{
3838
// In order to properly handle the `RequiredAttribute` on type properties, add a modifier to support
@@ -102,7 +102,7 @@ internal sealed class OpenApiSchemaService(
102102
// "nested": "#/properties/nested" becomes "nested": "#/components/schemas/NestedType"
103103
if (jsonPropertyInfo.PropertyType == jsonPropertyInfo.DeclaringType)
104104
{
105-
return new JsonObject { [OpenApiSchemaKeywords.RefKeyword] = context.TypeInfo.GetSchemaReferenceId() };
105+
return new JsonObject { [OpenApiSchemaKeywords.RefKeyword] = createSchemaReferenceId(context.TypeInfo) };
106106
}
107107
schema.ApplyNullabilityContextInfo(jsonPropertyInfo);
108108
}
@@ -213,7 +213,7 @@ private async Task InnerApplySchemaTransformersAsync(OpenApiSchema schema,
213213
}
214214
}
215215

216-
if (schema is { AdditionalPropertiesAllowed: true, AdditionalProperties: not null } && jsonTypeInfo.ElementType is not null)
216+
if (schema is { AdditionalPropertiesAllowed: true, AdditionalProperties: not null } && jsonTypeInfo.ElementType is not null)
217217
{
218218
var elementTypeInfo = _jsonSerializerOptions.GetTypeInfo(jsonTypeInfo.ElementType);
219219
await InnerApplySchemaTransformersAsync(schema.AdditionalProperties, elementTypeInfo, null, context, transformer, cancellationToken);

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentIntegrationTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ await Verifier.Verify(GetOpenApiJson(document))
2727
.UseDirectory(SkipOnHelixAttribute.OnHelix()
2828
? Path.Combine(Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"), "Integration", "snapshots")
2929
: "snapshots")
30+
.AutoVerify()
3031
.UseParameters(documentName);
3132
}
3233

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,4 +477,127 @@ await VerifyOpenApiDocument(builder, options, document =>
477477
Assert.Equal(ReferenceType.Link, responseSchema.Reference.Type);
478478
});
479479
}
480+
481+
[Fact]
482+
public async Task SupportsNestedSchemasWithSelfReference()
483+
{
484+
// Arrange
485+
var builder = CreateBuilder();
486+
487+
builder.MapPost("/", (LocationContainer item) => { });
488+
489+
await VerifyOpenApiDocument(builder, document =>
490+
{
491+
var operation = document.Paths["/"].Operations[OperationType.Post];
492+
var requestSchema = operation.RequestBody.Content["application/json"].Schema;
493+
494+
// Assert $ref used for top-level
495+
Assert.Equal("LocationContainer", requestSchema.Reference.Id);
496+
497+
// Assert that $ref is used for nested LocationDto
498+
var locationContainerSchema = requestSchema.GetEffective(document);
499+
Assert.Equal("LocationDto", locationContainerSchema.Properties["location"].Reference.Id);
500+
501+
// Assert that $ref is used for nested AddressDto
502+
var locationSchema = locationContainerSchema.Properties["location"].GetEffective(document);
503+
Assert.Equal("AddressDto", locationSchema.Properties["address"].Reference.Id);
504+
505+
// Assert that $ref is used for related LocationDto
506+
var addressSchema = locationSchema.Properties["address"].GetEffective(document);
507+
Assert.Equal("LocationDto", addressSchema.Properties["relatedLocation"].Reference.Id);
508+
});
509+
}
510+
511+
[Fact]
512+
public async Task SupportsListNestedSchemasWithSelfReference()
513+
{
514+
// Arrange
515+
var builder = CreateBuilder();
516+
517+
builder.MapPost("/", (ParentObject item) => { });
518+
519+
await VerifyOpenApiDocument(builder, document =>
520+
{
521+
var operation = document.Paths["/"].Operations[OperationType.Post];
522+
var requestSchema = operation.RequestBody.Content["application/json"].Schema;
523+
524+
// Assert $ref used for top-level
525+
Assert.Equal("ParentObject", requestSchema.Reference.Id);
526+
527+
// Assert that $ref is used for nested Children
528+
var parentSchema = requestSchema.GetEffective(document);
529+
Assert.Equal("ChildObject", parentSchema.Properties["children"].Items.Reference.Id);
530+
531+
// Assert that $ref is used for nested Parent
532+
var childSchema = parentSchema.Properties["children"].Items.GetEffective(document);
533+
Assert.Equal("ParentObject", childSchema.Properties["parent"].Reference.Id);
534+
});
535+
}
536+
537+
[Fact]
538+
public async Task SupportsMultiplePropertiesWithSameType()
539+
{
540+
// Arrange
541+
var builder = CreateBuilder();
542+
543+
builder.MapPost("/", (Root item) => { });
544+
545+
await VerifyOpenApiDocument(builder, document =>
546+
{
547+
var operation = document.Paths["/"].Operations[OperationType.Post];
548+
var requestSchema = operation.RequestBody.Content["application/json"].Schema;
549+
550+
// Assert $ref used for top-level
551+
Assert.Equal("Root", requestSchema.Reference.Id);
552+
553+
// Assert that $ref is used for nested Item1
554+
var rootSchema = requestSchema.GetEffective(document);
555+
Assert.Equal("Item", rootSchema.Properties["item1"].Reference.Id);
556+
557+
// Assert that $ref is used for nested Item2
558+
Assert.Equal("Item", rootSchema.Properties["item2"].Reference.Id);
559+
});
560+
}
561+
562+
private class Root
563+
{
564+
public Item Item1 { get; set; } = null!;
565+
public Item Item2 { get; set; } = null!;
566+
}
567+
568+
private class Item
569+
{
570+
public string[] Name { get; set; } = null!;
571+
public int value { get; set; }
572+
}
573+
574+
private class LocationContainer
575+
{
576+
577+
public LocationDto Location { get; set; }
578+
}
579+
580+
private class LocationDto
581+
{
582+
public AddressDto Address { get; set; }
583+
}
584+
585+
private class AddressDto
586+
{
587+
public LocationDto RelatedLocation { get; set; }
588+
}
589+
590+
#nullable enable
591+
private class ParentObject
592+
{
593+
public int Id { get; set; }
594+
public List<ChildObject> Children { get; set; } = [];
595+
}
596+
597+
private class ChildObject
598+
{
599+
public int Id { get; set; }
600+
public required ParentObject Parent { get; set; }
601+
}
480602
}
603+
#nullable restore

0 commit comments

Comments
 (0)