Problem Context
OpenAPI is the JSON / YAML contract that says "here's every endpoint, every parameter, every response shape, every error type." In 2026, OpenAPI 3.1 is mainstream โ it's aligned with JSON Schema 2020-12, which means your validation rules are reusable across REST, AsyncAPI, and config files. .NET 9 shipsMicrosoft.AspNetCore.OpenApi built in (replacing Swashbuckle in default project templates), and the modern UI layer is Scalar or Redocly โ both significantly nicer than the classic Swagger UI.
The real win of a good OpenAPI doc isn't the rendered UI. It's code generation: typed clients with NSwag orMicrosoft Kiota, mock servers with Prism, contract tests with Schemathesis, and AI agents that can call your API because they read the spec.
- Your "OpenAPI doc" is a stale Notion page
- Three teams have hand-written three different SDKs for the same API
- Your spec lies โ the API actually returns a different shape
- You're still on Swashbuckle and it doesn't support 3.1
Generate the spec from code on .NET 9, render with Scalar, ship typed SDKs with Kiota โ the spec stops drifting.
Concept Explanation
- Code-first โ write C#, generate the spec. Fast iteration, spec follows code.
- Spec-first โ write the spec, generate stubs. Better for cross-team contracts.
- Hybrid โ generate from code, then lint/diff in CI to catch breaking changes.
flowchart LR
Code[".NET 9 endpoints<br/>+ XML docs"] --> Gen["AddOpenApi()<br/>MapOpenApi()"]
Gen --> Spec["openapi.json<br/>(OpenAPI 3.1)"]
Spec --> UI["Scalar UI<br/>or Redoc"]
Spec --> Sdk["Kiota / NSwag<br/>typed SDKs"]
Spec --> Lint["Spectral lint<br/>+ openapi-diff in CI"]
Spec --> AI["AI agents<br/>(tool calling)"]
style Spec fill:#85EA2D,color:#000,stroke:#3aa31d
Implementation
Step 1: Enable .NET 9 built-in OpenAPI
// .csproj
// <PropertyGroup>
// <GenerateDocumentationFile>true</GenerateDocumentationFile>
// <NoWarn>$(NoWarn);1591</NoWarn>
// </PropertyGroup>
builder.Services.AddOpenApi("v1", o =>
{
o.AddDocumentTransformer((doc, ctx, ct) =>
{
doc.Info.Title = "Orders API";
doc.Info.Version = "v1";
doc.Info.Description = "Order management endpoints.";
return Task.CompletedTask;
});
});
var app = builder.Build();
app.MapOpenApi("/openapi/{documentName}.json"); // serves the specStep 2: Document endpoints (Minimal API)
app.MapGet("/orders/{id:long}", async (long id, IOrderRepo repo) =>
{
var o = await repo.FindAsync(id);
return o is null ? Results.NotFound() : Results.Ok(o);
})
.WithName("GetOrder")
.WithSummary("Get an order by id")
.WithDescription("Returns a single order. 404 if not found.")
.Produces<OrderDto>(200)
.ProducesProblem(404)
.WithTags("Orders");
/// <summary>Place a new order.</summary>
/// <remarks>Total must be > 0. Idempotency-Key header is required.</remarks>
app.MapPost("/orders", async (CreateOrderCmd cmd, IOrderRepo repo) =>
Results.Created($"/orders/{await repo.CreateAsync(cmd)}", null))
.Accepts<CreateOrderCmd>("application/json")
.Produces(201)
.ProducesValidationProblem();Step 3: Render with Scalar (modern Swagger UI replacement)
// dotnet add package Scalar.AspNetCore
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference("/docs", o =>
{
o.WithTitle("Orders API")
.WithTheme(ScalarTheme.Purple)
.WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient);
});
}
// Browse to /docs โ fast, dark mode, sample requests in 20 languages.Step 4: Schema transformers for cross-cutting concerns
builder.Services.AddOpenApi(o =>
{
// Add Bearer auth scheme to every operation
o.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
// Treat all DateTimeOffset as ISO-8601 strings with examples
o.AddSchemaTransformer((schema, ctx, ct) =>
{
if (ctx.JsonTypeInfo.Type == typeof(DateTimeOffset))
schema.Example = OpenApiAnyFactory.CreateFromJson("\"2026-04-01T12:00:00Z\"");
return Task.CompletedTask;
});
});Step 5: Generate typed SDKs with Kiota
// Microsoft Kiota โ official Microsoft generator, 8 languages
// kiota generate -l csharp -d https://api.example.com/openapi/v1.json \
// -c OrdersClient -n Example.Orders.Client -o ./Generated
// Usage:
var http = new HttpClient { BaseAddress = new Uri("https://api.example.com") };
var auth = new BaseBearerTokenAuthenticationProvider(new TokenProvider());
var adapter = new HttpClientRequestAdapter(auth, httpClient: http);
var client = new OrdersClient(adapter);
var order = await client.Orders[42].GetAsync(); // strongly typedStep 6: Lint + diff in CI (catch breaking changes)
# .github/workflows/api.yml
# - run: dotnet run --project Api -- --export-openapi openapi.json
# - uses: stoplightio/spectral-action@latest # style + correctness lint
# - uses: oasdiff/oasdiff-action@main # breaking-change detection
# with:
# base: ./baseline/openapi.json
# revision: ./openapi.json
# fail-on: breaking
# Result: PR fails if a contract-breaking change ships without a version bump.Common Pitfalls
- Hand-written specs that drift.If the spec isn't generated from (or validated against) the running code, it will lie within a sprint. Either generate or contract-test.
- No
operationId. Generated SDK method names becomeGetUsers_2_post_v3(). Set.WithName("CreateOrder")on every endpoint. - Missing error schemas. A spec that only documents the happy path produces SDKs that throw
ApiExceptionfor everything. Documentapplication/problem+jsonfor 4xx/5xx. - Inline schemas everywhere. Same shape repeated across 12 endpoints = 12 generated types. Use
$ref/ shared DTOs. - Ignoring breaking-change detection. Renaming a field is a breaking change even if your tests still pass. Run
oasdiffin CI. - Still on OpenAPI 2 / Swashbuckle 5. 3.1 has nullable, oneOf/anyOf, JSON Schema 2020-12 alignment. Modern tooling (Kiota, AI agents) targets 3.1.
Practical Takeaways
- .NET 9 has built-in OpenAPI โ drop Swashbuckle from new projects.
- Render with Scalar or Redoc; both beat the classic Swagger UI.
- Set
WithName,Produces<T>,ProducesProblemon every endpoint. - Generate clients with Kiota or NSwag โ never hand-write SDKs.
- Document errors with
application/problem+json(RFC 9457). - Lint specs with Spectral; diff against baseline with oasdiff in CI.
- OpenAPI 3.1 + JSON Schema 2020-12 is the format AI agents read for tool-calling โ keep it accurate.

