# Migrating from Swashbuckle to Built-In OpenAPI in .NET 10: A Step-by-Step Guide

You upgraded a working API to .NET 10, hit build, and Swashbuckle fell apart. Missing namespaces, `OpenApiSchema` refusing to compile, `AddSecurityRequirement` complaining about a delegate it never wanted before. This is the most common upgrade wall .NET teams hit right now, and it has two valid exits: pin a Swashbuckle version that actually supports .NET 10, or migrate to the built-in OpenAPI document generator that ships with ASP.NET Core. This guide walks both paths, then gives you a step-by-step migration to `Microsoft.AspNetCore.OpenApi` that does not break your existing clients.

I have run this migration on several production APIs since .NET 9 first dropped Swashbuckle from the Web API template, and the pattern is always the same: the package swap takes twenty minutes, and the security scheme takes the rest of the afternoon. If you want the annotated, end-to-end version of these patterns with the edge cases already solved, the deeper walkthroughs live on [Patreon](https://www.patreon.com/CodingDroplets) with source you can run against your own project.

The reason this migration feels bigger than it is: the OpenAPI document is only half the job. What the document *describes* - versioned routes, Problem Details responses, the JWT bearer scheme - is where the real work sits. The v2 refresh of the [Zero to Production course](https://aspnetcoreapi.codingdroplets.com/) did exactly this migration inside a full production API, so Chapter 1 shows the Swashbuckle removal and the built-in generator wired up, and Chapter 7 shows the `BearerSecuritySchemeTransformer` that replaces the old Swagger Authorize button.

[![ASP.NET Core Web API: Zero to Production](https://newsletter.codingdroplets.com/images/aspnet-core-api-course-banner-1.jpg align="center")](https://aspnetcoreapi.codingdroplets.com/)

## Which Swashbuckle Version Is Compatible With .NET 10?

Short answer: **Swashbuckle.AspNetCore v10.0.0 or later**. Anything below that will not build cleanly against .NET 10, because the .NET 10 OpenAPI stack moved to `Microsoft.OpenApi` v2.x, and v2 is a hard break from the v1 object model that Swashbuckle 6.x through 9.x depend on.

Two rules from the maintainers that save you a bad afternoon:

1.  **Upgrade to v9.0.6 first**, then to v10. Going straight from an older 6.x to v10 stacks two sets of breaking changes on top of each other and you lose the ability to tell which one broke you.
    
2.  **Expect API-surface churn, not just a version bump.** Swashbuckle v10 pulls in Microsoft.OpenApi v2.3+, and that is where the compile errors come from.
    

The changes you will actually hit in your `Program.cs` and filters:

| Before (Microsoft.OpenApi v1) | After (Microsoft.OpenApi v2) |
| --- | --- |
| `using Microsoft.OpenApi.Models;` | `using Microsoft.OpenApi;` |
| `OpenApiSchema` everywhere | `IOpenApiSchema`, cast to `OpenApiSchema` to mutate |
| `schema.Type = "string"` | `schema.Type = JsonSchemaType.String` (flags enum) |
| `schema.Nullable = true` | `JsonSchemaType.Null` combined into the flags value |
| `AddSecurityRequirement(req)` | `AddSecurityRequirement(doc => req)` |
| `OpenApiReference { Type = ReferenceType.Schema }` | `OpenApiSchemaReference(...)` |

If your team has a pile of `IOperationFilter` and `ISchemaFilter` implementations, that table is your migration checklist. The official [Swashbuckle v10 migration guide](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/docs/migrating-to-v10.md) is the authority here and worth reading before you touch anything.

## Why Migrate to the Built-In OpenAPI Generator at All?

Staying on Swashbuckle v10 is a legitimate choice. Migrate when one of these is true for you:

*   **You want Native AOT.** Swashbuckle relies on reflection paths that AOT does not like. `Microsoft.AspNetCore.OpenApi` is built for it.
    
*   **You want the framework's own model.** The built-in generator uses the same `ApiExplorer` metadata ASP.NET Core already produces, so `[ProducesResponseType]`, minimal-API type inference, and `AllowAnonymous` are understood natively instead of being reverse-engineered by a filter.
    
*   **You want build-time documents.** Adding `Microsoft.Extensions.ApiDescription.Server` emits the OpenAPI JSON during `dotnet build`, which means you can diff the contract in CI and fail the build on a breaking change.
    
*   **You want OpenAPI 3.1 by default.** .NET 10 generates 3.1 documents out of the box.
    
*   **You are tired of owning the dependency.** In production I have seen more than one release blocked because a documentation package had not shipped support for the new runtime yet. The in-box generator ships with the framework.
    

Stay on Swashbuckle if you depend on Swashbuckle-specific annotations (`SwaggerOperation`, `SwaggerSchema`), on `EnableAnnotations`, or on a large filter library you are not ready to rewrite. Both are defensible. If you are still weighing the options, we broke the tooling choice down in detail in [Scalar vs Swashbuckle vs NSwag in ASP.NET Core](https://codingdroplets.com/scalar-vs-swashbuckle-vs-nswag-in-asp-net-core-which-openapi-tool-should-your-net-team-use-in-2026).

## The Step-by-Step Migration Path

### Step 1: Get to .NET 10 First, With Swashbuckle Still In Place

Do not migrate the runtime and the documentation stack in the same commit. Move the TFM to `net10.0`, bump Swashbuckle to v9.0.6, fix whatever breaks, and ship that. You now have a green baseline to migrate from. This one discipline is the difference between a two-hour migration and a two-day bisect.

### Step 2: Swap the Packages

Remove `Swashbuckle.AspNetCore`. Add the generator and a UI:

```bash
dotnet remove package Swashbuckle.AspNetCore
dotnet add package Microsoft.AspNetCore.OpenApi
dotnet add package Scalar.AspNetCore
```

`Microsoft.AspNetCore.OpenApi` generates the document. It does **not** ship a UI - that is deliberate. Scalar is the UI that replaced Swagger UI in the ASP.NET Core templates from .NET 9 onward.

### Step 3: Rewrite the Registration

The old pair of calls becomes a new pair:

```csharp
// Before
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
app.UseSwagger();
app.UseSwaggerUI();

// After (.NET 10)
builder.Services.AddOpenApi();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}
```

Your document now serves from `/openapi/v1.json`, not `/swagger/v1/swagger.json`. Write that down - it is the single most common thing teams forget, and it silently breaks every client generator, CI contract check, and API gateway import that had the old path hardcoded.

Also update `launchSettings.json`: change `"launchUrl": "swagger"` to `"launchUrl": "scalar/v1"` so F5 still lands somewhere useful.

### Step 4: Replace Filters With Transformers

This is the conceptual jump. Swashbuckle had `IDocumentFilter`, `IOperationFilter`, and `ISchemaFilter`. The built-in generator has three transformers with the same shape, registered on `OpenApiOptions`:

```csharp
builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
    options.AddOperationTransformer((operation, context, ct) =>
    {
        operation.Responses ??= new OpenApiResponses();
        operation.Responses.Add("500", new OpenApiResponse { Description = "Internal server error" });
        return Task.CompletedTask;
    });
});
```

Execution order matters and is well defined: **schema transformers run first** (all schemas are registered before any operation is processed), **then operation transformers**, **then document transformers** as the final pass. If a document transformer of yours needs a schema that an operation transformer added, that ordering is why it works. If it needs something a document transformer added earlier, register that one first - within a category they run in registration order.

One .NET 10 addition worth knowing: transformer contexts expose `GetOrCreateSchemaAsync`, so a transformer can generate a schema for a C# type using the framework's own logic and add it to the document with `AddComponent`. That is how you add a shared `ProblemDetails` error response without hand-writing the schema.

### Step 5: Re-Add the JWT Bearer Scheme

Swashbuckle's `AddSecurityDefinition` is gone. The replacement is a DI-activated document transformer that reads the real authentication schemes and writes them into `components.securitySchemes`:

```csharp
internal sealed class BearerSecuritySchemeTransformer(
    IAuthenticationSchemeProvider schemeProvider) : IOpenApiDocumentTransformer
{
    public async Task TransformAsync(OpenApiDocument document,
        OpenApiDocumentTransformerContext context, CancellationToken ct)
    {
        var schemes = await schemeProvider.GetAllSchemesAsync();
        if (!schemes.Any(s => s.Name == "Bearer")) return;

        document.Components ??= new OpenApiComponents();
        document.Components.SecuritySchemes = new Dictionary<string, IOpenApiSecurityScheme>
        {
            ["Bearer"] = new OpenApiSecurityScheme
            {
                Type = SecuritySchemeType.Http,
                Scheme = "bearer",
                In = ParameterLocation.Header,
                BearerFormat = "Json Web Token"
            }
        };
    }
}
```

Note that this registers the *scheme*. Applying it as a *requirement* per operation is a separate step, and you almost certainly want it conditional - skip any operation whose endpoint metadata carries `AllowAnonymousAttribute`, or your login endpoint will render as locked. Use an operation transformer for that, because only operation transformers see `context.Description.ActionDescriptor.EndpointMetadata`.

### Step 6: Reconnect Versioning and XML Comments

If you use `Asp.Versioning.Mvc`, wire the API explorer group name format and register one named document per version:

```csharp
builder.Services.AddOpenApi("v1");
builder.Services.AddOpenApi("v2");
```

Each call takes its own options, and the framework decides membership through the `ShouldInclude` delegate on `OpenApiOptions` - by default it matches the endpoint's group name to the document name.

For XML comments, there is genuinely good news. In .NET 10 you no longer register an XML file path by hand. Set `<GenerateDocumentationFile>true</GenerateDocumentationFile>` in the project file and `Microsoft.AspNetCore.OpenApi` picks up the comments from your assembly and from any `ProjectReference` that also has the property set. A source generator processes them at compile time, so the runtime cost is close to nothing. Supported tags include `<c>`, `<code>`, `<list>`, `<para>`, `<see>`, `<seealso>`, and `<inheritdoc>`.

### Step 7: Verify the Contract, Not the Page

Do not sign this migration off because Scalar renders. Diff the documents:

```bash
curl -s https://localhost:5001/openapi/v1.json > new.json
# compare against the swagger.json you captured before Step 2
```

Look specifically at operation IDs, schema component names, required flags, and enum representation. These are the fields client generators consume, and a silent difference here surfaces as a broken SDK three sprints later.

## Common Migration Pitfalls

*   **The document URL changed.** `/swagger/v1/swagger.json` is now `/openapi/v1.json`. Update CI contract checks, gateway imports, and NSwag/Kiota client generation configs.
    
*   `MapOpenApi` **sits behind an environment check in the template.** Copy the template blindly and your staging environment has no document at all. Decide deliberately whether the document should be public.
    
*   **Schema component names shifted.** Class and record schemas get a `$ref` into `components.schemas` when they appear more than once, enums always get one, and primitives stay inline. If your generated client names change, `CreateSchemaReferenceId` on `OpenApiOptions` is the knob.
    
*   **Every endpoint looks locked in the UI.** You applied the security requirement globally in a document transformer instead of conditionally in an operation transformer.
    
*   `WithOpenApi()` **calls left behind.** These belong to the earlier minimal-API metadata story and should be replaced by `AddOpenApiOperationTransformer` or plain metadata attributes.
    
*   **Swashbuckle annotations silently do nothing.** `[SwaggerOperation]` and friends have no meaning to the built-in generator. Nothing errors. The description just disappears.
    
*   **You skipped the build-time document.** Adding `Microsoft.Extensions.ApiDescription.Server` gives you a checked-in JSON file per build, which makes the next contract change reviewable in a pull request instead of a mystery.
    

## Verification Checklist Before You Merge

*   \[ \] `dotnet build` clean, no `Swashbuckle` reference left in any `.csproj`
    
*   \[ \] `/openapi/v1.json` returns a 3.1 document with the expected `info` block
    
*   \[ \] Every previously documented endpoint is present, with the same operation IDs
    
*   \[ \] `components.securitySchemes` contains `Bearer`, and anonymous endpoints are not marked as secured
    
*   \[ \] XML summaries appear on operations and schemas
    
*   \[ \] Versioned documents resolve at each `/openapi/{name}.json`
    
*   \[ \] Client generation (Kiota, NSwag, openapi-generator) runs green against the new document
    
*   \[ \] `launchSettings.json` points at the new UI path
    

If you are doing this as part of a wider runtime upgrade, the sequencing advice in [Modernizing an ASP.NET Core API to .NET 10](https://codingdroplets.com/modernize-aspnet-core-api-dotnet-10) pairs well with this checklist.

## Frequently Asked Questions

### Is Swashbuckle Dead in .NET 10?

No. Swashbuckle.AspNetCore v10 supports .NET 10 and OpenAPI 3.1. What changed is that Microsoft removed it from the ASP.NET Core Web API template starting with .NET 9 and now ships a first-party generator instead. Swashbuckle is a community package that you now opt into rather than the default you inherit.

### Do I Have to Replace Swagger UI With Scalar?

No. `Microsoft.AspNetCore.OpenApi` only produces the document, so any UI that can read an OpenAPI 3.1 JSON file works - Scalar, Redoc, Swagger UI standalone, or your own portal. Scalar is simply what the .NET templates default to now.

### What Happens to My IOperationFilter and ISchemaFilter Classes?

They do not carry over. Rewrite them as `IOpenApiOperationTransformer` and `IOpenApiSchemaTransformer` implementations. The logic usually transfers almost line for line; what changes is the registration (`AddOperationTransformer<T>()` on `OpenApiOptions`) and the Microsoft.OpenApi v2 object model, mainly `JsonSchemaType` replacing string type names.

### How Do I Generate the OpenAPI Document at Build Time in .NET 10?

Add the `Microsoft.Extensions.ApiDescription.Server` package. It runs a `GetDocument` step during `dotnet build` and writes the JSON to disk. Note that build-time YAML output is not supported yet, and progress messages are hidden by the default Terminal Logger verbosity, so raise verbosity if you need to see the step run.

### Can I Run Swashbuckle and the Built-In Generator Side by Side During Migration?

Yes, and for a large API it is the safest route. Both can be registered at once, serving `/swagger/v1/swagger.json` and `/openapi/v1.json` in parallel. Keep them both alive for one release, diff the two documents, point consumers at the new URL, then delete the Swashbuckle registration. The overlap costs you a few milliseconds of startup and buys you a rollback that does not need a deployment.

### Does the Built-In Generator Support Native AOT?

Yes, and it is one of the main reasons to migrate. The generator is designed around the framework's own `ApiExplorer` metadata and the System.Text.Json source generator rather than the runtime reflection paths that make Swashbuckle unfriendly to trimming and AOT compilation.

* * *

## About the Author

I'm Celin Daniel, Co-founder of [Coding Droplets](https://codingdroplets.com/). I've been building .NET and ASP.NET Core systems in production for 13+ years - APIs, distributed backends, enterprise platforms. Everything I write here comes from real shipping experience: patterns that held up, trade-offs that bit us, and lessons learned the hard way.

*   GitHub: [codingdroplets](http://github.com/codingdroplets/)
    
*   YouTube: [Coding Droplets](https://www.youtube.com/@CodingDroplets)
    
*   Website: [codingdroplets.com](https://codingdroplets.com/)
