Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
9 changes: 9 additions & 0 deletions src/OpenApi/src/Comparers/OpenApiSchemaComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ public bool Equals(OpenApiSchema? x, OpenApiSchema? y)
return true;
}

// If a local reference is present, we can't compare the schema directly
// and should instead use the schema ID as a type-check to assert if the schemas are
// equivalent.
if ((x.Reference != null && y.Reference == null)
|| (x.Reference == null && y.Reference != null))
{
return SchemaIdEquals(x, y);
}

// Compare property equality in an order that should help us find inequality faster
return
x.Type == y.Type &&
Expand Down
6 changes: 3 additions & 3 deletions src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ internal sealed class OpenApiSchemaService(
IOptionsMonitor<OpenApiOptions> optionsMonitor)
{
private readonly OpenApiSchemaStore _schemaStore = serviceProvider.GetRequiredKeyedService<OpenApiSchemaStore>(documentName);
private readonly OpenApiJsonSchemaContext _jsonSchemaContext = new OpenApiJsonSchemaContext(new(jsonOptions.Value.SerializerOptions));
private readonly OpenApiJsonSchemaContext _jsonSchemaContext = new(new(jsonOptions.Value.SerializerOptions));
private readonly JsonSerializerOptions _jsonSerializerOptions = new(jsonOptions.Value.SerializerOptions)
{
// In order to properly handle the `RequiredAttribute` on type properties, add a modifier to support
Expand Down Expand Up @@ -102,7 +102,7 @@ internal sealed class OpenApiSchemaService(
// "nested": "#/properties/nested" becomes "nested": "#/components/schemas/NestedType"
if (jsonPropertyInfo.PropertyType == jsonPropertyInfo.DeclaringType)
{
return new JsonObject { [OpenApiSchemaKeywords.RefKeyword] = context.TypeInfo.GetSchemaReferenceId() };
return new JsonObject { [OpenApiSchemaKeywords.RefKeyword] = createSchemaReferenceId(context.TypeInfo) };
}
schema.ApplyNullabilityContextInfo(jsonPropertyInfo);
}
Expand Down Expand Up @@ -213,7 +213,7 @@ private async Task InnerApplySchemaTransformersAsync(OpenApiSchema schema,
}
}

if (schema is { AdditionalPropertiesAllowed: true, AdditionalProperties: not null } && jsonTypeInfo.ElementType is not null)
if (schema is { AdditionalPropertiesAllowed: true, AdditionalProperties: not null } && jsonTypeInfo.ElementType is not null)
{
var elementTypeInfo = _jsonSerializerOptions.GetTypeInfo(jsonTypeInfo.ElementType);
await InnerApplySchemaTransformersAsync(schema.AdditionalProperties, elementTypeInfo, null, context, transformer, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ public void ValidateDeepCopyOnSchemasWithReference()

var modifiedSchema = originalSchema.Clone();
modifiedSchema.Properties["name"].Reference = new OpenApiReference { Id = "Another Id", Type = ReferenceType.Schema };
modifiedSchema.Annotations = new Dictionary<string, object> { ["x-schema-id"] = "another value" };
Assert.False(OpenApiSchemaComparer.Instance.Equals(originalSchema, modifiedSchema));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,4 +477,127 @@ await VerifyOpenApiDocument(builder, options, document =>
Assert.Equal(ReferenceType.Link, responseSchema.Reference.Type);
});
}

[Fact]
public async Task SupportsNestedSchemasWithSelfReference()
{
// Arrange
var builder = CreateBuilder();

builder.MapPost("/", (LocationContainer item) => { });

await VerifyOpenApiDocument(builder, document =>
{
var operation = document.Paths["/"].Operations[OperationType.Post];
var requestSchema = operation.RequestBody.Content["application/json"].Schema;

// Assert $ref used for top-level
Assert.Equal("LocationContainer", requestSchema.Reference.Id);

// Assert that $ref is used for nested LocationDto
var locationContainerSchema = requestSchema.GetEffective(document);
Assert.Equal("LocationDto", locationContainerSchema.Properties["location"].Reference.Id);

// Assert that $ref is used for nested AddressDto
var locationSchema = locationContainerSchema.Properties["location"].GetEffective(document);
Assert.Equal("AddressDto", locationSchema.Properties["address"].Reference.Id);

// Assert that $ref is used for related LocationDto
var addressSchema = locationSchema.Properties["address"].GetEffective(document);
Assert.Equal("LocationDto", addressSchema.Properties["relatedLocation"].Reference.Id);
});
}

[Fact]
public async Task SupportsListNestedSchemasWithSelfReference()
{
// Arrange
var builder = CreateBuilder();

builder.MapPost("/", (ParentObject item) => { });

await VerifyOpenApiDocument(builder, document =>
{
var operation = document.Paths["/"].Operations[OperationType.Post];
var requestSchema = operation.RequestBody.Content["application/json"].Schema;

// Assert $ref used for top-level
Assert.Equal("ParentObject", requestSchema.Reference.Id);

// Assert that $ref is used for nested Children
var parentSchema = requestSchema.GetEffective(document);
Assert.Equal("ChildObject", parentSchema.Properties["children"].Items.Reference.Id);

// Assert that $ref is used for nested Parent
var childSchema = parentSchema.Properties["children"].Items.GetEffective(document);
Assert.Equal("ParentObject", childSchema.Properties["parent"].Reference.Id);
});
}

[Fact]
public async Task SupportsMultiplePropertiesWithSameType()
{
// Arrange
var builder = CreateBuilder();

builder.MapPost("/", (Root item) => { });

await VerifyOpenApiDocument(builder, document =>
{
var operation = document.Paths["/"].Operations[OperationType.Post];
var requestSchema = operation.RequestBody.Content["application/json"].Schema;

// Assert $ref used for top-level
Assert.Equal("Root", requestSchema.Reference.Id);

// Assert that $ref is used for nested Item1
var rootSchema = requestSchema.GetEffective(document);
Assert.Equal("Item", rootSchema.Properties["item1"].Reference.Id);

// Assert that $ref is used for nested Item2
Assert.Equal("Item", rootSchema.Properties["item2"].Reference.Id);
});
}

private class Root
{
public Item Item1 { get; set; } = null!;
public Item Item2 { get; set; } = null!;
}

private class Item
{
public string[] Name { get; set; } = null!;
public int value { get; set; }
}

private class LocationContainer
{

public LocationDto Location { get; set; }
}

private class LocationDto
{
public AddressDto Address { get; set; }
}

private class AddressDto
{
public LocationDto RelatedLocation { get; set; }
}

#nullable enable
private class ParentObject
{
public int Id { get; set; }
public List<ChildObject> Children { get; set; } = [];
}

private class ChildObject
{
public int Id { get; set; }
public required ParentObject Parent { get; set; }
}
}
#nullable restore
Loading