[ProducesResponseType]

Declare an action's status codes and response bodies for OpenAPI and ApiExplorer with [ProducesResponseType], and know it never changes runtime behaviour.

Signature
[ProducesResponseType<Order>(StatusCodes.Status200OK)]  |  [ProducesResponseType(typeof(Order), 200, "application/json")]  |  [ProducesResponseType(404)]
  • Added in .NET ASP.NET Core 1.0

Parameters

ParameterTypeDescription
$statusCodeintThe HTTP status code being documented. Use the StatusCodes constants.
$type / ToptionalType= 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.
$contentTypeoptionalstring= the configured output formattersContent type of the response, with optional additionalContentTypes, for non-JSON bodies such as text/csv.
$Descriptionoptionalstring?= nullText 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 returnsWhat OpenAPI learns without attributesAdd
IActionResultNothing usefulOne attribute per status code
ActionResult<Order>200 with OrderThe other codes (404, 409, 201 instead of 200)
Minimal API Results<Ok<Order>, NotFound>Every status and typeNothing
Minimal API IResultNothing 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 than typeof(Order); both produce the same metadata.
  • Under [ApiController], a 4xx without a type is documented as ProblemDetails, 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

Try it yourself
Loading editor…
Press Run to execute the code.

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&lt;Ok&lt;Order&gt;, NotFound, ProblemHttpResult&gt;</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.

Example · csharp
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.

Example · csharp
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.

Example · bash
# 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

  • Be the first to comment on this lesson.