Enhancing API Documentation
Swashbuckle tooling for Swagger provides an easy way to document APIs in ASP.NET Core with out of the box functionality that creates a lot without additional customization. The OpenAPI spec allows for a lot of additional description to be included, and by annotating your code in appropriate places the Swagger generation can automatically fill these for you. Many of these additional descriptions also show up in the automatically generated Swagger UI.
There are multiple ways to add annotations, each of which covers a subset of the available types, with overlap in many places. The ones we’ll look at here include:
- Swagger configuration options set in your startup code
- MVC
Produces
attributes in your API code - Swashbuckle
Swagger
attributes in your API code - XML documentation comments in your API code
Swagger Configuration
Some top-level descriptions of your API can be added in your startup configuration. There are a lot of different options here depending on what kind of details you want to include. I’ve added a few examples that show up in the Swagger UI here, but see the documentation of OpenApiInfo for more.
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Swagger Demo API",
Version = "v1",
Description = "This is the Web API description",
Contact = new OpenApiContact
{
Name = "John Bowen",
Url = new Uri("https://codemindinterface.com"),
},
License = new OpenApiLicense
{
Name = "The API License",
Url = new Uri("https://localhost/api-license"),
}
});
});
MVC Produces
Attributes
ASP.NET Core includes a set of built in attributes that can be applied to API methods. A few of these, like Produces
and Consumes
, can also affect the behavior of your API (limiting allowed media types), but additional Produces… variations are available primarily as documentation.
Produces
andConsumes
specify valid media types for an API method. These are listed in OpenAPI docs and show up as a dropdown in the testing UI.[Produces("application/json")]
ProducesResponseType
specifies a possible status code and optionally a data type that can be returned from the API method.[ProducesResponseType(typeof(User), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
ProducesErrorResponseType
can be used to specify a common data type to return for any error responses rather than specifying the type individually on each response type.[ProducesErrorResponseType(typeof(void))]
[ProducesErrorResponseType(typeof(MyCustomError))]
ProducesDefaultResponseType
specifies a default data type to return for any response type that is not explicitly specified. If no type is specified, the defaultProblemDetails
data type is used.[ProducesDefaultResponseType]
[ProducesDefaultResponseType(typeof(MyCustomError))]
Swashbuckle Swagger
Attributes
These attributes require an additional NuGet package: Swashbuckle.AspNetCore.Annotations
. Once this is referenced, additional configuration needs to be added to AddSwaggerGen
at startup to read the attributes.
builder.Services.AddSwaggerGen(options =>
{
options.EnableAnnotations();
});
SwaggerOperation
[SwaggerOperation("Get a user", "Gets a user by id", OperationId = "get-user", Tags = new[] { "users" })]
SwaggerResponse
[SwaggerResponse(StatusCodes.Status201Created, "The user was created", typeof(User), ContentTypes = new[] { "application/json" })]
SwaggerParameter
is used on method parameters to specify descriptions or mark as required[SwaggerParameter("SwaggerParameter.Description", Required = false)] string organization
SwaggerRequestBody
is used on method parameters likeSwaggerParameter
but specifically for request bodies (i.e. POST and PUT).[FromBody][SwaggerRequestBody("SwaggerRequestBody.Description", Required = true)] User? user
SwaggerSchema
is used on model object classes and properties to provide descriptions and specify other attributes, like nullable and readonly.[SwaggerSchema("The email address of the user", Nullable = false)]
SwaggerTag
is a class level attribute that will define the default tag grouping for all methods that don’t explicitly specify their own tags.
XML Documentation Comments
XML documentation comments (using ///
followed by defined tags) can be added on classes and methods to produce a doc file for your entire project which can then be read in by the Swagger generator. To enable XML documentation generation in your project, in your .csproj
file add the following lines:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
This will generate an XML file containing the documentation comments for your code. By default it names the file using the format <AssemblyName>.xml
. In order for Swagger to use these comments, additional configuration needs to again be added to AddSwaggerGen
at startup.
builder.Services.AddSwaggerGen(options =>
{
options.IncludeXmlComments(Path.Combine(
AppContext.BaseDirectory,
$"{Assembly.GetExecutingAssembly().GetName().Name}.xml"), true);
});
Although the XML doc spec includes a lot of additional valid tags, not all of them will come through to the OpenAPI doc. The key tags that can show up:
<summary>
on API classes, methods, model classes, and properties. These turn into summary or description in multiple places so are probably the primary thing you’ll want to add./// <summary> /// Create a new user /// </summary>
<remarks>
on API methods show up as additional descriptions and can be useful for adding things like examples or links/// <remarks> /// Creating a new user requires a valid email address object. <![CDATA[<br/>]]> /// See https://link.local/ for more details. /// </remarks>
<param>
on API methods and model classes. On methods, these work like theSwaggerParameter
attribute but also allow for specifying an example value. For model classes they work like the property levelSwaggerSchema
attribute./// <param name="id" example="A12345" required="true">The user ID</param>
/// <param name="Zip">The Zip Code</param>
Example
Because there are so many options it can be difficult to tell where various descriptions will actually show up in your OpenAPI doc and the Swagger UI. The following example code uses a mix of all of the techniques listed to demonstrate what the output will look like. The complete output can be found in the example OpenAPI doc. Here are some examples of where various descriptions show up in the Swagger UI:
Startup configuration in Program.Main
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.IncludeXmlComments(Path.Combine(
AppContext.BaseDirectory,
$"{Assembly.GetExecutingAssembly().GetName().Name}.xml"), true);
options.EnableAnnotations();
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "This is OpenApiInfo.Title",
Version = "v1",
Description = "This is OpenApiInfo.Description",
Contact = new OpenApiContact
{
Name = "OpenApiContact.Name",
Url = new Uri("https://codemindinterface.com"),
},
License = new OpenApiLicense
{
Name = "OpenApiLicense.Name",
Url = new Uri("https://localhost/api-license"),
}
});
});
Model objects
[SwaggerSchema("SwaggerSchema.Description on class", ReadOnly = true, Nullable = false)]
public record User(string Id, string First, string Last, string Email, string Username, Address HomeAddress)
{
[SwaggerSchema("SwaggerSchema.Description on property", ReadOnly = true, Nullable = false)]
public string Id { get; init; } = Id;
[SwaggerSchema("The first name of the user", Title = "User First Name")]
public string First { get; init; } = First;
[SwaggerSchema("The last name of the user")]
public string Last { get; init; } = Last;
[SwaggerSchema("The email address of the user")]
public string Email { get; init; } = Email;
[SwaggerSchema("The username of the user")]
public string Username { get; init; } = Username;
[SwaggerSchema("The home address of the user")]
public Address HomeAddress { get; init; } = HomeAddress;
}
/// <summary>
/// XML doc comment summary
/// </summary>
/// <param name="Street">XML doc comment param</param>
/// <param name="City">The City</param>
/// <param name="State">The US State Abbreviation</param>
/// <param name="Zip">The Zip Code</param>
public record Address(string Street, string City, string State, string Zip);
public class CustomErrorResponse
{
public int ErrorCode { get; set; }
public string? Message { get; set; }
}
API Controller with class and method level attributes
/// <summary>
/// XML doc comment class summary
/// </summary>
[Route("api/[controller]")]
[ApiController]
[SwaggerTag("SwaggerTag on class")]
public class UserController : ControllerBase
{
[HttpGet]
[ProducesResponseType(typeof(List<User>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(void))]
[SwaggerOperation("SwaggerOperation.Summary",
"SwaggerOperation.Description",
OperationId = "SwaggerOperation.OperationId",
Tags = new[] { "SwaggerOperation.Tags1", "users" })]
[Produces("application/json")]
public IActionResult Get()
{
//...
}
/// <param name="id" example="A12345" required="true">XML doc comment param</param>
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
[Produces("application/json")]
[SwaggerOperation("Get a user",
"Gets a user by id",
OperationId = "get-user",
Tags = new[] { "users" })]
[SwaggerResponse(StatusCodes.Status200OK,
"The user was found",
typeof(User))]
public IActionResult Get(string id)
{
//...
}
/// <summary>
/// XML doc comment summary
/// </summary>
/// <remarks>
/// XML doc comment remarks.
/// </remarks>
[HttpPost]
[SwaggerOperation(OperationId = "create-user")]
[SwaggerResponse(StatusCodes.Status201Created,
"The user was created",
typeof(User),
ContentTypes = new[] { "application/json" })]
[SwaggerResponse(StatusCodes.Status400BadRequest,
"The user was invalid",
typeof(void))]
[SwaggerResponse(StatusCodes.Status401Unauthorized,
"The user was not authorized",
typeof(void))]
public IActionResult Post(
[FromBody][SwaggerRequestBody("SwaggerRequestBody.Description", Required = true)] User? user,
[SwaggerParameter("SwaggerParameter.Description", Required = false)] string organization)
{
//...
}
[HttpDelete("{id}")]
[SwaggerOperation(OperationId = "delete-user")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesDefaultResponseType(typeof(CustomErrorResponse))]
public IActionResult Delete(string id)
{
//...
}
}
Code Versions
Example code is using .NET 7.0 with these library versions:
- Microsoft.AspNetCore.OpenApi 7.0.11
- Swashbuckle.AspNetCore 6.5.0
- Swashbuckle.AspNetCore.Annotations 6.5.0