Skip to content

Commit e7fa345

Browse files
captainsafiamikekistler
andauthoredJul 24, 2024··
Inline array and dictionary schemas in OpenAPI documents (#56980)
* Inline array and dictionary schemas in OpenAPI documents * Update src/OpenApi/src/Extensions/JsonTypeInfoExtensions.cs Co-authored-by: Mike Kistler <mikekistler@microsoft.com> --------- Co-authored-by: Mike Kistler <mikekistler@microsoft.com>
1 parent 2f79c47 commit e7fa345

7 files changed

+84
-143
lines changed
 

‎src/OpenApi/src/Extensions/JsonTypeInfoExtensions.cs

+7-15
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,17 @@ internal static class JsonTypeInfoExtensions
6666
return simpleName;
6767
}
6868

69-
if (jsonTypeInfo is JsonTypeInfo { Kind: JsonTypeInfoKind.Enumerable, ElementType: { } elementType })
69+
// Although arrays are enumerable types they are not encoded correctly
70+
// with JsonTypeInfoKind.Enumerable so we handle the Enumerble type
71+
// case here.
72+
if (jsonTypeInfo is JsonTypeInfo { Kind: JsonTypeInfoKind.Enumerable } || type.IsArray)
7073
{
71-
var elementTypeInfo = jsonTypeInfo.Options.GetTypeInfo(elementType);
72-
return $"ArrayOf{elementTypeInfo.GetSchemaReferenceId(isTopLevel: false)}";
74+
return null;
7375
}
7476

75-
if (jsonTypeInfo is JsonTypeInfo { Kind: JsonTypeInfoKind.Dictionary, KeyType: { } keyType, ElementType: { } valueType })
77+
if (jsonTypeInfo is JsonTypeInfo { Kind: JsonTypeInfoKind.Dictionary })
7678
{
77-
var keyTypeInfo = jsonTypeInfo.Options.GetTypeInfo(keyType);
78-
var valueTypeInfo = jsonTypeInfo.Options.GetTypeInfo(valueType);
79-
return $"DictionaryOf{keyTypeInfo.GetSchemaReferenceId(isTopLevel: false)}And{valueTypeInfo.GetSchemaReferenceId(isTopLevel: false)}";
79+
return null;
8080
}
8181

8282
return type.GetSchemaReferenceId(jsonTypeInfo.Options);
@@ -91,14 +91,6 @@ internal static string GetSchemaReferenceId(this Type type, JsonSerializerOption
9191
return simpleName;
9292
}
9393

