Skip to content

Commit 72c2131

Browse files
committed
Fix self-referential schema handling to close #58006
1 parent 01bda26 commit 72c2131

File tree

3 files changed

+57
-0
lines changed

3 files changed

+57
-0
lines changed

src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@ private void AddOrUpdateAnyOfSubSchemaByReference(OpenApiSchema schema)
159159
private void AddOrUpdateSchemaByReference(OpenApiSchema schema, string? baseTypeSchemaId = null, bool captureSchemaByRef = false)
160160
{
161161
var targetReferenceId = baseTypeSchemaId is not null ? $"{baseTypeSchemaId}{GetSchemaReferenceId(schema)}" : GetSchemaReferenceId(schema);
162+
// Schemas that already have a reference provided by JsonSchemaExporter are skipped here
163+
// and handled by the OpenApiSchemaReferenceTransformer instead. This case typically kicks
164+
// in for self-referencing schemas where JsonSchemaExporter inlines references to avoid
165+
// infinite recursion.
166+
if (schema.Reference is not null)
167+
{
168+
return;
169+
}
162170
if (SchemasByReference.TryGetValue(schema, out var referenceId) || captureSchemaByRef)
163171
{
164172
// If we've already used this reference ID else where in the document, increment a counter value to the reference

src/OpenApi/src/Transformers/Implementations/OpenApiSchemaReferenceTransformer.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerC
101101
return new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = referenceId } };
102102
}
103103

104+
// Handle schemas where the references have been inline by the JsonSchemaExporter. In this case,
105+
// the `#` ID is generated by the exporter since it has no base document to baseline against. In this
106+
// case we we want to replace the reference ID with the schema ID that was generated by the
107+
// `CreateSchemaReferenceId` method in the OpenApiSchemaService.
108+
if (!isTopLevel && schema.Reference is { Id: "#" }
109+
&& schema.Annotations.TryGetValue(OpenApiConstants.SchemaId, out var schemaId))
110+
{
111+
return new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = schemaId?.ToString() } };
112+
}
113+
104114
if (schema.AllOf is not null)
105115
{
106116
for (var i = 0; i < schema.AllOf.Count; i++)

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.RequestBodySchemas.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,4 +659,43 @@ private class ExampleWithSkippedUnmappedMembers
659659
{
660660
public int Number { get; init; }
661661
}
662+
663+
[Fact]
664+
public async Task SupportsTypesWithSelfReferencedProperties()
665+
{
666+
// Arrange
667+
var builder = CreateBuilder();
668+
669+
// Act
670+
builder.MapPost("/api", (Parent parent) => { });
671+
672+
// Assert
673+
await VerifyOpenApiDocument(builder, document =>
674+
{
675+
var operation = document.Paths["/api"].Operations[OperationType.Post];
676+
var requestBody = operation.RequestBody;
677+
var content = Assert.Single(requestBody.Content);
678+
var schema = content.Value.Schema.GetEffective(document);
679+
Assert.Collection(schema.Properties,
680+
property =>
681+
{
682+
Assert.Equal("selfReferenceList", property.Key);
683+
Assert.Equal("array", property.Value.Type);
684+
Assert.Equal("Parent", property.Value.Items.Reference.Id);
685+
},
686+
property =>
687+
{
688+
Assert.Equal("selfReferenceDictionary", property.Key);
689+
Assert.Equal("object", property.Value.Type);
690+
Assert.Equal("Parent", property.Value.AdditionalProperties.Reference.Id);
691+
});
692+
});
693+
}
694+
695+
public class Parent
696+
{
697+
public IEnumerable<Parent> SelfReferenceList { get; set; } = [ ];
698+
public IDictionary<string, Parent> SelfReferenceDictionary { get; set; } = new Dictionary<string, Parent>();
699+
}
700+
662701
}

0 commit comments

Comments
 (0)