OpenAPI in ASP.NET Core is now a first-party feature: the Microsoft.AspNetCore.OpenApi package generates OpenAPI documents from your endpoints at runtime or at build time, with no Swashbuckle required. This guide is for developers who build HTTP APIs with Minimal APIs or controllers. You will learn how document generation works, how to enrich and transform documents, what OpenAPI 3.1 in .NET 10 and 3.2 in .NET 11 change, which UI to add, the state of Swashbuckle, how to generate clients with Kiota or NSwag, and how to manage versioned documents with either an API-first or a code-first workflow.
What Is OpenAPI in ASP.NET Core?#
OpenAPI is a language-neutral specification for describing HTTP APIs: paths, operations, parameters, request and response bodies (as JSON Schema), security schemes and more. A good OpenAPI document drives interactive documentation, client SDK generation, contract testing, API gateways and, increasingly, AI tools that call your API.
For years, .NET developers produced these documents with community libraries, mainly Swashbuckle and NSwag. Starting with .NET 9, ASP.NET Core ships its own generator in Microsoft.AspNetCore.OpenApi. You register it with AddOpenApi, expose it with MapOpenApi, and customize it with transformers. It works with Minimal APIs and controllers, uses System.Text.Json contracts for schemas, supports multiple documents per app, and is compatible with Native AOT.
| .NET version | OpenAPI capability |
|---|---|
| .NET 8 | Endpoint metadata for Minimal APIs. Documents and UI come from Swashbuckle or NSwag. |
| .NET 9 | Built-in generation with AddOpenApi and MapOpenApi, plus transformers. Swashbuckle removed from templates. |
| .NET 10 (LTS) | OpenAPI 3.1 by default, JSON Schema 2020-12, YAML output, XML doc comments, Microsoft.OpenApi 2.0 |
| .NET 11 (RC) | OpenAPI 3.2 by default, [Obsolete] mapped to deprecated, HTTP QUERY, SSE item schemas, Microsoft.OpenApi 3.x |
How OpenAPI Document Generation Works#
Generation starts from the same metadata that routing uses. ASP.NET Core's API Explorer turns every endpoint into an ApiDescription that holds its route, HTTP method, parameters, request body, response types and endpoint metadata such as tags, summaries and group names. The OpenAPI document service walks those descriptions, builds schemas from System.Text.Json type information, and assembles a Microsoft.OpenApi document object.
Transformers then run in a fixed order: schema transformers as each schema is registered, operation transformers as each operation is added, and document transformers last, over the complete document. The result is serialized as JSON or YAML. At runtime, the document is regenerated on every request to the endpoint, so transformers can use live app state. At build time, a tool launches your app with a mock server, collects the same documents and writes them to disk.
Getting Started with Microsoft.AspNetCore.OpenApi#
The .NET 9 and later Web API templates already include this setup. The key to a useful document is metadata: typed results declare response shapes, and summaries, tags and descriptions become documentation.
using System.ComponentModel;
using Microsoft.AspNetCore.Http.HttpResults;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
builder.Services.AddOpenApi(); // document "v1" at /openapi/v1.json
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
var todos = app.MapGroup("/todos").WithTags("Todos");
todos.MapGet("/{id:int}", GetTodo)
.WithName("GetTodo")
.WithSummary("Get a todo by id");
todos.MapPost("/", CreateTodo)
.WithSummary("Create a todo")
.ProducesValidationProblem();
app.Run();
static async Task<Results<Ok<Todo>, NotFound>> GetTodo(
[Description("The todo identifier.")] int id, TodoStore store, CancellationToken ct) =>
await store.FindAsync(id, ct) is { } todo ? TypedResults.Ok(todo) : TypedResults.NotFound();
static async Task<Created<Todo>> CreateTodo(NewTodo input, TodoStore store, CancellationToken ct)
{
var todo = await store.AddAsync(input, ct);
return TypedResults.Created($"/todos/{todo.Id}", todo);
}
public sealed record Todo(int Id, string Title, bool IsDone);
public sealed record NewTodo([property: Description("What needs doing.")] string Title);Returning Results<Ok<Todo>, NotFound> documents both the 200 and 404 responses with no extra attributes, because typed results carry their own endpoint metadata. Controllers use the equivalent attributes, such as [ProducesResponseType], [EndpointSummary] and [Tags]. In .NET 10, [ProducesResponseType] also accepts a Description. The older WithOpenApi extension is obsolete in .NET 10 (diagnostic ASPDEPR002). Use the metadata methods above or an endpoint-specific operation transformer instead.
Document, Operation and Schema Transformers#
Transformers are the supported way to customize output. Document transformers see the whole document and suit global information such as titles, servers and security schemes. Operation transformers run once per path and method pair, and schema transformers run once per schema. Each can be a delegate, an instance or a DI-activated class that receives services through its constructor.
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, context, ct) =>
{
document.Info = new OpenApiInfo
{
Title = "Catalog API",
Version = "v1",
Description = "Products, prices and stock for the Contoso storefront."
};
return Task.CompletedTask;
});
options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
options.AddOperationTransformer((operation, context, ct) =>
{
operation.Responses ??= new OpenApiResponses();
operation.Responses.TryAdd("500", new OpenApiResponse { Description = "Unexpected error" });
return Task.CompletedTask;
});
options.AddSchemaTransformer((schema, context, ct) =>
{
if (context.JsonTypeInfo.Type == typeof(decimal))
{
schema.Format = "decimal"; // not "double"
}
return Task.CompletedTask;
});
});
internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider schemes)
: IOpenApiDocumentTransformer
{
public async Task TransformAsync(
OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken ct)
{
var registered = await schemes.GetAllSchemesAsync();
if (!registered.Any(s => s.Name == "Bearer")) return;
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT"
};
foreach (var operation in document.Paths.Values.SelectMany(p => p.Operations ?? new()))
{
operation.Value.Security ??= [];
operation.Value.Security.Add(new OpenApiSecurityRequirement
{
[new OpenApiSecuritySchemeReference("Bearer", document)] = []
});
}
}
}For a single endpoint, AddOpenApiOperationTransformer on the endpoint builder applies a transformer only there. In .NET 10, transformer contexts also expose GetOrCreateSchemaAsync, which generates a schema for any .NET type with the framework's own rules, so you can register it with document.AddComponent. Beware version upgrades: .NET 10 moved to Microsoft.OpenApi 2.0, which changed the model (entities are interfaces such as IOpenApiSchema, OpenApiAny became JsonNode, and Nullable was removed from schemas), and .NET 11 moves to Microsoft.OpenApi 3.x with further breaking changes. The sample above targets .NET 10. Keep transformers small and covered by tests.
OpenAPI 3.1 and 3.2 Support#
.NET 10 generates OpenAPI 3.1 documents by default. Version 3.1 aligns schemas with JSON Schema draft 2020-12, and the output changes accordingly. Nullable types become type arrays that include null instead of nullable: true. Property descriptions can now sit next to $ref. Integer properties may lose type: integer and gain a digits-only pattern instead, because ASP.NET Core's default JSON options allow reading numbers from strings. Set NumberHandling to Strict if you want plain type: integer.
.NET 11, at release candidate stage as of September 2026, makes OpenAPI 3.2 the default. 3.2 can describe the HTTP QUERY method and per-event itemSchema for server-sent events. Many tools lag behind the specification, so pin the version your generators, gateways and linters understand:
using Microsoft.OpenApi;
builder.Services.AddOpenApi(options =>
{
// .NET 10 defaults to 3.1 and .NET 11 to 3.2. Pin what your toolchain supports.
options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1;
});
// Serve the same document as YAML too (.NET 10+).
app.MapOpenApi("/openapi/{documentName}.yaml");For build-time generation, pass the version through OpenApiGenerateDocumentsOptions, for example --openapi-version OpenApi3_1.
What Happened to Swashbuckle?#
For .NET 9, Microsoft removed Swashbuckle from the Web API templates because the project appeared unmaintained at the time, and invested in the built-in generator instead. The project has since resumed regular releases. Swashbuckle.AspNetCore 10.x moved to Microsoft.OpenApi 2.x, adding OpenAPI 3.1 output while keeping 3.0 as its default, and it supports ASP.NET Core 8 and later.
Existing Swashbuckle apps therefore do not need an urgent migration. Move when you want build-time generation, Native AOT compatibility, or first-party alignment with new framework features. Start new projects on Microsoft.AspNetCore.OpenApi. If you migrate, rewrite Swashbuckle filters (IOperationFilter, ISchemaFilter, IDocumentFilter) as the matching transformers, and compare old and new documents in a diff before switching clients over.
Versioned OpenAPI Documents#
Publish one document per API version or audience so each consumer sees only what applies to them. The built-in approach registers several documents and assigns endpoints to them by group name. Endpoints without a group name appear in every document, and you can replace the selection logic with ShouldInclude:
builder.Services.AddOpenApi("v1");
builder.Services.AddOpenApi("v2");
var app = builder.Build();
app.MapGroup("/api/v1/orders").WithGroupName("v1").MapGet("/{id:int}", GetOrderV1);
app.MapGroup("/api/v2/orders").WithGroupName("v2").MapGet("/{id:int}", GetOrderV2);
app.MapOpenApi(); // serves /openapi/v1.json and /openapi/v2.jsonFor real versioning policies, with version readers, deprecation and sunset headers, and version-neutral endpoints, use the Asp.Versioning libraries. Their Asp.Versioning.OpenApi package now integrates with Microsoft.AspNetCore.OpenApi directly:
builder.Services.AddApiVersioning(options => options.ReportApiVersions = true)
.AddApiExplorer(options => options.GroupNameFormat = "'v'VVV")
.AddOpenApi();
var app = builder.Build();
var orders = app.NewVersionedApi("Orders");
orders.MapGroup("/api/orders").HasApiVersion(1.0).MapGet("/{id:int}", GetOrderV1);
orders.MapGroup("/api/orders").HasApiVersion(2.0).MapGet("/{id:int}", GetOrderV2);
if (app.Environment.IsDevelopment())
{
app.MapOpenApi().WithDocumentPerVersion();
}For the versioning strategy itself, including URL, header and media-type trade-offs, see the REST API design guide.
API-First vs Code-First Design#
In code-first development, the implementation is the source of truth and the document is generated from it, which is the model this guide has used so far. In API-first development, teams design and review the OpenAPI document before writing code, then implement against it.
| Aspect | Code-first | API-first |
|---|---|---|
| Source of truth | C# endpoints and types | The OpenAPI document |
| Speed for one team | Fast, with no duplication | Slower start, as the contract is written by hand |
| Cross-team alignment | Consumers wait for code | Consumers mock and build in parallel |
| Drift risk | Document always matches code | Code can drift unless contract tests enforce it |
| Governance | Lint and diff generated documents in CI | Lint and review the contract before implementation |
| Good fit | Internal APIs, single-team services | Public APIs, partner integrations, platform teams |
The approaches mix well. Many teams design new public endpoints contract-first, implement them in ASP.NET Core, and then treat the build-time generated document as a verification artifact: CI diffs it against the approved contract and fails on unintended breaking changes. Integration tests that call endpoints through WebApplicationFactory, as the integration testing guide shows, catch the behavioral side of drift.
Best Practices#
- Treat the document as an API artifact. Generate it at build time, commit it, and review diffs in pull requests.
- Use typed results and explicit metadata. Documented 4xx and 5xx responses matter as much as 200.
- Give every operation a stable
operationIdwithWithName, because client generators turn it into method names. - Lint in CI with Spectral or a similar tool, so missing descriptions and inconsistent naming fail the build.
- Expose documents and UIs only where appropriate: development by default, or behind authorization.
- Pin the OpenAPI version your downstream tools support, and upgrade deliberately.
- Regenerate clients in CI from the committed document, so client and server never drift silently.
Common Pitfalls#
- Anonymous lambdas without metadata, which produce operations with no summaries, names or documented error responses.
- Relying on
WithOpenApi, which is obsolete in .NET 10. - Upgrading .NET without testing transformers, since Microsoft.OpenApi 2.0 in .NET 10 and 3.x in .NET 11 both break model APIs.
- Assuming tools understand 3.1 or 3.2. Some generators and gateways still expect 3.0.
- Startup code with side effects that runs, and fails, during build-time generation.
- Returning
IResultwithout declared response types, which leaves responses undocumented. - Publishing Swagger UI or Scalar in production by accident, which exposes internal endpoints.
Frequently Asked Questions#
Do I still need Swashbuckle in .NET 10?#
No. Microsoft.AspNetCore.OpenApi generates documents, and you add a UI such as Scalar or Swagger UI separately. Swashbuckle is maintained again and its version 10 supports OpenAPI 3.1, so existing apps can keep it, but new projects should start with the built-in generator.
Which OpenAPI version does ASP.NET Core generate?#
.NET 9 generates OpenAPI 3.0, .NET 10 defaults to 3.1, and .NET 11 defaults to 3.2. You can choose a version with OpenApiOptions.OpenApiVersion at runtime or the --openapi-version option at build time, which matters when downstream tools do not yet support newer versions.
Should I use Kiota or NSwag to generate a C# client?#
Use NSwag when you want a compact, familiar client with one method per operation and interfaces for mocking. Use Kiota for very large APIs, multi-language SDKs or when you need path filtering and repeatable regeneration. Both work from the same document, so try both against your API.
Can I generate the OpenAPI document without running the app?#
Yes, in the sense that you do not deploy or call it. Microsoft.Extensions.ApiDescription.Server generates the document during dotnet build by launching your entry point with a mock server. Your startup code still executes, so guard infrastructure-dependent code with the GetDocument.Insider entry assembly check.
How do I document authentication in the OpenAPI document?#
Add a security scheme with a document transformer, as in the bearer example above, and add security requirements to the operations that need them. An operation transformer can skip endpoints that allow anonymous access by inspecting the endpoint metadata in its context.
Summary#
Microsoft.AspNetCore.OpenApiis the built-in generator from .NET 9. Register it withAddOpenApi, expose it withMapOpenApi, and customize it with transformers.- .NET 10 defaults to OpenAPI 3.1 with JSON Schema 2020-12, YAML output and XML comments, while .NET 11 defaults to 3.2.
- Build-time generation turns the document into a reviewable, lintable and diffable artifact.
- Add Scalar or Swagger UI for interactive docs, in development only. Swashbuckle is optional, not obsolete.
- Generate typed clients with Kiota or NSwag, publish one document per version, and choose API-first or code-first per API.
Enriching Documents with XML Comments#
From .NET 10, a source generator in the OpenAPI package reads XML documentation comments on handler methods, types and properties, and adds them to the document. Enable the documentation file in the project, and keep in mind that lambdas cannot carry XML comments, so handlers you want documented this way must be named methods, as above. The same project file can also turn on build-time generation, which the next sections use:
In .NET 11,
[Obsolete]on an endpoint, type or property automatically becomesdeprecated: truein the document, so API consumers see the same deprecation warnings as .NET callers.