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

Search for a command to run...

No comments yet. Be the first to comment.
You built the pipeline. Documents are chunked, embedded, and sitting in a vector store. Retrieval returns results. And your RAG answers still hallucinate in .NET, confidently telling users about a stu

You add an entity, run dotnet ef migrations add AddOrders, and the tooling stops dead: Unable to create an object of type 'AppDbContext'. For the different patterns supported at design time, see https

Tool calling is the moment an LLM stops being a text generator and starts touching your systems. Securing LLM tool calling in ASP.NET Core is therefore not really an AI problem - it is an authorizatio

Retrieval-Augmented Generation lives or dies on one unglamorous step, and it is not the model or the vector database. It is chunking. When you get chunking documents for RAG in .NET wrong, the model r

Coding Droplets
301 posts
Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.
From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.
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 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 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.
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:
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.
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 is the authority here and worth reading before you touch anything.
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.
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.
Remove Swashbuckle.AspNetCore. Add the generator and a UI:
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.
The old pair of calls becomes a new pair:
// 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.
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:
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.
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:
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.
If you use Asp.Versioning.Mvc, wire the API explorer group name format and register one named document per version:
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>.
Do not sign this migration off because Scalar renders. Diff the documents:
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.
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.
[ ] 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 pairs well with this checklist.
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.
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.
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.
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.
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.
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.
I'm Celin Daniel, Co-founder of Coding Droplets. 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
YouTube: Coding Droplets
Website: codingdroplets.com