94-
// Although arrays are enumerable types they are not encoded correctly
95-
// with JsonTypeInfoKind.Enumerable so we handle that here
96-
if (type.IsArray && type.GetElementType() is { } elementType)
97-
{
98-
var elementTypeInfo = options.GetTypeInfo(elementType);
99-
return $"ArrayOf{elementTypeInfo.GetSchemaReferenceId(isTopLevel: false)}";
100-
}
101-
10294
// Special handling for anonymous types
10395
if (type.Name.StartsWith("<>f", StringComparison.Ordinal))
10496
{

‎src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonTypeInfoExtensionsTests.cs

+9-9
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ private class Container
3535
public static IEnumerable<object[]> GetSchemaReferenceId_Data =>
3636
[
3737
[typeof(Todo), "Todo"],
38-
[typeof(IEnumerable<Todo>), "ArrayOfTodo"],
39-
[typeof(List<Todo>), "ArrayOfTodo"],
38+
[typeof(IEnumerable<Todo>), null],
39+
[typeof(List<Todo>), null],
4040
[typeof(TodoWithDueDate), "TodoWithDueDate"],
41-
[typeof(IEnumerable<TodoWithDueDate>), "ArrayOfTodoWithDueDate"],
41+
[typeof(IEnumerable<TodoWithDueDate>), null],
4242
[(new { Id = 1 }).GetType(), "AnonymousTypeOfint"],
4343
[(new { Id = 1, Name = "Todo" }).GetType(), "AnonymousTypeOfintAndstring"],
4444
[typeof(IFormFile), "IFormFile"],
@@ -50,14 +50,14 @@ private class Container
5050
[typeof(NotFound<TodoWithDueDate>), "NotFoundOfTodoWithDueDate"],
5151
[typeof(TestDelegate), "TestDelegate"],
5252
[typeof(Container.ContainedTestDelegate), "ContainedTestDelegate"],
53-
[typeof(List<int>), "ArrayOfint"],
54-
[typeof(List<List<int>>), "ArrayOfArrayOfint"],
55-
[typeof(int[]), "ArrayOfint"],
53+
[typeof(List<int>), null],
54+
[typeof(List<List<int>>), null],
55+
[typeof(int[]), null],
5656
[typeof(ValidationProblemDetails), "ValidationProblemDetails"],
5757
[typeof(ProblemDetails), "ProblemDetails"],
58-
[typeof(Dictionary<string, string[]>), "DictionaryOfstringAndArrayOfstring"],
59-
[typeof(Dictionary<string, List<string[]>>), "DictionaryOfstringAndArrayOfArrayOfstring"],
60-
[typeof(Dictionary<string, IEnumerable<string[]>>), "DictionaryOfstringAndArrayOfArrayOfstring"],
58+
[typeof(Dictionary<string, string[]>), null],
59+
[typeof(Dictionary<string, List<string[]>>), null],
60+
[typeof(Dictionary<string, IEnumerable<string[]>>), null],
6161
];
6262

6363
[Theory]

‎src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt

+20-18
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,11 @@
185185
"content": {
186186
"application/json": {
187187
"schema": {
188-
"$ref": "#/components/schemas/ArrayOfint"
188+
"type": "array",
189+
"items": {
190+
"type": "integer",
191+
"format": "int32"
192+
}
189193
}
190194
}
191195
},
@@ -215,7 +219,11 @@
215219
"content": {
216220
"application/json": {
217221
"schema": {
218-
"$ref": "#/components/schemas/ArrayOfint"
222+
"type": "array",
223+
"items": {
224+
"type": "integer",
225+
"format": "int32"
226+
}
219227
}
220228
}
221229
},
@@ -267,7 +275,11 @@
267275
"content": {
268276
"application/json": {
269277
"schema": {
270-
"$ref": "#/components/schemas/DictionaryOfstringAndint"
278+
"type": "object",
279+
"additionalProperties": {
280+
"type": "integer",
281+
"format": "int32"
282+
}
271283
}
272284
}
273285
}
@@ -286,7 +298,11 @@
286298
"content": {
287299
"application/json": {
288300
"schema": {
289-
"$ref": "#/components/schemas/DictionaryOfstringAndint"
301+
"type": "object",
302+
"additionalProperties": {
303+
"type": "integer",
304+
"format": "int32"
305+
}
290306
}
291307
}
292308
}
@@ -375,20 +391,6 @@
375391
}
376392
}
377393
},
378-
"ArrayOfint": {
379-
"type": "array",
380-
"items": {
381-
"type": "integer",
382-
"format": "int32"
383-
}
384-
},
385-
"DictionaryOfstringAndint": {
386-
"type": "object",
387-
"additionalProperties": {
388-
"type": "integer",
389-
"format": "int32"
390-
}
391-
},
392394
"Person": {
393395
"required": [
394396
"discriminator"

‎src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=v1.verified.txt

+10-9
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
"in": "query",
1717
"required": true,
1818
"schema": {
19-
"$ref": "#/components/schemas/ArrayOfGuid"
19+
"type": "array",
20+
"items": {
21+
"type": "string",
22+
"format": "uuid"
23+
}
2024
}
2125
},
2226
{
@@ -34,7 +38,11 @@
3438
"content": {
3539
"application/json": {
3640
"schema": {
37-
"$ref": "#/components/schemas/ArrayOfGuid"
41+
"type": "array",
42+
"items": {
43+
"type": "string",
44+
"format": "uuid"
45+
}
3846
}
3947
}
4048
}
@@ -117,13 +125,6 @@
117125
},
118126
"components": {
119127
"schemas": {
120-
"ArrayOfGuid": {
121-
"type": "array",
122-
"items": {
123-
"type": "string",
124-
"format": "uuid"
125-
}
126-
},
127128
"Todo": {
128129
"required": [
129130
"id",

‎src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=v2.verified.txt

+13-19
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,18 @@
2323
"content": {
2424
"application/json": {
2525
"schema": {
26-
"$ref": "#/components/schemas/ArrayOfstring"
26+
"type": "array",
27+
"items": {
28+
"type": "string",
29+
"externalDocs": {
30+
"description": "Documentation for this OpenAPI schema",
31+
"url": "https://example.com/api/docs/schemas/string"
32+
}
33+
},
34+
"externalDocs": {
35+
"description": "Documentation for this OpenAPI schema",
36+
"url": "https://example.com/api/docs/schemas/array"
37+
}
2738
}
2839
}
2940
}
@@ -47,24 +58,7 @@
4758
}
4859
}
4960
},
50-
"components": {
51-
"schemas": {
52-
"ArrayOfstring": {
53-
"type": "array",
54-
"items": {
55-
"type": "string",
56-
"externalDocs": {
57-
"description": "Documentation for this OpenAPI schema",
58-
"url": "https://example.com/api/docs/schemas/string"
59-
}
60-
},
61-
"externalDocs": {
62-
"description": "Documentation for this OpenAPI schema",
63-
"url": "https://example.com/api/docs/schemas/array"
64-
}
65-
}
66-
}
67-
},
61+
"components": { },
6862
"tags": [
6963
{
7064
"name": "users"

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,11 @@ await VerifyOpenApiDocument(builder, document =>
242242

243243
var enumerableTodoSchema = enumerableTodo.RequestBody.Content["application/json"].Schema;
244244
var arrayTodoSchema = arrayTodo.RequestBody.Content["application/json"].Schema;
245-
// Assert that both IEnumerable<Todo> and Todo[] map to the same schemas
246-
Assert.Equal(enumerableTodoSchema.Reference.Id, arrayTodoSchema.Reference.Id);
245+
// Assert that both IEnumerable<Todo> and Todo[] have items that map to the same schema
246+
Assert.Equal(enumerableTodoSchema.Items.Reference.Id, arrayTodoSchema.Items.Reference.Id);
247247
// Assert all types materialize as arrays
248-
Assert.Equal("array", enumerableTodoSchema.GetEffective(document).Type);
249-
Assert.Equal("array", arrayTodoSchema.GetEffective(document).Type);
248+
Assert.Equal("array", enumerableTodoSchema.Type);
249+
Assert.Equal("array", arrayTodoSchema.Type);
250250

251251
Assert.Equal("array", parameter.Schema.Type);
252252
Assert.Equal("string", parameter.Schema.Items.Type);
@@ -255,7 +255,7 @@ await VerifyOpenApiDocument(builder, document =>
255255
// Assert the array items are the same as the Todo schema
256256
foreach (var element in new[] { enumerableTodoSchema, arrayTodoSchema })
257257
{
258-
Assert.Collection(element.GetEffective(document).Items.GetEffective(document).Properties,
258+
Assert.Collection(element.Items.GetEffective(document).Properties,
259259
property =>
260260
{
261261
Assert.Equal("id", property.Key);

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

+20-68
Original file line numberDiff line numberDiff line change
@@ -230,31 +230,37 @@ await VerifyOpenApiDocument(builder, document =>
230230
var requestBodySchema2 = requestBody2.Schema;
231231

232232
// {
233-
// "$ref": "#/components/schemas/TodoArray"
234-
// }
233+
// "type": "array",
234+
// "items": {
235+
// "$ref": "#/components/schemas/Todo"
236+
// }
235237
// {
236-
// "$ref": "#/components/schemas/TodoArray"
237-
// }
238+
// "type": "array",
239+
// "items": {
240+
// "$ref": "#/components/schemas/Todo"
241+
// }
238242
// {
239243
// "components": {
240244
// "schemas": {
241245
// "TodoArray": {
242-
// "type": "array",
243-
// "items": {
244-
// "$ref": "#/components/schemas/Todo"
246+
// "type": "object",
247+
// "properties": {
248+
// ...
245249
// }
246250
// }
247251
// }
248252
// }
249253
// }
250254

251-
// Both list types should point to the same reference ID
252-
Assert.Equal(requestBodySchema.Reference.Id, requestBodySchema2.Reference.Id);
253-
// The referenced schema has an array type
254-
Assert.Equal("array", requestBodySchema.GetEffective(document).Type);
255-
// The items in the array are mapped to the Todo reference
256-
Assert.NotNull(requestBodySchema.GetEffective(document).Items.Reference.Id);
257-
Assert.Equal(4, requestBodySchema.GetEffective(document).Items.GetEffective(document).Properties.Count);
255+
// Both list types should be inlined
256+
Assert.Null(requestBodySchema.Reference);
257+
Assert.Equal(requestBodySchema.Reference, requestBodySchema2.Reference);
258+
// And have an `array` type
259+
Assert.Equal("array", requestBodySchema.Type);
260+
// With an `items` sub-schema should consist of a $ref to Todo
261+
Assert.Equal("Todo", requestBodySchema.Items.Reference.Id);
262+
Assert.Equal(requestBodySchema.Items.Reference.Id, requestBodySchema2.Items.Reference.Id);
263+
Assert.Equal(4, requestBodySchema.Items.GetEffective(document).Properties.Count);
258264
});
259265
}
260266

@@ -289,58 +295,4 @@ await VerifyOpenApiDocument(builder, options, document =>
289295
Assert.False(responseSchema.GetEffective(document).Extensions.TryGetValue("x-my-extension", out var _));
290296
});
291297
}
292-
293-
[Fact]
294-
public static async Task ProducesStableSchemaRefsForListOf()
295-
{
296-
// Arrange
297-
var builder = CreateBuilder();
298-
299-
// Act
300-
builder.MapPost("/api", (List<Todo> todo) => { });
301-
builder.MapPost("/api-2", (List<Todo> todo) => { });
302-
303-
// Assert -- call twice to ensure the schema reference is stable
304-
await VerifyOpenApiDocument(builder, VerifyDocument);
305-
await VerifyOpenApiDocument(builder, VerifyDocument);
306-
307-
static void VerifyDocument(OpenApiDocument document)
308-
{
309-
var operation = document.Paths["/api"].Operations[OperationType.Post];
310-
var requestBody = operation.RequestBody.Content["application/json"];
311-
var requestBodySchema = requestBody.Schema;
312-
313-
var operation2 = document.Paths["/api-2"].Operations[OperationType.Post];
314-
var requestBody2 = operation2.RequestBody.Content["application/json"];
315-
var requestBodySchema2 = requestBody2.Schema;
316-
317-
// {
318-
// "$ref": "#/components/schemas/TodoList"
319-
// }
320-
// {
321-
// "$ref": "#/components/schemas/TodoList"
322-
// }
323-
// {
324-
// "components": {
325-
// "schemas": {
326-
// "ArrayOfTodo": {
327-
// "type": "array",
328-
// "items": {
329-
// "$ref": "#/components/schemas/Todo"
330-
// }
331-
// }
332-
// }
333-
// }
334-
// }
335-
336-
// Both list types should point to the same reference ID
337-
Assert.Equal("ArrayOfTodo", requestBodySchema.Reference.Id);
338-
Assert.Equal(requestBodySchema.Reference.Id, requestBodySchema2.Reference.Id);
339-
// The referenced schema has an array type
340-
Assert.Equal("array", requestBodySchema.GetEffective(document).Type);
341-
var itemsSchema = requestBodySchema.GetEffective(document).Items;
342-
Assert.Equal("Todo", itemsSchema.Reference.Id);
343-
Assert.Equal(4, itemsSchema.GetEffective(document).Properties.Count);
344-
}
345-
}
346298
}

0 commit comments

Comments
 (0)
Please sign in to comment.