Swashbuckle doesn't know what MIME type each example response will be returned as, so the UI shows a combo box of all MIME types:

My proposal is for SwaggerResponseAttribute to also accept a MIME type parameter:
[SwaggerResponse(
StatusCodes.Status201Created,
"The car was created.",
typeof(Car),
"application/vnd.restful+json")]
[SwaggerResponse(
StatusCodes.Status400BadRequest,
"The car is invalid.",
typeof(ProblemDetails),
"application/problem+json")]
You'll need to elaborate further. Currently, Swashbuckle lists all mime types that are _supported_ by your ASP.Net Core application - e.g. via Produces/Consumes filters (controller-level, action-level or global) or, if you need more granular control, by tweaking the configured InputFormatters and OutputFormatters in you MVC config. I've tried to explain this in more detail here so I'd advise you to read that for more context.
If you still feel there's an issue or missing feature, please elaborate on your use-case a little further. Thanks
I'm familiar with the input/output formatters and produces/consumes. These work globally or at the the controller action level.
Swagger has the ability to show multiple examples of responses and each one can have a different MIME type, so it's kind of working at a lower level. Currently the UI shows this in a combo box (See screenshot above). I'd like to specify exactly which MIME type the consumer can expect for each swagger response.
Bearing in mind that Swagger/OpenAPI is an open-source specification and the swagger-ui is a tool that's powered by that specification, and that both are developed independently of Swashbuckle, it might be more appropriate for you to provide some example Swagger/OpenAPI JSON that will drive the behavior you're looking for in the UI.
I think that will make it easier for me to understand your use case and figure out the best way for Swashbuckle to generate the required Swagger/OpenAPI JSON.
Here is an example of an OpenAPI JSON file:
{
"openapi": "3.0.1",
"info": {
"title": "PROJECT-TITLE-XML",
"description": "PROJECT-DESCRIPTION-XML",
"version": "1.0"
},
"paths": {
"/cars": {
"options": {
"tags": [
"Cars"
],
"summary": "Returns an Allow HTTP header with the allowed HTTP methods.",
"operationId": "CarsOptionsCars",
"parameters": [
{
"name": "api-version",
"in": "query",
"description": "The requested API version",
"schema": {
"type": "string",
"default": "1.0"
}
}
],
"responses": {
"500": {
"description": "The MIME type in the Accept HTTP header is not acceptable.",
"content": {
"application/vnd.restful+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/problem+xml": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"200": {
"description": "The allowed HTTP methods."
}
}
}
}
}
}
I'd like to further constrain the responses, so if I add the following attribute:
[SwaggerResponse(
StatusCodes.Status500InternalServerError,
"Something bad happened.",
typeof(ProblemDetails),
"application/problem+json",
"application/problem+xml")]
The JSON should come back as:
{
"openapi": "3.0.1",
"info": {
"title": "PROJECT-TITLE-XML",
"description": "PROJECT-DESCRIPTION-XML",
"version": "1.0"
},
"paths": {
"/cars": {
"options": {
"tags": [
"Cars"
],
"summary": "Returns an Allow HTTP header with the allowed HTTP methods.",
"operationId": "CarsOptionsCars",
"parameters": [
{
"name": "api-version",
"in": "query",
"description": "The requested API version",
"schema": {
"type": "string",
"default": "1.0"
}
}
],
"responses": {
"500": {
"description": "The MIME type in the Accept HTTP header is not acceptable.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/problem+xml": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"200": {
"description": "The allowed HTTP methods."
}
}
}
}
}
}
Hmmm ... the general philosophy of Swashbuckle (with the Annotations library being the one exception) is to generate API descriptions according to how the application _actually_ behaves in reality. In other words, the implementation drives the documentation.
So, with that said, I'm wondering how you're implementing the behavior you've described in ASP.NET Core. That is, how are you causing the action method to allow different values in the Accept header depending on whether or not the response is 200, 400 etc.?
My use case is pretty simple: I want to return an application/pdf when status is 200, or an application/json in case of errors.
My application's behaviour is already like that, I just can't figure out how to have a more precise openapi output :)
I needed the same thing so I wrote an attribute and operation filter to achieve this. I'm sure it doesn't cover all Type cases, for example nullables, but it should be obvious what's going on to extend it for specific use cases.
public class SwaggerResponseMimeTypeOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var attrs = context.MethodInfo
.GetCustomAttributes(true)
.OfType<SwaggerResponseMimeTypeAttribute>()
.ToList();
var declType = context.MethodInfo.DeclaringType;
while (declType != null)
{
attrs.AddRange(declType
.GetCustomAttributes(true)
.OfType<SwaggerResponseMimeTypeAttribute>());
declType = declType.DeclaringType;
}
if (attrs.Any())
{
foreach (var attr in attrs)
{
HttpStatusCode statusCode = (HttpStatusCode)attr.StatusCode;
string statusString = attr.StatusCode.ToString();
if (!operation.Responses.TryGetValue(statusString, out OpenApiResponse response))
{
response = new OpenApiResponse();
operation.Responses.Add(statusString, response);
}
if (!string.IsNullOrEmpty(attr.Description))
response.Description = attr.Description;
else if (string.IsNullOrEmpty(response.Description))
response.Description = statusCode.ToString();
response.Content ??= new Dictionary<string, OpenApiMediaType>();
var openApiMediaType = new OpenApiMediaType();
string swaggerDataType =
IsNumericType(attr.Type) ? "number"
: IsStringType(attr.Type) ? "string"
: IsBooleanType(attr.Type) ? "boolean"
: null;
if (swaggerDataType == null)
{
// this is not a native type, try to register it in the repository
if (!context.SchemaRepository.TryLookupByType(attr.Type, out var schema))
{
schema = context.SchemaGenerator.GenerateSchema(attr.Type, context.SchemaRepository);
if (schema == null)
throw new InvalidOperationException($"Failed to register swagger schema type '{attr.Type.Name}'");
}
openApiMediaType.Schema = schema;
}
else
{
openApiMediaType.Schema = new OpenApiSchema
{
Type = swaggerDataType
};
}
foreach (string mediaType in attr.MediaTypes)
response.Content.Add(mediaType, openApiMediaType);
}
}
}
private bool IsNumericType(Type type)
{
switch (Type.GetTypeCode(type))
{
case TypeCode.Decimal:
case TypeCode.Double:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.Single:
case TypeCode.UInt16:
case TypeCode.UInt32:
case TypeCode.UInt64:
return true;
default:
return false;
}
}
private bool IsStringType(Type type)
{
switch (Type.GetTypeCode(type))
{
case TypeCode.String:
return true;
default:
return false;
}
}
private bool IsBooleanType(Type type)
{
switch (Type.GetTypeCode(type))
{
case TypeCode.Boolean:
return true;
default:
return false;
}
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class SwaggerResponseMimeTypeAttribute : Attribute
{
public int StatusCode { get; set; }
public Type Type { get; set; }
public string[] MediaTypes { get; set; }
public string Description { get; set; }
public SwaggerResponseMimeTypeAttribute(int statusCode, Type type, params string[] mediaTypes)
{
if (type == null) throw new ArgumentNullException(nameof(type));
if (!mediaTypes?.Any() ?? true) throw new ArgumentNullException(nameof(mediaTypes));
StatusCode = statusCode;
Type = type;
MediaTypes = mediaTypes;
}
}
Register and usage
services.AddSwaggerGen(c =>
{
c.OperationFilter<SwaggerResponseMimeTypeOperationFilter>();
}
//------
[SwaggerResponseMimeType((int)HttpStatusCode.Created, typeof(string), MediaTypeNames.Text.Plain, Description = "my 201 description")]
[SwaggerResponseMimeType((int)HttpStatusCode.NotAcceptable, typeof(ProblemDetails), MediaTypeNames.Application.Json, "app/mycustomtype", Description = "my 406 description")]
public async Task<ActionResult<string>>MyApi() {}
Produces swagger json
"201": {
"description": "my 201 description",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
},
"406": {
"description": "my 406 description",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"app/mycustomtype": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
@ErikPilsits-RJW's your implementation is a great start. Only missing the ability to pass multiple MIME types.
@domaindrivendev Would you be willing to take something like this but built into [SwaggerResponse]?
[SwaggerResponse(
StatusCodes.Status500InternalServerError,
"Something bad happened.",
typeof(ProblemDetails),
"application/problem+json",
"application/problem+xml")]
Updated to support multiple mime types, fixes an issue finding registered schema types when the schema repo decides to register the name differently, and walks the method hierarchy all the way up looking for the attribute.
@RehanSaeed - if you want to create a PR to enrich the existing SwaggerResponse attribute as you've described above, I'd be happy to merge it in.
Raised https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/1956 based on the excellent work by @ErikPilsits-RJW. Not sure if completely correct as I'm not an expert on the internals of Swashbuckle.
This is exactly what I need, awesome job. Any idea when this will be merged?
@AnnaGuzy I provided feedback in the MR but have not received any follow up yet from the submitter. @RehanSaeed are you in a position to address the MR feedback.
@domaindrivendev I don't see any comments in the PR https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/1956. Am I missing something?
Doh! I created the comments but never actually submitted. You should see them now
@domaindrivendev and @RehanSaeed, what is the status of this PR? I'm also a bit confused because I've read that SwaggerResponse is no longer supported in recent versions of Swashbuckle.
I also need to specify content types per status code. For example, when returning a 201 (Created) response, the content type would be application/json. However, when returning 400 (Bad Request) or 404 (Not Found), the content type would be application/problem+json rather than application/json.
Most helpful comment
My use case is pretty simple: I want to return an
application/pdfwhen status is 200, or anapplication/jsonin case of errors.My application's behaviour is already like that, I just can't figure out how to have a more precise openapi output :)