Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -1003,10 +1003,15 @@ private OpenApiResponses GenerateResponses(
.OrderBy((responseType) => responseType.IsDefaultResponse() ? 1 : 0);

var responses = new OpenApiResponses();
foreach (var responseType in supportedResponseTypes)

// Multiple response types can be reported for the same status code, e.g. several
// [ProducesResponseType] attributes with the same code, or a C# union return type
// (.NET 11+) which the framework may surface as one entry per case.
foreach (var responsesForStatusCode in supportedResponseTypes.GroupBy(
(responseType) => responseType.IsDefaultResponse() ? "default" : responseType.StatusCode.ToString()))
{
var statusCode = responseType.IsDefaultResponse() ? "default" : responseType.StatusCode.ToString();
responses.Add(statusCode, GenerateResponse(apiDescription, schemaRepository, statusCode, responseType));
var statusCode = responsesForStatusCode.Key;
responses.Add(statusCode, GenerateResponse(apiDescription, schemaRepository, statusCode, [.. responsesForStatusCode]));
}
return responses;
}
Expand All @@ -1015,12 +1020,14 @@ private OpenApiResponse GenerateResponse(
ApiDescription apiDescription,
SchemaRepository schemaRepository,
string statusCode,
ApiResponseType apiResponseType)
IReadOnlyList<ApiResponseType> apiResponseTypes)
{
string description = null;

#if NET10_0_OR_GREATER
description = apiResponseType.Description;
description = apiResponseTypes
.Select((responseType) => responseType.Description)
.FirstOrDefault((value) => !string.IsNullOrEmpty(value));
#endif

if (string.IsNullOrEmpty(description))
Expand All @@ -1030,18 +1037,54 @@ private OpenApiResponse GenerateResponse(
.Value;
}

var responseContentTypes = InferResponseContentTypes(apiDescription, apiResponseType);
// Collect the schemas contributed by each response type, grouped by content type and
// preserving the order in which they are encountered. A model type is only emitted once
// per content type so identical response types do not produce duplicate schemas.
var contentTypes = new List<string>();
var schemasByContentType = new Dictionary<string, List<IOpenApiSchema>>();
var seenModelTypesByContentType = new Dictionary<string, HashSet<Type>>();

foreach (var apiResponseType in apiResponseTypes)
{
var modelType = apiResponseType.ModelMetadata?.ModelType ?? apiResponseType.Type;

foreach (var contentType in InferResponseContentTypes(apiDescription, apiResponseType))
{
if (!schemasByContentType.TryGetValue(contentType, out var schemas))
{
contentTypes.Add(contentType);
schemas = [];
schemasByContentType[contentType] = schemas;
seenModelTypesByContentType[contentType] = [];
}

if (seenModelTypesByContentType[contentType].Add(modelType ?? typeof(void)))
{
schemas.Add(GenerateSchema(modelType, schemaRepository));
}
}
}

return new OpenApiResponse
{
Description = description,
Content = responseContentTypes.ToDictionary(
Content = contentTypes.ToDictionary(
(contentType) => contentType,
(contentType) => CreateResponseMediaType(apiResponseType.ModelMetadata?.ModelType ?? apiResponseType.Type, schemaRepository)
(contentType) => new OpenApiMediaType { Schema = CombineResponseSchemas(schemasByContentType[contentType]) }
)
};
}

// When more than one distinct schema is reported for the same status code and content type
// (e.g. multiple response types or the cases of a C# union), combine them into an anyOf schema.
// This mirrors how System.Text.Json's JsonSchemaExporter emits union types.
private static IOpenApiSchema CombineResponseSchemas(List<IOpenApiSchema> schemas)
{
return schemas.Count == 1
? schemas[0]
: new OpenApiSchema { AnyOf = [.. schemas] };
}

private static IEnumerable<string> InferResponseContentTypes(ApiDescription apiDescription, ApiResponseType apiResponseType)
{
// If there's no associated model type, return an empty list (i.e. no content)
Expand All @@ -1067,14 +1110,6 @@ private static IEnumerable<string> InferResponseContentTypes(ApiDescription apiD
.Distinct()];
}

private OpenApiMediaType CreateResponseMediaType(Type modelType, SchemaRepository schemaRepository)
{
return new OpenApiMediaType
{
Schema = GenerateSchema(modelType, schemaRepository)
};
}

private static bool IsFromFormAttributeUsedWithIFormFile(ApiParameterDescription apiParameter)
{
var parameterInfo = apiParameter.ParameterInfo();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ public async Task SwaggerMiddleware_CanBeConfiguredMultipleTimes(
[Theory]
[InlineData(typeof(MinimalApp.Program), "/swagger/v1/swagger.json")]
[InlineData(typeof(MinimalAppWithNullableEnums.Program), "/swagger/v1/swagger.json")]
[InlineData(typeof(MultipleResponseTypes.Program), "/swagger/v1/swagger.json")]
[InlineData(typeof(MvcWithNullable.Program), "/swagger/v1/swagger.json")]
[InlineData(typeof(TodoApp.Program), "/swagger/v1/swagger.json")]
[InlineData(typeof(TopLevelSwaggerDoc.Program), "/swagger/v1.json")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ await Verify(swagger)
[Theory]
[InlineData(typeof(MinimalApp.Program), "/swagger/v1/swagger.json")]
[InlineData(typeof(MinimalAppWithNullableEnums.Program), "/swagger/v1/swagger.json")]
[InlineData(typeof(MultipleResponseTypes.Program), "/swagger/v1/swagger.json")]
[InlineData(typeof(MvcWithNullable.Program), "/swagger/v1/swagger.json")]
[InlineData(typeof(TodoApp.Program), "/swagger/v1/swagger.json")]
[InlineData(typeof(TopLevelSwaggerDoc.Program), "/swagger/v1.json")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"openapi": "3.0.4",
"info": {
"title": "MultipleResponseTypes",
"version": "v1"
},
"paths": {
"/animals/{id}": {
"get": {
"tags": [
"MultipleResponseTypes"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/Cat"
},
{
"$ref": "#/components/schemas/Dog"
}
]
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Cat": {
"type": "object",
"properties": {
"name": {
"type": "string",
"nullable": true
},
"livesLeft": {
"type": "integer",
"format": "int32"
}
},
"additionalProperties": false
},
"Dog": {
"type": "object",
"properties": {
"name": {
"type": "string",
"nullable": true
},
"goodBoy": {
"type": "boolean"
}
},
"additionalProperties": false
}
}
},
"tags": [
{
"name": "MultipleResponseTypes"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"openapi": "3.0.4",
"info": {
"title": "MultipleResponseTypes",
"version": "v1"
},
"paths": {
"/animals/{id}": {
"get": {
"tags": [
"MultipleResponseTypes"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/Cat"
},
{
"$ref": "#/components/schemas/Dog"
}
]
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Cat": {
"type": "object",
"properties": {
"name": {
"type": "string",
"nullable": true
},
"livesLeft": {
"type": "integer",
"format": "int32"
}
},
"additionalProperties": false
},
"Dog": {
"type": "object",
"properties": {
"name": {
"type": "string",
"nullable": true
},
"goodBoy": {
"type": "boolean"
}
},
"additionalProperties": false
}
}
},
"tags": [
{
"name": "MultipleResponseTypes"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"openapi": "3.0.4",
"info": {
"title": "MultipleResponseTypes",
"version": "v1"
},
"paths": {
"/animals/{id}": {
"get": {
"tags": [
"MultipleResponseTypes"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/Cat"
},
{
"$ref": "#/components/schemas/Dog"
}
]
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Cat": {
"type": "object",
"properties": {
"name": {
"type": "string",
"nullable": true
},
"livesLeft": {
"type": "integer",
"format": "int32"
}
},
"additionalProperties": false
},
"Dog": {
"type": "object",
"properties": {
"name": {
"type": "string",
"nullable": true
},
"goodBoy": {
"type": "boolean"
}
},
"additionalProperties": false
}
}
},
"tags": [
{
"name": "MultipleResponseTypes"
}
]
}
Loading