Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
This article explains how to add OpenAPI metadata in an ASP.NET Core app.
Include OpenAPI metadata for endpoints
ASP.NET collects metadata from the web app's endpoints and uses it to generate an OpenAPI document.
In controller-based apps, metadata is collected from attributes such as [EndpointDescription]
, [HttpPost]
,
and [Produces]
when the controller has the [ApiController]
attribute.
In minimal APIs, metadata can be collected from attributes but may also be set by using extension methods and other strategies, such as returning TypedResults from route handlers.
The following table provides an overview of the metadata collected and the strategies for setting it.
Metadata | Attribute | Extension method | Other strategies |
---|---|---|---|
summary | [EndpointSummary] |
WithSummary | |
description | [EndpointDescription] |
WithDescription | |
tags | [Tags] |
WithTags | |
operationId | [EndpointName] |
WithName | |
parameters | [FromQuery] , [FromRoute] , [FromHeader] , [FromForm] |
||
parameter description | [Description] |
||
requestBody | [FromBody] |
Accepts | |
responses | [Produces] |
Produces, ProducesProblem | TypedResults |
Excluding endpoints | [ExcludeFromDescription] , [ApiExplorerSettings] |
ExcludeFromDescription |
ASP.NET Core can also collect metadata from XML doc comments. For more information, see ASP.NET Core OpenAPI XML documentation comment support in ASP.NET Core for more details.
The following sections demonstrate how to include metadata in an app to customize the generated OpenAPI document.
Summary and description
The endpoint summary and description can be set using the [EndpointSummary]
and [EndpointDescription]
attributes,
or in minimal APIs, using the WithSummary and WithDescription extension methods.
The following sample demonstrates the different strategies for setting summaries and descriptions.
Note that the attributes are placed on the delegate method and not on the app.MapGet method.
app.MapGet("/extension-methods", () => "Hello world!")
.WithSummary("This is a summary.")
.WithDescription("This is a description.");
app.MapGet("/attributes",
[EndpointSummary("This is a summary.")]
[EndpointDescription("This is a description.")]
() => "Hello world!");
tags
OpenAPI supports specifying tags on each endpoint as a form of categorization.
In minimal APIs, tags can be set using either the [Tags]
attribute or the WithTags extension method.
The following sample demonstrates the different strategies for setting tags.
app.MapGet("/extension-methods", () => "Hello world!")
.WithTags("todos", "projects");
app.MapGet("/attributes",
[Tags("todos", "projects")]
() => "Hello world!");
operationId
OpenAPI supports an operationId on each endpoint as a unique identifier or name for the operation.
In minimal APIs, the operationId can be set using either the [EndpointName]
attribute or the WithName extension method.
The following sample demonstrates the different strategies for setting the operationId.
app.MapGet("/extension-methods", () => "Hello world!")
.WithName("FromExtensionMethods");
app.MapGet("/attributes",
[EndpointName("FromAttributes")]
() => "Hello world!");
parameters
OpenAPI supports annotating path, query string, header, and cookie parameters that are consumed by an API.
The framework infers the types for request parameters automatically based on the signature of the route handler.
The [Description]
attribute can be used to provide a description for a parameter.
The [Description]
attribute works in an MVC app but doesn't work in a Minimal API app at this time. For more information, see Description
parameter of ProducesResponseTypeAttribute
does not work in minimal API app (dotnet/aspnetcore
#60518).
Describe the request body
The requestBody
field in OpenAPI describes the body of a request that an API client can send to the server, including the content type(s) supported and the schema for the body content.
When the endpoint handler method accepts parameters that are bound from the request body, ASP.NET Core generates a corresponding requestBody
for the operation in the OpenAPI document. Metadata for the request body can also be specified using attributes or extension methods. Additional metadata can be set with a document transformer or operation transformer.
If the endpoint doesn't define any parameters bound to the request body, but instead consumes the request body from the HttpContext directly, ASP.NET Core provides mechanisms to specify request body metadata. This is a common scenario for endpoints that process the request body as a stream.
Some request body metadata can be determined from the FromBody
or FromForm
parameters of the route handler method.
A description for the request body can be set with a [Description]
attribute on the parameter with FromBody
or FromForm
.
If the FromBody
parameter is non-nullable and EmptyBodyBehavior is not set to Allow in the FromBody
attribute, the request body is required and the required
field of the requestBody
is set to true
in the generated OpenAPI document.
Form bodies are always required and have required
set to true
.
Use a document transformer or an operation transformer to set the example
, examples
, or encoding
fields, or to add specification extensions for the request body in the generated OpenAPI document.
Other mechanisms for setting request body metadata depend on the type of app being developed and are described in the following sections.
The content types for the request body in the generated OpenAPI document are determined from the type of the parameter that is bound to the request body or specified with the Accepts extension method.
By default, the content type of a FromBody
parameter will be application/json
and the content type for FromForm
parameter(s) will be multipart/form-data
or application/x-www-form-urlencoded
.
Support for these default content types is built in to Minimal APIs, and other content types can be handled by using custom binding. See the Custom binding topic of the Minimal APIs documentation for more information.
There are several ways to specify a different content type for the request body.
If the type of the FromBody
parameter implements IEndpointParameterMetadataProvider, ASP.NET Core uses this interface to determine the content type(s) in the request body.
The framework uses the PopulateMetadata method of this interface to set the content type(s) and type of the body content of the request body. For example, a Todo
class that accepts either application/xml
or text/xml
content-type can use IEndpointParameterMetadataProvider to provide this information to the framework.
public class Todo : IEndpointParameterMetadataProvider
{
public static void PopulateMetadata(
ParameterInfo parameter,
EndpointBuilder builder)
{
builder.Metadata.Add(
new AcceptsMetadata(
["application/xml", "text/xml"],
typeof(Todo)
)
);
}
}
The Accepts extension method can also be used to specify the content type of the request body.
In the following example, the endpoint accepts a Todo
object in the request body with an expected content-type of application/xml
.
app.MapPut("/todos/{id}", (int id, Todo todo) => ...)
.Accepts<Todo>("application/xml");
Since application/xml
is not a built-in content type, the Todo
class must implement the IBindableFromHttpContext<TSelf> interface to provide a custom binding for the request body. For example:
public class Todo : IBindableFromHttpContext<Todo>
{
public static async ValueTask<Todo?> BindAsync(
HttpContext context,
ParameterInfo parameter)
{
var xmlDoc = await XDocument.LoadAsync(context.Request.Body, LoadOptions.None, context.RequestAborted);
var serializer = new XmlSerializer(typeof(Todo));
return (Todo?)serializer.Deserialize(xmlDoc.CreateReader());
}
}
If the endpoint doesn't define any parameters bound to the request body, use the Accepts extension method to specify the content type that the endpoint accepts.
If you specify <AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.Accepts%2A> multiple times, only metadata from the last one is used -- they aren't combined.
Describe response types
OpenAPI supports providing a description of the responses returned from an API. ASP.NET Core provides several strategies for setting the response metadata of an endpoint. Response metadata that can be set includes the status code, the type of the response body, and content type(s) of a response. Responses in OpenAPI may have additional metadata, such as description, headers, links, and examples. This additional metadata can be set with a document transformer or operation transformer.
The specific mechanisms for setting response metadata depend on the type of app being developed.
In Minimal API apps, ASP.NET Core can extract the response metadata added by extension methods on the endpoint, attributes on the route handler, and the return type of the route handler.
- The Produces extension method can be used on the endpoint to specify the status code, the type of the response body, and content type(s) of a response from an endpoint.
- The
[ProducesResponseType]
or ProducesResponseTypeAttribute<T> attribute can be used to specify the type of the response body. - A route handler can be used to return a type that implements IEndpointMetadataProvider to specify the type and content-type(s) of the response body.
- The ProducesProblem extension method on the endpoint can be used to specify the status code and content-type(s) of an error response.
Note that the Produces and ProducesProblem extension methods are supported on both RouteHandlerBuilder and on RouteGroupBuilder. This allows, for example, a common set of error responses to be defined for all operations in a group.
When not specified by one of the preceding strategies, the:
- Status code for the response defaults to 200.
- Schema for the response body can be inferred from the implicit or explicit return type of the endpoint method, for example, from
T
in Task<TResult>; otherwise, it's considered to be unspecified. - Content-type for the specified or inferred response body is "application/json".
In Minimal APIs, the Produces extension method and the [ProducesResponseType]
attribute only set the response metadata for the endpoint. They do not modify or constrain the behavior of the endpoint, which may return a different status code or response body type than specified by the metadata, and the content-type is determined by the return type of the route handler method, irrespective of any content-type specified in attributes or extension methods.
The Produces extension method can specify an endpoint's response type, with a default status code of 200 and a default content type of application/json
. The following example illustrates this:
app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
.Produces<IList<Todo>>();
The [ProducesResponseType]
can be used to add response metadata to an endpoint. Note that the attribute is applied to the route handler method, not the method invocation to create the route, as shown in the following example:
app.MapGet("/todos",
[ProducesResponseType<List<Todo>>(200)]
async (TodoDb db) => await db.Todos.ToListAsync());
[ProducesResponseType]
, [Produces]
, and [ProducesDefaultResponseType]
also support an optional string property called Description
that can be used to describe the response. This is useful for explaining why or when clients can expect a specific response:
app.MapGet("/todos/{id}",
[ProducesResponseType<Todo>(200,
Description = "Returns the requested Todo item.")]
[ProducesResponseType(404, Description = "Requested item not found.")]
[ProducesDefault(Description = "Undocumented status code.")]
async (int id, TodoDb db) => /* Code here */);
Using TypedResults in the implementation of an endpoint's route handler automatically includes the response type metadata for the endpoint. For example, the following code automatically annotates the endpoint with a response under the 200
status code with an application/json
content type.
app.MapGet("/todos", async (TodoDb db) =>
{
var todos = await db.Todos.ToListAsync();
return TypedResults.Ok(todos);
});
Only return types that implement IEndpointMetadataProvider create a responses
entry in the OpenAPI document. The following is a partial list of some of the TypedResults helper methods that produce a responses
entry:
TypedResults helper method |
Status code |
---|---|
Ok() | 200 |
Created() | 201 |
CreatedAtRoute() | 201 |
Accepted() | 202 |
AcceptedAtRoute() | 202 |
NoContent() | 204 |
BadRequest() | 400 |
ValidationProblem() | 400 |
NotFound() | 404 |
Conflict() | 409 |
UnprocessableEntity() | 422 |
All of these methods except NoContent
have a generic overload that specifies the type of the response body.
A class can be implemented to set the endpoint metadata and return it from the route handler.
Set responses for ProblemDetails
When setting the response type for endpoints that may return a ProblemDetails response, the following can be used to add the appropriate response metadata for the endpoint:
- ProducesProblem
- ProducesValidationProblem extension method.
- TypedResults with a status code in the (400-499) range.
For more information on how to configure a Minimal API app to return ProblemDetails responses, see Handle errors in minimal APIs.
Multiple response types
If an endpoint can return different response types in different scenarios, you can provide metadata in the following ways:
Call the Produces extension method multiple times, as shown in the following example:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) => await db.Todos.FindAsync(id) is Todo todo ? Results.Ok(todo) : Results.NotFound()) .Produces<Todo>(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound);
Use Results<TResult1,TResult2,TResult3,TResult4,TResult5,TResult6> in the signature and TypedResults in the body of the handler, as shown in the following example:
app.MapGet("/book/{id}", Results<Ok<Book>, NotFound> (int id, List<Book> bookList) => { return bookList.FirstOrDefault((i) => i.Id == id) is Book book ? TypedResults.Ok(book) : TypedResults.NotFound(); });
The
Results<TResult1,TResult2,TResultN>
union types declare that a route handler returns multipleIResult
-implementing concrete types, and any of those types that implement IEndpointMetadataProvider will contribute to the endpoint’s metadata.The union types implement implicit cast operators. These operators enable the compiler to automatically convert the types specified in the generic arguments to an instance of the union type. This capability has the added benefit of providing compile-time checking that a route handler only returns the results that it declares it does. Attempting to return a type that isn't declared as one of the generic arguments to
Results<TResult1,TResult2,TResultN>
results in a compilation error.
Exclude endpoints from the generated document
By default, all endpoints that are defined in an app are documented in the generated OpenAPI file, but endpoints can be excluded from the document using attributes or extension methods.
The mechanism for specifying an endpoint that should be excluded depends on the type of app being developed.
Minimal APIs support two strategies for excluding a given endpoint from the OpenAPI document:
- ExcludeFromDescription extension method
[ExcludeFromDescription]
attribute
The following sample demonstrates the different strategies for excluding a given endpoint from the generated OpenAPI document.
app.MapGet("/extension-method", () => "Hello world!")
.ExcludeFromDescription();
app.MapGet("/attributes",
[ExcludeFromDescription]
() => "Hello world!");
Include OpenAPI metadata for data types
C# classes or records used in request or response bodies are represented as schemas in the generated OpenAPI document. By default, only public properties are represented in the schema, but there are JsonSerializerOptions to also create schema properties for fields.
When the PropertyNamingPolicy is set to camel-case (this is the default
in ASP.NET web applications), property names in a schema are the camel-case form
of the class or record property name.
The [JsonPropertyName]
can be used on an individual property to specify the name
of the property in the schema.
type and format
Numeric types
The JSON Schema library maps standard C# numeric types to OpenAPI type
and format
based on the
NumberHandling property of the JsonSerializerOptions
used in the app. In ASP.NET Core Web API apps, the default value of this property is JsonNumberHandling.AllowReadingFromString
.
When the NumberHandling property is set to JsonNumberHandling.AllowReadingFromString
, the numeric types are mapped as follows:
C# Type | OpenAPI type |
OpenAPI format |
Other assertions |
---|---|---|---|
int | [integer,string] | int32 | pattern <digits> |
long | [integer,string] | int64 | pattern <digits> |
short | [integer,string] | int16 | pattern <digits> |
byte | [integer,string] | uint8 | pattern <digits> |
float | [number,string] | float | pattern <digits with decimal > |
double | [number,string] | double | pattern <digits with decimal > |
decimal | [number,string] | double | pattern <digits with decimal > |
If the app is configured to produce OpenAPI 3.0 or OpenAPI v2 documents, where the type
field cannot have an array value, the type
field is dropped.
When the NumberHandling property is set to JsonNumberHandling.Strict
, the numeric types are mapped as follows:
C# Type | OpenAPI type |
OpenAPI format |
---|---|---|
int | integer | int32 |
long | integer | int64 |
short | integer | int16 |
byte | integer | uint8 |
float | number | float |
double | number | double |
decimal | number | double |
String types
The following table shows how C# types map to string
type properties in the generated OpenAPI document:
C# Type | OpenAPI type |
OpenAPI format |
Other assertions |
---|---|---|---|
string | string | ||
char | string | char | minLength: 1, maxLength: 1 |
byte[] | string | byte | |
DateTimeOffset | string | date-time | |
DateOnly | string | date | |
TimeOnly | string | time | |
Uri | string | uri | |
Guid | string | uuid |
Other types
Other C# types are represented in the generated OpenAPI document as shown in the following table:
C# Type | OpenAPI type |
OpenAPI format |
---|---|---|
bool | boolean | |
object | omitted | |
dynamic | omitted |
Use attributes to add metadata
ASP.NET uses metadata from attributes on class or record properties to set metadata on the corresponding properties of the generated schema.
The following table summarizes attributes from the System.ComponentModel
namespace that provide metadata for the generated schema:
Attribute | Description |
---|---|
[Description] |
Sets the description of a property in the schema. |
[Required] |
Marks a property as required in the schema. |
[DefaultValue] |
Sets the default value of a property in the schema. |
[Range] |
Sets the minimum and maximum value of an integer or number. |
[MinLength] |
Sets the minLength of a string or minItems of an array. |
[MaxLength] |
Sets the maxLength of a string or maxItems of an array. |
[RegularExpression] |
Sets the pattern of a string. |
Note that in controller-based apps, these attributes add filters to the operation to validate that any incoming data satisfies the constraints. In Minimal APIs, these attributes set the metadata in the generated schema but validation must be performed explicitly via an endpoint filter, in the route handler's logic, or via a third-party package.
Attributes can also be placed on parameters in the parameter list of a record definition but must include the property
modifier. For example:
public record Todo(
[property: Required]
[property: Description("The unique identifier for the todo")]
int Id,
[property: Description("The title of the todo")]
[property: MaxLength(120)]
string Title,
[property: Description("Whether the todo has been completed")]
bool Completed
) {}
Other sources of metadata for generated schemas
required
In a class, struct, or record, properties with the [Required]
attribute or required modifier are always required
in the corresponding schema.
Other properties may also be required based on the constructors (implicit and explicit) for the class, struct, or record.
- For a class or record class with a single public constructor, any property with the same type and name (case-insensitive match) as a parameter to the constructor is required in the corresponding schema.
- For a class or record class with multiple public constructors, no other properties are required.
- For a struct or record struct, no other properties are required since C# always defines an implicit parameterless constructor for a struct.
enum
Enum types in C# are integer-based, but can be represented as strings in JSON with a [JsonConverter]
and a JsonStringEnumConverter. When an enum type is represented as a string in JSON, the generated schema will have an enum
property with the string values of the enum.
The following example demonstrates how to use a JsonStringEnumConverter
to represent an enum as a string in JSON:
[JsonConverter(typeof(JsonStringEnumConverter<DayOfTheWeekAsString>))]
public enum DayOfTheWeekAsString
{
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
}
A special case is when an enum type has the [Flags]
attribute, which indicates that the enum can be treated as a bit field; that is, a set of flags. A flags enum
with a [JsonConverterAttribute]
is defined as type: string
in the generated schema with no enum
property. No enum
property is generated because the value could be any combination of the enum values. For example, the following enum
could have values such as "Pepperoni, Sausage"
or "Sausage, Mushrooms, Anchovies"
:
[Flags, JsonConverter(typeof(JsonStringEnumConverter<PizzaToppings>))]
public enum PizzaToppings {
Pepperoni = 1,
Sausage = 2,
Mushrooms = 4,
Anchovies = 8
}
An enum type without a [JsonConverter]
will be defined as type: integer
in the generated schema.
Note: The [AllowedValues]
attribute does not set the enum
values of a property.
Set JSON options globally shows how to set the JsonStringEnumConverter
globally.
nullable
Properties defined as a nullable value or reference type appear in the generated schema with a type
keyword whose value is an array that includes null
as one of the types. This is consistent with the default behavior of the System.Text.Json deserializer, which accepts null
as a valid value for a nullable property.
For example, a C# property defined as string?
is represented in the generated schema as:
"nullableString": {
"description": "A property defined as string?",
"type": [
"null",
"string"
]
},
If the app is configured to produce OpenAPI v3.0 or OpenAPI v2 documents, nullable value or reference types have nullable: true
in the generated schema because these OpenAPI versions do not allow the type
field to be an array.
additionalProperties
Schemas are generated without an additionalProperties
assertion by default, which implies the default of true
. This is consistent with the default behavior of the System.Text.Json deserializer, which silently ignores additional properties in a JSON object.
If the additional properties of a schema should only have values of a specific type, define the property or class as a Dictionary<string, type>
. The key type for the dictionary must be string
. This generates a schema with additionalProperties
specifying the schema for "type" as the required value types.
Polymorphic types
Use the [JsonPolymorphic]
and [JsonDerivedType]
attributes on a parent class to specify the discriminator field and subtypes for a polymorphic type.
The [JsonDerivedType]
adds the discriminator field to the schema for each subclass, with an enum specifying the specific discriminator value for the subclass. This attribute also modifies the constructor of each derived class to set the discriminator value.
An abstract class with a [JsonPolymorphic]
attribute has a discriminator
field in the schema, but a concrete class with a [JsonPolymorphic]
attribute doesn't have a discriminator
field. OpenAPI requires that the discriminator property be a required property in the schema, but since the discriminator property isn't defined in the concrete base class, the schema cannot include a discriminator
field.
Add metadata with a schema transformer
A schema transformer can be used to override any default metadata or add additional metadata, such as example
values, to the generated schema. See Use schema transformers for more information.
Set JSON serialization options globally
The following code configures some JSON options globally, for Minimal APIs and Controller based APIs:
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http.Json;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
builder.Services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.Converters.Add(
new JsonStringEnumConverter<DayOfTheWeekAsString>());
options.SerializerOptions.DefaultIgnoreCondition =
JsonIgnoreCondition.WhenWritingNull;
options.SerializerOptions.PropertyNamingPolicy =
JsonNamingPolicy.CamelCase;
});
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(
new JsonStringEnumConverter<DayOfTheWeekAsString>());
options.JsonSerializerOptions.DefaultIgnoreCondition =
JsonIgnoreCondition.WhenWritingNull;
options.JsonSerializerOptions.PropertyNamingPolicy =
JsonNamingPolicy.CamelCase;
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.MapGet("/", () =>
{
var day = DayOfTheWeekAsString.Friday;
return Results.Json(day);
});
app.MapPost("/", (DayOfTheWeekAsString day) =>
{
return Results.Json($"Received: {day}");
});
app.UseRouting();
app.MapControllers();
app.Run();
MVC JSON options and global JSON options
The following table shows the key differences beween the MVC JSON options and global Minimal API JSON options:
Aspect | MVC JSON Options | Global JSON Options |
---|---|---|
Scope | Limited to MVC controllers and endpoints. | Minimal APIs and OpenAPI docs. |
Configuration | AddControllers().AddJsonOptions() |
Configure<JsonOptions>() |
Purpose | Handles serialization and deserialization of JSON requests and responses in APIs. | Defines global JSON handling for Minimal APIs and OpenAPI schemas. |
Influence on OpenAPI | None | Directly influences OpenAPI schema generation. |
Additional resources
ASP.NET collects metadata from the web app's endpoints and uses it to generate an OpenAPI document.
In controller-based apps, metadata is collected from attributes such as [EndpointDescription]
, [HttpPost]
,
and [Produces]
when the controller has the [ApiController]
attribute.
In minimal APIs, metadata can be collected from attributes but may also be set by using extension methods and other strategies, such as returning TypedResults from route handlers.
The following table provides an overview of the metadata collected and the strategies for setting it.
Metadata | Attribute | Extension method | Other strategies |
---|---|---|---|
summary | [EndpointSummary] |
WithSummary | |
description | [EndpointDescription] |
WithDescription | |
tags | [Tags] |
WithTags | |
operationId | [EndpointName] |
WithName | |
parameters | [FromQuery] , [FromRoute] , [FromHeader] , [FromForm] |
||
parameter description | [Description] |
||
requestBody | [FromBody] |
Accepts | |
responses | [Produces] |
Produces, ProducesProblem | TypedResults |
Excluding endpoints | [ExcludeFromDescription] , [ApiExplorerSettings] |
ExcludeFromDescription |
ASP.NET Core does not collect metadata from XML doc comments.
The following sections demonstrate how to include metadata in an app to customize the generated OpenAPI document.
Summary and description
The endpoint summary and description can be set using the [EndpointSummary]
and [EndpointDescription]
attributes,
or in minimal APIs, using the WithSummary and WithDescription extension methods.
The following sample demonstrates the different strategies for setting summaries and descriptions.
Note that the attributes are placed on the delegate method and not on the app.MapGet method.
app.MapGet("/extension-methods", () => "Hello world!")
.WithSummary("This is a summary.")
.WithDescription("This is a description.");
app.MapGet("/attributes",
[EndpointSummary("This is a summary.")]
[EndpointDescription("This is a description.")]
() => "Hello world!");
tags
OpenAPI supports specifying tags on each endpoint as a form of categorization.
In minimal APIs, tags can be set using either the [Tags]
attribute or the WithTags extension method.
The following sample demonstrates the different strategies for setting tags.
app.MapGet("/extension-methods", () => "Hello world!")
.WithTags("todos", "projects");
app.MapGet("/attributes",
[Tags("todos", "projects")]
() => "Hello world!");
operationId
OpenAPI supports an operationId on each endpoint as a unique identifier or name for the operation.
In minimal APIs, the operationId can be set using either the [EndpointName]
attribute or the WithName extension method.
The following sample demonstrates the different strategies for setting the operationId.
app.MapGet("/extension-methods", () => "Hello world!")
.WithName("FromExtensionMethods");
app.MapGet("/attributes",
[EndpointName("FromAttributes")]
() => "Hello world!");
parameters
OpenAPI supports annotating path, query string, header, and cookie parameters that are consumed by an API.
The framework infers the types for request parameters automatically based on the signature of the route handler.
The [Description]
attribute can be used to provide a description for a parameter.
The [Description]
attribute works in an MVC app but doesn't work in a Minimal API app at this time. For more information, see Description
parameter of ProducesResponseTypeAttribute
does not work in minimal API app (dotnet/aspnetcore
#60518).
Describe the request body
The requestBody
field in OpenAPI describes the body of a request that an API client can send to the server, including the content type(s) supported and the schema for the body content.
When the endpoint handler method accepts parameters that are bound from the request body, ASP.NET Core generates a corresponding requestBody
for the operation in the OpenAPI document. Metadata for the request body can also be specified using attributes or extension methods. Additional metadata can be set with a document transformer or operation transformer.
If the endpoint doesn't define any parameters bound to the request body, but instead consumes the request body from the HttpContext directly, ASP.NET Core provides mechanisms to specify request body metadata. This is a common scenario for endpoints that process the request body as a stream.
Some request body metadata can be determined from the FromBody
or FromForm
parameters of the route handler method.
A description for the request body can be set with a [Description]
attribute on the parameter with FromBody
or FromForm
.
If the FromBody
parameter is non-nullable and EmptyBodyBehavior is not set to Allow in the FromBody
attribute, the request body is required and the required
field of the requestBody
is set to true
in the generated OpenAPI document.
Form bodies are always required and have required
set to true
.
Use a document transformer or an operation transformer to set the example
, examples
, or encoding
fields, or to add specification extensions for the request body in the generated OpenAPI document.
Other mechanisms for setting request body metadata depend on the type of app being developed and are described in the following sections.
The content types for the request body in the generated OpenAPI document are determined from the type of the parameter that is bound to the request body or specified with the Accepts extension method.
By default, the content type of a FromBody
parameter will be application/json
and the content type for FromForm
parameter(s) will be multipart/form-data
or application/x-www-form-urlencoded
.
Support for these default content types is built in to Minimal APIs, and other content types can be handled by using custom binding. See the Custom binding topic of the Minimal APIs documentation for more information.
There are several ways to specify a different content type for the request body.
If the type of the FromBody
parameter implements IEndpointParameterMetadataProvider, ASP.NET Core uses this interface to determine the content type(s) in the request body.
The framework uses the PopulateMetadata method of this interface to set the content type(s) and type of the body content of the request body. For example, a Todo
class that accepts either application/xml
or text/xml
content-type can use IEndpointParameterMetadataProvider to provide this information to the framework.
public class Todo : IEndpointParameterMetadataProvider
{
public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
{
builder.Metadata.Add(new AcceptsMetadata(["application/xml", "text/xml"], typeof(Todo)));
}
}
The Accepts extension method can also be used to specify the content type of the request body.
In the following example, the endpoint accepts a Todo
object in the request body with an expected content-type of application/xml
.
app.MapPut("/todos/{id}", (int id, Todo todo) => ...)
.Accepts<Todo>("application/xml");
Since application/xml
is not a built-in content type, the Todo
class must implement the IBindableFromHttpContext<TSelf> interface to provide a custom binding for the request body. For example:
public class Todo : IBindableFromHttpContext<Todo>
{
public static async ValueTask<Todo?> BindAsync(HttpContext context, ParameterInfo parameter)
{
var xmlDoc = await XDocument.LoadAsync(context.Request.Body, LoadOptions.None, context.RequestAborted);
var serializer = new XmlSerializer(typeof(Todo));
return (Todo?)serializer.Deserialize(xmlDoc.CreateReader());
}
}
If the endpoint doesn't define any parameters bound to the request body, use the Accepts extension method to specify the content type that the endpoint accepts.
If you specify <AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.Accepts%2A> multiple times, only metadata from the last one is used -- they aren't combined.
Describe response types
OpenAPI supports providing a description of the responses returned from an API. ASP.NET Core provides several strategies for setting the response metadata of an endpoint. Response metadata that can be set includes the status code, the type of the response body, and content type(s) of a response. Responses in OpenAPI may have additional metadata, such as description, headers, links, and examples. This additional metadata can be set with a document transformer or operation transformer.
The specific mechanisms for setting response metadata depend on the type of app being developed.
In Minimal API apps, ASP.NET Core can extract the response metadata added by extension methods on the endpoint, attributes on the route handler, and the return type of the route handler.
- The Produces extension method can be used on the endpoint to specify the status code, the type of the response body, and content type(s) of a response from an endpoint.
- The
[ProducesResponseType]
or ProducesResponseTypeAttribute<T> attribute can be used to specify the type of the response body. - A route handler can be used to return a type that implements IEndpointMetadataProvider to specify the type and content-type(s) of the response body.
- The ProducesProblem extension method on the endpoint can be used to specify the status code and content-type(s) of an error response.
Note that the Produces and ProducesProblem extension methods are supported on both RouteHandlerBuilder and on RouteGroupBuilder. This allows, for example, a common set of error responses to be defined for all operations in a group.
When not specified by one of the preceding strategies, the:
- Status code for the response defaults to 200.
- Schema for the response body can be inferred from the implicit or explicit return type of the endpoint method, for example, from
T
in Task<TResult>; otherwise, it's considered to be unspecified. - Content-type for the specified or inferred response body is "application/json".
In Minimal APIs, the Produces extension method and the [ProducesResponseType]
attribute only set the response metadata for the endpoint. They do not modify or constrain the behavior of the endpoint, which may return a different status code or response body type than specified by the metadata, and the content-type is determined by the return type of the route handler method, irrespective of any content-type specified in attributes or extension methods.
The Produces extension method can specify an endpoint's response type, with a default status code of 200 and a default content type of application/json
. The following example illustrates this:
app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
.Produces<IList<Todo>>();
The [ProducesResponseType]
can be used to add response metadata to an endpoint. Note that the attribute is applied to the route handler method, not the method invocation to create the route, as shown in the following example:
app.MapGet("/todos",
[ProducesResponseType<List<Todo>>(200)]
async (TodoDb db) => await db.Todos.ToListAsync());
Using TypedResults in the implementation of an endpoint's route handler automatically includes the response type metadata for the endpoint. For example, the following code automatically annotates the endpoint with a response under the 200
status code with an application/json
content type.
app.MapGet("/todos", async (TodoDb db) =>
{
var todos = await db.Todos.ToListAsync();
return TypedResults.Ok(todos);
});
Only return types that implement IEndpointMetadataProvider create a responses
entry in the OpenAPI document. The following is a partial list of some of the TypedResults helper methods that produce a responses
entry:
TypedResults helper method |
Status code |
---|---|
Ok() | 200 |
Created() | 201 |
CreatedAtRoute() | 201 |
Accepted() | 202 |
AcceptedAtRoute() | 202 |
NoContent() | 204 |
BadRequest() | 400 |
ValidationProblem() | 400 |
NotFound() | 404 |
Conflict() | 409 |
UnprocessableEntity() | 422 |
All of these methods except NoContent
have a generic overload that specifies the type of the response body.
A class can be implemented to set the endpoint metadata and return it from the route handler.
Set responses for ProblemDetails
When setting the response type for endpoints that may return a ProblemDetails response, the following can be used to add the appropriate response metadata for the endpoint:
- ProducesProblem
- ProducesValidationProblem extension method.
- TypedResults with a status code in the (400-499) range.
For more information on how to configure a Minimal API app to return ProblemDetails responses, see Handle errors in minimal APIs.
Multiple response types
If an endpoint can return different response types in different scenarios, you can provide metadata in the following ways:
Call the Produces extension method multiple times, as shown in the following example:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) => await db.Todos.FindAsync(id) is Todo todo ? Results.Ok(todo) : Results.NotFound()) .Produces<Todo>(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound);
Use Results<TResult1,TResult2,TResult3,TResult4,TResult5,TResult6> in the signature and TypedResults in the body of the handler, as shown in the following example:
app.MapGet("/book/{id}", Results<Ok<Book>, NotFound> (int id, List<Book> bookList) => { return bookList.FirstOrDefault((i) => i.Id == id) is Book book ? TypedResults.Ok(book) : TypedResults.NotFound(); });
The
Results<TResult1,TResult2,TResultN>
union types declare that a route handler returns multipleIResult
-implementing concrete types, and any of those types that implement IEndpointMetadataProvider will contribute to the endpoint’s metadata.The union types implement implicit cast operators. These operators enable the compiler to automatically convert the types specified in the generic arguments to an instance of the union type. This capability has the added benefit of providing compile-time checking that a route handler only returns the results that it declares it does. Attempting to return a type that isn't declared as one of the generic arguments to
Results<TResult1,TResult2,TResultN>
results in a compilation error.
Exclude endpoints from the generated document
By default, all endpoints that are defined in an app are documented in the generated OpenAPI file, but endpoints can be excluded from the document using attributes or extension methods.
The mechanism for specifying an endpoint that should be excluded depends on the type of app being developed.
Minimal APIs support two strategies for excluding a given endpoint from the OpenAPI document:
- ExcludeFromDescription extension method
[ExcludeFromDescription]
attribute
The following sample demonstrates the different strategies for excluding a given endpoint from the generated OpenAPI document.
app.MapGet("/extension-method", () => "Hello world!")
.ExcludeFromDescription();
app.MapGet("/attributes",
[ExcludeFromDescription]
() => "Hello world!");
Include OpenAPI metadata for data types
C# classes or records used in request or response bodies are represented as schemas in the generated OpenAPI document. By default, only public properties are represented in the schema, but there are JsonSerializerOptions to also create schema properties for fields.
When the PropertyNamingPolicy is set to camel-case (this is the default
in ASP.NET web applications), property names in a schema are the camel-case form
of the class or record property name.
The [JsonPropertyName]
can be used on an individual property to specify the name
of the property in the schema.
type and format
The JSON Schema library maps standard C# types to OpenAPI type
and format
as follows:
C# Type | OpenAPI type |
OpenAPI format |
---|---|---|
int | integer | int32 |
long | integer | int64 |
short | integer | int16 |
byte | integer | uint8 |
float | number | float |
double | number | double |
decimal | number | double |
bool | boolean | |
string | string | |
char | string | char |
byte[] | string | byte |
DateTimeOffset | string | date-time |
DateOnly | string | date |
TimeOnly | string | time |
Uri | string | uri |
Guid | string | uuid |
object | omitted | |
dynamic | omitted |
Note that object and dynamic types have no type defined in the OpenAPI because these can contain data of any type, including primitive types like int or string.
The type
and format
can also be set with a Schema Transformer. For example, you may want the format
of decimal types to be decimal
instead of double
.
Use attributes to add metadata
ASP.NET uses metadata from attributes on class or record properties to set metadata on the corresponding properties of the generated schema.
The following table summarizes attributes from the System.ComponentModel
namespace that provide metadata for the generated schema:
Attribute | Description |
---|---|
[Description] |
Sets the description of a property in the schema. |
[Required] |
Marks a property as required in the schema. |
[DefaultValue] |
Sets the default value of a property in the schema. |
[Range] |
Sets the minimum and maximum value of an integer or number. |
[MinLength] |
Sets the minLength of a string or minItems of an array. |
[MaxLength] |
Sets the maxLength of a string or maxItems of an array. |
[RegularExpression] |
Sets the pattern of a string. |
Note that in controller-based apps, these attributes add filters to the operation to validate that any incoming data satisfies the constraints. In Minimal APIs, these attributes set the metadata in the generated schema but validation must be performed explicitly via an endpoint filter, in the route handler's logic, or via a third-party package.
Attributes can also be placed on parameters in the parameter list of a record definition but must include the property
modifier. For example:
public record Todo(
[property: Required]
[property: Description("The unique identifier for the todo")]
int Id,
[property: Description("The title of the todo")]
[property: MaxLength(120)]
string Title,
[property: Description("Whether the todo has been completed")]
bool Completed
) {}
Other sources of metadata for generated schemas
required
In a class, struct, or record, properties with the [Required]
attribute or required modifier are always required
in the corresponding schema.
Other properties may also be required based on the constructors (implicit and explicit) for the class, struct, or record.
- For a class or record class with a single public constructor, any property with the same type and name (case-insensitive match) as a parameter to the constructor is required in the corresponding schema.
- For a class or record class with multiple public constructors, no other properties are required.
- For a struct or record struct, no other properties are required since C# always defines an implicit parameterless constructor for a struct.
enum
Enum types in C# are integer-based, but can be represented as strings in JSON with a [JsonConverter]
and a JsonStringEnumConverter. When an enum type is represented as a string in JSON, the generated schema will have an enum
property with the string values of the enum.
The following example demonstrates how to use a JsonStringEnumConverter
to represent an enum as a string in JSON:
[JsonConverter(typeof(JsonStringEnumConverter<DayOfTheWeekAsString>))]
public enum DayOfTheWeekAsString
{
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
}
A special case is when an enum type has the [Flags]
attribute, which indicates that the enum can be treated as a bit field; that is, a set of flags. A flags enum with a [JsonConverterAttribute]
is defined as type: string
in the generated schema with no enum
property, since the value could be any combination of the enum values. For example, the following enum:
[Flags, JsonConverter(typeof(JsonStringEnumConverter<PizzaToppings>))]
public enum PizzaToppings { Pepperoni = 1, Sausage = 2, Mushrooms = 4, Anchovies = 8 }
could have values such as "Pepperoni, Sausage"
or "Sausage, Mushrooms, Anchovies"
.
An enum type without a [JsonConverter]
will be defined as type: integer
in the generated schema.
Note: The [AllowedValues]
attribute does not set the enum
values of a property.
nullable
Properties defined as a nullable value or reference type have nullable: true
in the generated schema. This is consistent with the default behavior of the System.Text.Json deserializer, which accepts null
as a valid value for a nullable property.
additionalProperties
Schemas are generated without an additionalProperties
assertion by default, which implies the default of true
. This is consistent with the default behavior of the System.Text.Json deserializer, which silently ignores additional properties in a JSON object.
If the additional properties of a schema should only have values of a specific type, define the property or class as a Dictionary<string, type>
. The key type for the dictionary must be string
. This generates a schema with additionalProperties
specifying the schema for "type" as the required value types.
Polymorphic types
Use the [JsonPolymorphic]
and [JsonDerivedType]
attributes on a parent class to specify the discriminator field and subtypes for a polymorphic type.
The [JsonDerivedType]
adds the discriminator field to the schema for each subclass, with an enum specifying the specific discriminator value for the subclass. This attribute also modifies the constructor of each derived class to set the discriminator value.
An abstract class with a [JsonPolymorphic]
attribute has a discriminator
field in the schema, but a concrete class with a [JsonPolymorphic]
attribute doesn't have a discriminator
field. OpenAPI requires that the discriminator property be a required property in the schema, but since the discriminator property isn't defined in the concrete base class, the schema cannot include a discriminator
field.
Add metadata with a schema transformer
A schema transformer can be used to override any default metadata or add additional metadata, such as example
values, to the generated schema. See Use schema transformers for more information.
Additional resources
ASP.NET Core