Swagger and OpenAPI
Front-end and mobile developers should not have to read your C# to call the API. An OpenAPI document describes every endpoint, parameter and response in a standard JSON format — the basis for interactive docs, generated clients and contract tests. .NET 9 generates it for you.
On this page · 6 min · Intermediate
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 29, 2026
Related tutorials
In simple words#
OpenAPI is the printed menu of your API: every endpoint, input and output described, so others can order correctly — and Swagger UI lets them try it.
- OpenAPI
- Standard JSON description of an API.
- Swagger UI
- Interactive docs page.
- Client generation
- Create typed clients from the spec.
Learning objectives#
- Generate an OpenAPI document with AddOpenApi and MapOpenApi
- Describe responses with ProducesResponseType and summaries
- Customise the document with transformers
- Add an interactive UI (Scalar or Swagger UI)
Explanation#
.NET 9 includes Microsoft.AspNetCore.OpenApi: builder.Services.AddOpenApi() and app.MapOpenApi() serve the document at /openapi/v1.json. (Earlier templates used the Swashbuckle package; it still works but is no longer in the template.)
The generator reads routes, parameters, request bodies and return types. Add [ProducesResponseType<ProductDto>(200)] and [ProducesResponseType(404)] so error responses are documented too; ActionResult<T> and TypedResults help automatically.
[EndpointSummary] and [EndpointDescription] (or .WithSummary() for minimal APIs) give each operation a human-readable summary.
Document transformers edit the whole document: set the title, version, description, contact, and add security schemes such as Bearer JWT.
For an interactive UI, add Scalar (app.MapScalarApiReference()) or Swashbuckle’s Swagger UI pointing at /openapi/v1.json. Expose docs only where appropriate — often just in Development or behind auth.
Syntax#
builder.Services.AddOpenApi();
app.MapOpenApi(); // GET /openapi/v1.json
[HttpGet("{id:int}")]
[EndpointSummary("Get one product")]
[ProducesResponseType<ProductDto>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<ProductDto> Get(int id) => …;
Code example: Generating and reading the OpenAPI document#
// Program.cs
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders(); // keep the demo output clean
builder.Services.AddControllers();
builder.Services.AddOpenApi(options => options.AddDocumentTransformer((document, context, ct) =>
{
document.Info.Title = "ShopSmart API";
document.Info.Description = "Products, carts and orders for the ShopSmart store.";
return Task.CompletedTask;
}));
var app = builder.Build();
app.MapOpenApi(); // /openapi/v1.json
app.MapControllers();
app.Urls.Add("http://127.0.0.1:5222");
await app.StartAsync();
using var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:5222") };
var document = JsonDocument.Parse(await http.GetStringAsync("/openapi/v1.json")).RootElement;
Console.WriteLine(quot;OpenAPI {document.GetProperty("openapi")}: {document.GetProperty("info").GetProperty("title")} ({document.GetProperty("info").GetProperty("version")})");
foreach (var path in document.GetProperty("paths").EnumerateObject())
foreach (var operation in path.Value.EnumerateObject())
{
var summary = operation.Value.TryGetProperty("summary", out var s) ? s.GetString() : "";
var codes = string.Join(", ", operation.Value.GetProperty("responses").EnumerateObject().Select(r => r.Name));
Console.WriteLine(quot; {operation.Name.ToUpperInvariant(),-6} {path.Name,-24} {summary,-22} responses: {codes}");
}
Console.WriteLine(quot;Schemas: {string.Join(", ", document.GetProperty("components").GetProperty("schemas").EnumerateObject().Select(x => x.Name))}");
await app.StopAsync();
// Controllers/ProductsController.cs
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
public record ProductDto(int Id, string Name, decimal Price);
public record CreateProductDto([Required] string Name, [Range(1, 1_000_000)] decimal Price);
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
[HttpGet]
[EndpointSummary("List products")]
public IEnumerable<ProductDto> GetAll() => [new(1, "Wireless Mouse", 799m)];
[HttpGet("{id:int}")]
[EndpointSummary("Get one product")]
[ProducesResponseType<ProductDto>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<ProductDto> Get(int id) => id == 1 ? new ProductDto(1, "Wireless Mouse", 799m) : NotFound();
[HttpPost]
[EndpointSummary("Create a product")]
[ProducesResponseType<ProductDto>(StatusCodes.Status201Created)]
[ProducesResponseType<ValidationProblemDetails>(StatusCodes.Status400BadRequest)]
public ActionResult<ProductDto> Create(CreateProductDto dto) =>
CreatedAtAction(nameof(Get), new { id = 2 }, new ProductDto(2, dto.Name, dto.Price));
}
OpenAPI 3.0.1: ShopSmart API (1.0.0)
GET /api/products List products responses: 200
POST /api/products Create a product responses: 201, 400
GET /api/products/{id} Get one product responses: 200, 404
Schemas: CreateProductDto, ProblemDetails, ProductDto, ValidationProblemDetailsThis example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- AddOpenApi registers the generator; the document transformer sets a friendly title and description.
- MapOpenApi serves the document at /openapi/v1.json, which the demo reads like any client would.
- Each operation lists its summary and documented response codes, including 404 and 400.
- Request and response types become reusable schemas (ProductDto, CreateProductDto, ValidationProblemDetails…).
- Tools such as Scalar, Swagger UI, NSwag or Kiota turn this document into interactive docs and typed clients.
Real-world use case#
ShopSmart’s Android and iOS teams regenerate their API clients from /openapi/v1.json in CI. When a pull request changes a response type, the generated client stops compiling — before the app ever ships.
Common mistakes#
Clients need to know about 400, 404 and 409 just as much as 200. Add ProducesResponseType for each.
The generator cannot know the success type. Use ActionResult<T>, TypedResults or ProducesResponseType<T>.
Interactive docs for admin endpoints on the public internet help attackers. Restrict them.
Best practices#
- AddOpenApi + MapOpenApi (.NET 9+)
- Summaries and all response types documented
- Transformers for title, version and security
- Generate clients from the document
Remember this#
/openapi/v1.json is generated from your endpoints.
ProducesResponseType for every status a client may see.
Scalar or Swagger UI reads the same document.
Practice exercise#
Your task: Add the Scalar interactive UI in Development only, available at /scalar/v1.
Need a hint?
Show solution
// Program.cs
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi(); // /openapi/v1.json
app.MapScalarApiReference(); // /scalar/v1
}
app.MapControllers();
app.Run();
Mini quiz#
Where does MapOpenApi serve the default document in .NET 9?
Want more? Practise all ASP.NET Core Web API MCQs · ASP.NET Core Web API interview questions
My notes#
Saved in this browser.