Advanced OpenAPI — Schemas, Examples and Custom Operations

Beyond basic endpoint listing, OpenAPI documentation can include response examples, request examples, detailed parameter descriptions, error response schemas, and authentication requirements. Well-documented APIs reduce integration time for Angular developers, reduce support requests, and serve as the binding contract between API producers and consumers. Swashbuckle’s extension packages add examples, filters, and schema customisation that transform a bare controller list into reference documentation.

Rich OpenAPI Documentation

// dotnet add package Swashbuckle.AspNetCore.Filters
// dotnet add package Swashbuckle.AspNetCore.Annotations

// ── Complete action documentation ─────────────────────────────────────────
/// <summary>
/// Creates a new blog post. Requires authentication.
/// The slug must be unique across all posts.
/// </summary>
/// <remarks>
/// Example request:
/// <code>
/// POST /api/v1/posts
/// { "title": "My First Post", "slug": "my-first-post", "body": "Content here..." }
/// </code>
/// </remarks>
[HttpPost]
[Authorize]
[SwaggerOperation(
    Summary     = "Create a post",
    Description = "Creates a new blog post. Slug must be unique.",
    OperationId = "Posts_Create",
    Tags        = new[] { "Posts" })]
[SwaggerRequestExample(typeof(CreatePostRequest), typeof(CreatePostRequestExample))]
[SwaggerResponseExample(201, typeof(PostDtoExample))]
[ProducesResponseType(typeof(PostDto),                  StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails),           StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ProblemDetails),           StatusCodes.Status409Conflict)]
public async Task<ActionResult<PostDto>> Create(CreatePostRequest request, CancellationToken ct)
    => CreatedAtAction(nameof(GetById), new { id = 1 }, await _service.CreateAsync(request, ct));

// ── Request/response example providers ───────────────────────────────────
public class CreatePostRequestExample : IExamplesProvider<CreatePostRequest>
{
    public CreatePostRequest GetExamples() => new()
    {
        Title = "Building REST APIs with ASP.NET Core",
        Slug  = "building-rest-apis-aspnet-core",
        Body  = "In this article we explore how to build production-ready REST APIs...",
        Tags  = ["dotnet", "api", "rest"],
    };
}

public class PostDtoExample : IExamplesProvider<PostDto>
{
    public PostDto GetExamples() => new()
    {
        Id          = 42,
        Title       = "Building REST APIs with ASP.NET Core",
        Slug        = "building-rest-apis-aspnet-core",
        PublishedAt = new DateTime(2025, 6, 1),
        Author      = new AuthorDto(7, "Jane Developer", "https://avatars.blogapp.com/7"),
    };
}
Note: The [ProducesResponseType] attribute is documentation-only — it tells Swagger what response types are possible for a given status code. Swagger uses this to generate the response schema in the OpenAPI spec. It does not affect runtime behaviour. If you declare [ProducesResponseType(typeof(PostDto), 200)] but the action returns a 404, the API still returns 404 at runtime. These attributes are a contract declaration — keep them accurate by updating them when action behaviour changes.
Tip: Add a global operation filter that injects common parameters into every operation’s Swagger documentation — parameters like X-Correlation-Id (optional request correlation), Accept-Language (for localised responses), or API version headers. This documents infrastructure-level parameters once without adding them to every controller action: opts.OperationFilter<AddCommonHeadersFilter>(). The filter implements IOperationFilter and adds the parameters to the Swagger operation descriptor.
Warning: Keep Swagger documentation accurate — outdated docs are worse than no docs. When you add a new status code an endpoint can return, add a new [ProducesResponseType]. When you remove an endpoint, remove its documentation. When you add a required request header, document it. Clients who build Angular integrations from the OpenAPI spec and find the spec inaccurate lose trust in both the documentation and the API team. Treat the OpenAPI spec as a first-class artefact that is reviewed in code reviews.

Custom Schema Filter for Enums

// ── Custom schema filter — documents enums with descriptions ───────────────
public class EnumSchemaFilter : ISchemaFilter
{
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        if (!context.Type.IsEnum) return;

        schema.Enum.Clear();
        schema.Type        = "string";
        schema.Description = string.Join(", ", Enum.GetNames(context.Type));

        foreach (var name in Enum.GetNames(context.Type))
        {
            schema.Enum.Add(new OpenApiString(name));
        }
    }
}

// Register: opts.SchemaFilter<EnumSchemaFilter>();

// ── Global operation filter — add correlation ID header to all operations ──
public class AddCorrelationIdHeaderFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        operation.Parameters ??= [];
        operation.Parameters.Add(new OpenApiParameter
        {
            Name        = "X-Correlation-Id",
            In          = ParameterLocation.Header,
            Required    = false,
            Schema      = new OpenApiSchema { Type = "string" },
            Description = "Optional correlation ID for request tracing.",
        });
    }
}

Common Mistakes

Mistake 1 — Not documenting 401/403 responses on protected endpoints

❌ Wrong — Swagger shows only 200 for an [Authorize] endpoint; Angular developers get surprise 401s.

✅ Correct — add [ProducesResponseType(401)] and [ProducesResponseType(403)] to all protected endpoints.

Mistake 2 — Examples that don’t match the actual schema (misleads integrators)

❌ Wrong — example shows a field “author_name” but the actual response uses camelCase “authorName”.

✅ Correct — examples use the same serialisation settings as the production response (camelCase).

🧠 Test Yourself

A controller action has [ProducesResponseType(typeof(PostDto), 200)] but at runtime it actually returns a PostSummaryDto (different type). What impact does this have?