зеркало из https://github.com/dotnet/aspnetcore.git
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:
Родитель
2f79c475a7
Коммит
e7fa345782
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче