Skip to content

Commit 3d1af00

Browse files
committed
Fix array circular references.
The fix is done by registering components before going deep into the recursion tree (where the leaves would be registered first). Fixing this revealed an issue for default values for "local" attributes. Local attributes/parameter info should not apply to componetized schemas.
1 parent 021a0c0 commit 3d1af00

File tree

6 files changed

+40
-15
lines changed

6 files changed

+40
-15
lines changed

src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,16 @@ internal static void ApplyDefaultValue(this JsonNode schema, object? defaultValu
175175
return;
176176
}
177177

178+
var isReferencedSchema = schema[OpenApiConstants.SchemaId] is null;
179+
var schemaAttribute = isReferencedSchema ? OpenApiConstants.RefDefaultAnnotation : OpenApiSchemaKeywords.DefaultKeyword;
180+
178181
if (defaultValue is null)
179182
{
180-
schema[OpenApiSchemaKeywords.DefaultKeyword] = null;
183+
schema[schemaAttribute] = null;
181184
}
182185
else
183186
{
184-
schema[OpenApiSchemaKeywords.DefaultKeyword] = JsonSerializer.SerializeToNode(defaultValue, jsonTypeInfo);
187+
schema[schemaAttribute] = JsonSerializer.SerializeToNode(defaultValue, jsonTypeInfo);
185188
}
186189
}
187190

src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,27 @@ internal static class OpenApiDocumentExtensions
1717
/// <returns>An <see cref="IOpenApiSchema"/> with a reference to the stored schema.</returns>
1818
public static IOpenApiSchema AddOpenApiSchemaByReference(this OpenApiDocument document, string schemaId, IOpenApiSchema schema)
1919
{
20-
document.Components ??= new();
21-
document.Components.Schemas ??= new Dictionary<string, IOpenApiSchema>();
22-
document.Components.Schemas[schemaId] = schema;
20+
// Make sure the document has a workspace,
21+
// AddComponent will add it to the workspace when adding the component.
2322
document.Workspace ??= new();
24-
var location = document.BaseUri + "/components/schemas/" + schemaId;
25-
document.Workspace.RegisterComponentForDocument(document, schema, location);
23+
// AddComponent will only add the schema if it doesn't already exist.
24+
document.AddComponent(schemaId, schema);
2625

2726
object? description = null;
2827
object? example = null;
28+
object? defaultAnnotation = null;
2929
if (schema is OpenApiSchema actualSchema)
3030
{
3131
actualSchema.Metadata?.TryGetValue(OpenApiConstants.RefDescriptionAnnotation, out description);
3232
actualSchema.Metadata?.TryGetValue(OpenApiConstants.RefExampleAnnotation, out example);
33+
actualSchema.Metadata?.TryGetValue(OpenApiConstants.RefDefaultAnnotation, out defaultAnnotation);
3334
}
3435

3536
return new OpenApiSchemaReference(schemaId, document)
3637
{
3738
Description = description as string,
3839
Examples = example is JsonNode exampleJson ? [exampleJson] : null,
40+
Default = defaultAnnotation as JsonNode,
3941
};
4042
}
4143
}

src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,11 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName,
355355
schema.Metadata ??= new Dictionary<string, object>();
356356
schema.Metadata[OpenApiConstants.RefDescriptionAnnotation] = reader.GetString() ?? string.Empty;
357357
break;
358-
358+
case OpenApiConstants.RefDefaultAnnotation:
359+
reader.Read();
360+
schema.Metadata ??= new Dictionary<string, object>();
361+
schema.Metadata[OpenApiConstants.RefDefaultAnnotation] = ReadJsonNode(ref reader)!;
362+
break;
359363
default:
360364
reader.Skip();
361365
break;

src/OpenApi/src/Services/OpenApiConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ internal static class OpenApiConstants
1313
internal const string DescriptionId = "x-aspnetcore-id";
1414
internal const string SchemaId = "x-schema-id";
1515
internal const string RefId = "x-ref-id";
16+
internal const string RefDefaultAnnotation = "x-ref-default";
1617
internal const string RefDescriptionAnnotation = "x-ref-description";
1718
internal const string RefExampleAnnotation = "x-ref-example";
1819
internal const string RefKeyword = "$ref";

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

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,20 @@ internal static IOpenApiSchema ResolveReferenceForSchema(OpenApiDocument documen
265265
{
266266
var schema = UnwrapOpenApiSchema(inputSchema);
267267

268+
if (inputSchema is OpenApiSchema && schema.Metadata is not null &&
269+
!schema.Metadata.ContainsKey(OpenApiConstants.RefId) &&
270+
schema.Metadata.TryGetValue(OpenApiConstants.SchemaId, out var referenceId) &&
271+
referenceId is string referenceIdString)
272+
{
273+
var targetReferenceId = baseSchemaId is not null
274+
? $"{baseSchemaId}{referenceIdString}"
275+
: referenceIdString;
276+
if (!string.IsNullOrEmpty(targetReferenceId))
277+
{
278+
document.AddOpenApiSchemaByReference(targetReferenceId, schema);
279+
}
280+
}
281+
268282
if (schema.Metadata is not null &&
269283
schema.Metadata.TryGetValue(OpenApiConstants.SchemaId, out var resolvedBaseSchemaId))
270284
{
@@ -347,14 +361,14 @@ internal static IOpenApiSchema ResolveReferenceForSchema(OpenApiDocument documen
347361
// we don't want to replace the top-level inline schema with a reference to itself. We want to replace
348362
// inline schemas to reference schemas for all schemas referenced in the top-level schema though (such as
349363
// `allOf`, `oneOf`, `anyOf`, `items`, `properties`, etc.) which is why `isTopLevel` is only set once.
350-
if (schema is OpenApiSchema && schema.Metadata is not null &&
364+
if (inputSchema is OpenApiSchema && schema.Metadata is not null &&
351365
!schema.Metadata.ContainsKey(OpenApiConstants.RefId) &&
352-
schema.Metadata.TryGetValue(OpenApiConstants.SchemaId, out var referenceId) &&
353-
referenceId is string referenceIdString)
366+
schema.Metadata.TryGetValue(OpenApiConstants.SchemaId, out var referenceId2) &&
367+
referenceId2 is string referenceIdString2)
354368
{
355369
var targetReferenceId = baseSchemaId is not null
356-
? $"{baseSchemaId}{referenceIdString}"
357-
: referenceIdString;
370+
? $"{baseSchemaId}{referenceIdString2}"
371+
: referenceIdString2;
358372
if (!string.IsNullOrEmpty(targetReferenceId))
359373
{
360374
return document.AddOpenApiSchemaByReference(targetReferenceId, schema);

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -984,8 +984,9 @@ private void VerifyOptionalEnum(OpenApiDocument document)
984984
var property = properties["status"];
985985

986986
Assert.NotNull(property);
987-
Assert.Equal(3, property.Enum.Count);
988-
Assert.Equal("Approved", property.Default.GetValue<string>());
987+
var statusReference = Assert.IsType<OpenApiSchemaReference>(property);
988+
Assert.Equal(3, statusReference.RecursiveTarget.Enum.Count);
989+
Assert.Equal("Approved", statusReference.Default.GetValue<string>());
989990
}
990991

991992
[ApiController]

0 commit comments

Comments
 (0)