[ProducesResponseType]
Declare an action's status codes and response bodies for OpenAPI and ApiExplorer with [ProducesResponseType], and know it never changes runtime behaviour.
[ProducesResponseType<Order>(StatusCodes.Status200OK)] | [ProducesResponseType(typeof(Order), 200, "application/json")] | [ProducesResponseType(404)]- Added in .NET ASP.NET Core 1.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$statusCode | int | The HTTP status code being documented. Use the StatusCodes constants. |
$type / Toptional | Type= void, or ProblemDetails for client errors under [ApiController] | The response body type. Omit for bodiless responses; for 4xx/5xx in [ApiController] controllers ProblemDetails is inferred. |
$contentTypeoptional | string= the configured output formatters | Content type of the response, with optional additionalContentTypes, for non-JSON bodies such as text/csv. |
$Descriptionoptional | string?= null | Text for the response's description in the OpenAPI document. |
Return value
n/aAdds response metadata (IProducesResponseTypeMetadata) that ApiExplorer and OpenAPI generators read. It has no effect on what the action actually returns.
Errors
Nothing. A mismatch between the attribute and the action's real responses is silent; only tests or OpenAPI diffs catch it.
Changelog
- PHP ASP.NET Core 2.1With [ApiController], client error responses without a type are documented as ProblemDetails.
OpenAPI generators cannot run your action to see what it returns. For a controller action returning IActionResult they only know the method signature, so they guess "200, no schema". [ProducesResponseType] states each possible status code and body type as metadata; ApiExplorer turns that into SupportedResponseTypes, and Microsoft.AspNetCore.OpenApi turns those into the responses section of the document.
Metadata only
The attribute does not filter, convert or validate anything. Declare 201 and return 200, or declare Order and return a string, and the client gets what the code returns while the documentation says otherwise. Generated clients then fail at runtime on exactly the paths nobody tested. This is the main argument for return types that carry the information themselves.
When you need it
| Handler returns | What OpenAPI learns without attributes | Add |
|---|---|---|
IActionResult | Nothing useful | One attribute per status code |
ActionResult<Order> | 200 with Order | The other codes (404, 409, 201 instead of 200) |
Minimal API Results<Ok<Order>, NotFound> | Every status and type | Nothing |
Minimal API IResult | Nothing useful | .Produces<Order>(200), .ProducesProblem(404) |
In minimal APIs prefer TypedResults with Results<...>: the compiler checks that the handler can only return the declared outcomes, so the documentation cannot drift. In controllers use ActionResult<T> and attributes for the remaining codes.
Details worth knowing
- The generic form
[ProducesResponseType<Order>(200)]is shorter thantypeof(Order); both produce the same metadata. - Under
[ApiController], a 4xx without a type is documented asProblemDetails, matching what client error mapping returns. - Attributes on the controller class apply to every action, a good place for 401 and 403 on a protected controller.
[ProducesDefaultResponseType]documents a catch-all error response; API conventions ([ApiConventionType(typeof(DefaultApiConventions))]) apply standard sets by method name, but explicit attributes are easier to review.
Production failure mode
Drift. Someone adds return Conflict() and no attribute; the generated TypeScript client has no case for 409 and treats it as an unknown error. Snapshot the OpenAPI document in CI and fail the build on unreviewed changes, and add integration tests for each documented status.
Example
When to use it
- Document that GET /api/invoices/{id} returns an Invoice or a 404 problem details body so generated clients handle both
- Declare text/csv as the response of a sales export so API consumers do not try to parse it as JSON
- Add 401 to every action of a protected controller with one class-level attribute
Frequently asked questions
Does [ProducesResponseType(201)] make the action return 201?
No. It is metadata for documentation only. The action's return value decides the status; use CreatedAtAction or TypedResults.Created to actually return 201.
Do minimal APIs need [ProducesResponseType]?
Rarely. TypedResults with a Results<...> return type document themselves. For handlers returning IResult use .Produces<T>(), .ProducesProblem() and .ProducesValidationProblem().
Why does my 404 show ProblemDetails in the document?
Under [ApiController], client error responses declared without a type are documented as ProblemDetails, because client error mapping returns a problem details body for them at runtime.
More examples
Minimal APIs: let the return type document itself
<code>Results<Ok<Order>, NotFound, ProblemHttpResult></code> implements the response metadata itself, and the compiler rejects a return of any other type, so the OpenAPI document cannot drift from the code. The fluent <code>.Produces</code> methods are the minimal API counterpart of the attribute.
using Microsoft.AspNetCore.Http.HttpResults;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
app.MapOpenApi();
// Every status code and body type is part of the signature: no attributes needed.
app.MapGet("/orders/{id:int}", Results<Ok<Order>, NotFound, ProblemHttpResult> (int id) =>
id switch
{
<= 0 => TypedResults.Problem("Order ids start at 1.", statusCode: StatusCodes.Status400BadRequest),
> 100_000 => TypedResults.NotFound(),
_ => TypedResults.Ok(new Order(id, "CUST-1042", 89.90m)),
});
// Handlers returning IResult need the metadata spelled out.
app.MapPost("/orders", (Order order) => Results.Created($"/orders/{order.Id}", order))
.Produces<Order>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.ProducesProblem(StatusCodes.Status409Conflict);
app.Run();
public record Order(int Id, string CustomerId, decimal Total);
A non-JSON response
The content type argument replaces the JSON media types the output formatters would otherwise list, so the document tells clients to expect <code>text/csv</code>. Without it, generated clients try to parse the CSV as JSON.
using System.Text;
using Microsoft.AspNetCore.Mvc;
namespace Shop.Api;
[ApiController]
[Route("api/reports")]
public class ReportsController : ControllerBase
{
[HttpGet("sales.csv")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK, "text/csv")]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult SalesCsv([FromQuery] DateOnly from, [FromQuery] DateOnly to)
{
if (to < from) return BadRequest();
var csv = new StringBuilder("date,orders,revenue\n")
.Append($"{from:yyyy-MM-dd},42,3150.75\n")
.ToString();
return File(Encoding.UTF8.GetBytes(csv), "text/csv", $"sales-{from:yyyyMMdd}-{to:yyyyMMdd}.csv");
}
}
Catch documentation drift in CI
Committing the OpenAPI document and diffing it in CI turns every change to status codes, schemas and descriptions into a reviewed change. It does not prove the attributes are true, so pair it with integration tests for each documented status code.
# Generate the document at build time (Microsoft.Extensions.ApiDescription.Server)
# or fetch it from a test host, then compare with the committed copy.
dotnet build src/Shop.Api -c Release
cp src/Shop.Api/obj/Shop.Api.json /tmp/openapi-new.json
if ! diff -u docs/openapi.json /tmp/openapi-new.json; then
echo "OpenAPI document changed. Review it and commit docs/openapi.json." >&2
exit 1
fi
Discussion