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>
This commit is contained in:
Safia Abdalla 2024-07-24 13:35:14 -07:00 коммит произвёл GitHub
Родитель 2f79c475a7
Коммит e7fa345782
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
7 изменённых файлов: 84 добавлений и 143 удалений

Просмотреть файл

@ -66,17 +66,17 @@ internal static class JsonTypeInfoExtensions
return simpleName;
}
if (jsonTypeInfo is JsonTypeInfo { Kind: JsonTypeInfoKind.Enumerable, ElementType: { } elementType })
// Although arrays are enumerable types they are not encoded correctly
// with JsonTypeInfoKind.Enumerable so we handle the Enumerble type
// case here.
if (jsonTypeInfo is JsonTypeInfo { Kind: JsonTypeInfoKind.Enumerable } || type.IsArray)
{
var elementTypeInfo = jsonTypeInfo.Options.GetTypeInfo(elementType);
return $"ArrayOf{elementTypeInfo.GetSchemaReferenceId(isTopLevel: false)}";
return null;
}
if (jsonTypeInfo is JsonTypeInfo { Kind: JsonTypeInfoKind.Dictionary, KeyType: { } keyType, ElementType: { } valueType })
if (jsonTypeInfo is JsonTypeInfo { Kind: JsonTypeInfoKind.Dictionary })
{
var keyTypeInfo = jsonTypeInfo.Options.GetTypeInfo(keyType);
var valueTypeInfo = jsonTypeInfo.Options.GetTypeInfo(valueType);
return $"DictionaryOf{keyTypeInfo.GetSchemaReferenceId(isTopLevel: false)}And{valueTypeInfo.GetSchemaReferenceId(isTopLevel: false)}";
return null;
}
return type.GetSchemaReferenceId(jsonTypeInfo.Options);
@ -91,14 +91,6 @@ internal static class JsonTypeInfoExtensions
return simpleName;
}
// Although arrays are enumerable types they are not encoded correctly
// with JsonTypeInfoKind.Enumerable so we handle that here
if (type.IsArray && type.GetElementType() is { } elementType)
{
var elementTypeInfo = options.GetTypeInfo(elementType);
return $"ArrayOf{elementTypeInfo.GetSchemaReferenceId(isTopLevel: false)}";
}
// Special handling for anonymous types
if (type.Name.StartsWith("<>f", StringComparison.Ordinal))
{

Просмотреть файл

@ -35,10 +35,10 @@ public class JsonTypeInfoExtensionsTests
public static IEnumerable<object[]> GetSchemaReferenceId_Data =>
[
[typeof(Todo), "Todo"],
[typeof(IEnumerable<Todo>), "ArrayOfTodo"],
[typeof(List<Todo>), "ArrayOfTodo"],
[typeof(IEnumerable<Todo>), null],
[typeof(List<Todo>), null],
[typeof(TodoWithDueDate), "TodoWithDueDate"],
[typeof(IEnumerable<TodoWithDueDate>), "ArrayOfTodoWithDueDate"],
[typeof(IEnumerable<TodoWithDueDate>), null],
[(new { Id = 1 }).GetType(), "AnonymousTypeOfint"],
[(new { Id = 1, Name = "Todo" }).GetType(), "AnonymousTypeOfintAndstring"],
[typeof(IFormFile), "IFormFile"],
@ -50,14 +50,14 @@ public class JsonTypeInfoExtensionsTests
[typeof(NotFound<TodoWithDueDate>), "NotFoundOfTodoWithDueDate"],
[typeof(TestDelegate), "TestDelegate"],
[typeof(Container.ContainedTestDelegate), "ContainedTestDelegate"],
[typeof(List<int>), "ArrayOfint"],
[typeof(List<List<int>>), "ArrayOfArrayOfint"],
[typeof(int[]), "ArrayOfint"],
[typeof(List<int>), null],
[typeof(List<List<int>>), null],
[typeof(int[]), null],
[typeof(ValidationProblemDetails), "ValidationProblemDetails"],
[typeof(ProblemDetails), "ProblemDetails"],
[typeof(Dictionary<string, string[]>), "DictionaryOfstringAndArrayOfstring"],
[typeof(Dictionary<string, List<string[]>>), "DictionaryOfstringAndArrayOfArrayOfstring"],
[typeof(Dictionary<string, IEnumerable<string[]>>), "DictionaryOfstringAndArrayOfArrayOfstring"],
[typeof(Dictionary<string, string[]>), null],
[typeof(Dictionary<string, List<string[]>>), null],
[typeof(Dictionary<string, IEnumerable<string[]>>), null],
];
[Theory]

Просмотреть файл

@ -185,7 +185,11 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ArrayOfint"
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
}
}
},
@ -215,7 +219,11 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ArrayOfint"
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
}
}
},
@ -267,7 +275,11 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DictionaryOfstringAndint"
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int32"
}
}
}
}
@ -286,7 +298,11 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DictionaryOfstringAndint"
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int32"
}
}
}
}
@ -375,20 +391,6 @@
}
}
},
"ArrayOfint": {
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
},
"DictionaryOfstringAndint": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int32"
}
},
"Person": {
"required": [
"discriminator"

Просмотреть файл

@ -16,7 +16,11 @@
"in": "query",
"required": true,
"schema": {
"$ref": "#/components/schemas/ArrayOfGuid"
"type": "array",
"items": {
"type": "string",
"format": "uuid"
}
}
},
{
@ -34,7 +38,11 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ArrayOfGuid"
"type": "array",
"items": {
"type": "string",
"format": "uuid"
}
}
}
}
@ -117,13 +125,6 @@
},
"components": {
"schemas": {
"ArrayOfGuid": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
}
},
"Todo": {
"required": [
"id",

Просмотреть файл

@ -23,7 +23,18 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ArrayOfstring"
"type": "array",
"items": {
"type": "string",
"externalDocs": {
"description": "Documentation for this OpenAPI schema",
"url": "https://example.com/api/docs/schemas/string"
}
},
"externalDocs": {
"description": "Documentation for this OpenAPI schema",
"url": "https://example.com/api/docs/schemas/array"
}
}
}
}
@ -47,24 +58,7 @@
}
}
},
"components": {
"schemas": {
"ArrayOfstring": {
"type": "array",
"items": {
"type": "string",
"externalDocs": {
"description": "Documentation for this OpenAPI schema",
"url": "https://example.com/api/docs/schemas/string"
}
},
"externalDocs": {
"description": "Documentation for this OpenAPI schema",
"url": "https://example.com/api/docs/schemas/array"
}
}
}
},
"components": { },
"tags": [
{
"name": "users"

Просмотреть файл

@ -242,11 +242,11 @@ public partial class OpenApiSchemaServiceTests : OpenApiDocumentServiceTestBase
var enumerableTodoSchema = enumerableTodo.RequestBody.Content["application/json"].Schema;
var arrayTodoSchema = arrayTodo.RequestBody.Content["application/json"].Schema;
// Assert that both IEnumerable<Todo> and Todo[] map to the same schemas
Assert.Equal(enumerableTodoSchema.Reference.Id, arrayTodoSchema.Reference.Id);
// Assert that both IEnumerable<Todo> and Todo[] have items that map to the same schema
Assert.Equal(enumerableTodoSchema.Items.Reference.Id, arrayTodoSchema.Items.Reference.Id);
// Assert all types materialize as arrays
Assert.Equal("array", enumerableTodoSchema.GetEffective(document).Type);
Assert.Equal("array", arrayTodoSchema.GetEffective(document).Type);
Assert.Equal("array", enumerableTodoSchema.Type);
Assert.Equal("array", arrayTodoSchema.Type);
Assert.Equal("array", parameter.Schema.Type);
Assert.Equal("string", parameter.Schema.Items.Type);
@ -255,7 +255,7 @@ public partial class OpenApiSchemaServiceTests : OpenApiDocumentServiceTestBase
// Assert the array items are the same as the Todo schema
foreach (var element in new[] { enumerableTodoSchema, arrayTodoSchema })
{
Assert.Collection(element.GetEffective(document).Items.GetEffective(document).Properties,
Assert.Collection(element.Items.GetEffective(document).Properties,
property =>
{
Assert.Equal("id", property.Key);

Просмотреть файл

@ -230,31 +230,37 @@ public class OpenApiSchemaReferenceTransformerTests : OpenApiDocumentServiceTest
var requestBodySchema2 = requestBody2.Schema;
// {
// "$ref": "#/components/schemas/TodoArray"
// }
// "type": "array",
// "items": {
// "$ref": "#/components/schemas/Todo"
// }
// {
// "$ref": "#/components/schemas/TodoArray"
// }
// "type": "array",
// "items": {
// "$ref": "#/components/schemas/Todo"
// }
// {
// "components": {
// "schemas": {
// "TodoArray": {
// "type": "array",
// "items": {
// "$ref": "#/components/schemas/Todo"
// "type": "object",
// "properties": {
// ...
// }
// }
// }
// }
// }
// Both list types should point to the same reference ID
Assert.Equal(requestBodySchema.Reference.Id, requestBodySchema2.Reference.Id);
// The referenced schema has an array type
Assert.Equal("array", requestBodySchema.GetEffective(document).Type);
// The items in the array are mapped to the Todo reference
Assert.NotNull(requestBodySchema.GetEffective(document).Items.Reference.Id);
Assert.Equal(4, requestBodySchema.GetEffective(document).Items.GetEffective(document).Properties.Count);
// Both list types should be inlined
Assert.Null(requestBodySchema.Reference);
Assert.Equal(requestBodySchema.Reference, requestBodySchema2.Reference);
// And have an `array` type
Assert.Equal("array", requestBodySchema.Type);
// With an `items` sub-schema should consist of a $ref to Todo
Assert.Equal("Todo", requestBodySchema.Items.Reference.Id);
Assert.Equal(requestBodySchema.Items.Reference.Id, requestBodySchema2.Items.Reference.Id);
Assert.Equal(4, requestBodySchema.Items.GetEffective(document).Properties.Count);
});
}
@ -289,58 +295,4 @@ public class OpenApiSchemaReferenceTransformerTests : OpenApiDocumentServiceTest
Assert.False(responseSchema.GetEffective(document).Extensions.TryGetValue("x-my-extension", out var _));
});
}
[Fact]
public static async Task ProducesStableSchemaRefsForListOf()
{
// Arrange
var builder = CreateBuilder();
// Act
builder.MapPost("/api", (List<Todo> todo) => { });
builder.MapPost("/api-2", (List<Todo> todo) => { });
// Assert -- call twice to ensure the schema reference is stable
await VerifyOpenApiDocument(builder, VerifyDocument);
await VerifyOpenApiDocument(builder, VerifyDocument);
static void VerifyDocument(OpenApiDocument document)
{
var operation = document.Paths["/api"].Operations[OperationType.Post];
var requestBody = operation.RequestBody.Content["application/json"];
var requestBodySchema = requestBody.Schema;
var operation2 = document.Paths["/api-2"].Operations[OperationType.Post];
var requestBody2 = operation2.RequestBody.Content["application/json"];
var requestBodySchema2 = requestBody2.Schema;
// {
// "$ref": "#/components/schemas/TodoList"
// }
// {
// "$ref": "#/components/schemas/TodoList"
// }
// {
// "components": {
// "schemas": {
// "ArrayOfTodo": {
// "type": "array",
// "items": {
// "$ref": "#/components/schemas/Todo"
// }
// }
// }
// }
// }
// Both list types should point to the same reference ID
Assert.Equal("ArrayOfTodo", requestBodySchema.Reference.Id);
Assert.Equal(requestBodySchema.Reference.Id, requestBodySchema2.Reference.Id);
// The referenced schema has an array type
Assert.Equal("array", requestBodySchema.GetEffective(document).Type);
var itemsSchema = requestBodySchema.GetEffective(document).Items;
Assert.Equal("Todo", itemsSchema.Reference.Id);
Assert.Equal(4, itemsSchema.GetEffective(document).Properties.Count);
}
}
}