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 versionOpenAPI capability
.NET 8Endpoint metadata for Minimal APIs. Documents and UI come from Swashbuckle or NSwag.
.NET 9Built-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.

C#
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.

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:

XML
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <!-- Feed XML doc comments into the OpenAPI document. -->
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <NoWarn>$(NoWarn);CS1591</NoWarn>
    <!-- Write the document next to the project file on every build. -->
    <OpenApiGenerateDocumentsOnBuild>true</OpenApiGenerateDocumentsOnBuild>
    <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
    <OpenApiGenerateDocumentsOptions>--file-name catalog-api</OpenApiGenerateDocumentsOptions>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
    <PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.12"
                      PrivateAssets="all" />
  </ItemGroup>

</Project>

In .NET 11, [Obsolete] on an endpoint, type or property automatically becomes deprecated: true in the document, so API consumers see the same deprecation warnings as .NET callers.

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.

C#
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:

C#
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.

Generating OpenAPI Documents at Build Time#

Adding Microsoft.Extensions.ApiDescription.Server, as the project file above does, generates documents during dotnet build. OpenApiDocumentsDirectory controls where the file lands, and --file-name and --document-name in OpenApiGenerateDocumentsOptions control naming and selection. A committed document is powerful. Pull requests show API changes as readable diffs, a CI pipeline can lint the file with a tool such as Spectral, and client generators can run without starting the service.

Build-time generation works by running your app's entry point with a mock server, so startup code executes. Guard anything that needs real infrastructure:

C#
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);

// During build-time generation the entry assembly is "GetDocument.Insider".
var generatingOpenApi =
    Assembly.GetEntryAssembly()?.GetName().Name == "GetDocument.Insider";

if (!generatingOpenApi)
{
    builder.AddServiceDefaults(); // Aspire telemetry, service discovery and health checks
}

builder.Services.AddOpenApi();
var app = builder.Build();

if (!generatingOpenApi)
{
    await app.Services.ApplyCatalogMigrationsAsync(); // app-specific startup work
}

.NET 11 adds an OpenApiGenerationEnvironment MSBuild property, which selects the environment, such as Development, used during generation, so environment-specific configuration and transformers apply to the build-time document too.

UI Options: Scalar, Swagger UI and Others#

Microsoft.AspNetCore.OpenApi produces documents only. Interactive documentation comes from separate packages, and Microsoft's guidance is to enable these UIs only in development, because they advertise your entire API surface.

C#
using Scalar.AspNetCore;

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();

    // Scalar at /scalar (package Scalar.AspNetCore).
    app.MapScalarApiReference();

    // Swagger UI at /swagger (package Swashbuckle.AspNetCore.SwaggerUi).
    app.UseSwaggerUI(options => options.SwaggerEndpoint("/openapi/v1.json", "Catalog API v1"));
}

Scalar is a modern open-source API reference with built-in request testing and code samples. Swagger UI is the familiar classic, and the Swashbuckle.AspNetCore.SwaggerUi package lets you use it without Swashbuckle's generator. ReDoc suits read-only documentation portals. Visual Studio can also send requests from .http files, and VS Code offers similar extensions, which is often the quickest way to exercise an endpoint.

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.

Generating Clients with Kiota and NSwag#

A good document makes typed clients cheap. Two generators dominate in .NET:

GeneratorOutput styleStrengthsTrade-offs
Kiota (Microsoft)Fluent request builders that mirror URL pathsMany languages, handles very large APIs, path filtering, lock file for repeatable updatesMany generated files, runtime Microsoft.Kiota.* dependencies
NSwagOne C# class per client, optional interfacesFamiliar method-per-operation API, easy to mock, MSBuild integrationC# and TypeScript only, one very large file for big APIs
dotnet-openapi with OpenApiReferenceNSwag generation driven by an MSBuild itemRegenerates on build from a checked-in documentFew options, defaults to NSwag
Bash
# Kiota: install once, then generate from the committed document.
dotnet tool install --global Microsoft.OpenApi.Kiota
kiota generate -l CSharp -c CatalogClient -n Contoso.Catalog.Client \
  -d ./catalog-api.json -o ./Client
dotnet add package Microsoft.Kiota.Bundle   # in the client project

# NSwag: a single-file client with interfaces and an injected HttpClient.
dotnet tool install --global NSwag.ConsoleCore
nswag openapi2csclient /input:catalog-api.json /classname:CatalogClient \
  /namespace:Contoso.Catalog.Client /output:CatalogClient.cs /GenerateClientInterfaces:true

Kiota writes a kiota-lock.json file next to the generated code, recording the inputs so kiota update can regenerate consistently. Commit it with the sources. Wiring a Kiota client into dependency injection lets IHttpClientFactory manage connections, resilience handlers and telemetry:

C#
using Contoso.Catalog.Client;
using Microsoft.Kiota.Abstractions.Authentication;
using Microsoft.Kiota.Http.HttpClientLibrary;

builder.Services.AddHttpClient("catalog", c =>
    c.BaseAddress = new Uri("https://catalog.contoso.com"));
builder.Services.AddTransient(sp =>
{
    var http = sp.GetRequiredService<IHttpClientFactory>().CreateClient("catalog");
    var adapter = new HttpClientRequestAdapter(
        new AnonymousAuthenticationProvider(), httpClient: http);
    return new CatalogClient(adapter);
});

// In a consumer: request builders mirror the URL, so this calls GET /products/42.
var product = await catalog.Products[42].GetAsync(cancellationToken: ct);

The generated client is only as good as the document. Precise response types, required properties, operationId values from WithName, and consistent problem details all translate directly into better client code.

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:

C#
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.json

For 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:

C#
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.

AspectCode-firstAPI-first
Source of truthC# endpoints and typesThe OpenAPI document
Speed for one teamFast, with no duplicationSlower start, as the contract is written by hand
Cross-team alignmentConsumers wait for codeConsumers mock and build in parallel
Drift riskDocument always matches codeCode can drift unless contract tests enforce it
GovernanceLint and diff generated documents in CILint and review the contract before implementation
Good fitInternal APIs, single-team servicesPublic 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 operationId with WithName, 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 IResult without 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.OpenApi is the built-in generator from .NET 9. Register it with AddOpenApi, expose it with MapOpenApi, 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.

Further Reading#