"Swagger" and "OpenAPI" get used interchangeably, and untangling that is actually the first useful thing to do here, one is a specification, the other is a tool that renders it. This post uses the Product API from an earlier CRUD post as the running example throughout, since the goal is showing exactly how documentation gets generated from code you've already written, not introducing a new domain to learn alongside a new concept.
What Swagger and OpenAPI Actually Are
OpenAPI is a specification, a structured JSON or YAML document that precisely describes an API: every endpoint, every parameter, every request body shape, every possible response. Swagger UI is one specific tool that takes that OpenAPI document and renders it as an interactive, browsable webpage, letting someone read the docs and actually try a request right from the browser. Swagger existed before OpenAPI became the open, vendor-neutral standard name for the specification format, which is exactly why people still say "Swagger" when they technically mean "OpenAPI."
Think of a restaurant's recipe card versus its printed menu. The recipe card in the kitchen is the precise, structured truth, exact ingredients, exact quantities, exact steps. The printed menu a customer reads is a different, friendlier presentation of that same underlying information, letting the customer browse and choose without ever seeing the recipe card itself. OpenAPI is the recipe card. Swagger UI is the printed menu.
What the Actual Generated Document Looks Like
Before going further, it helps to actually see one. This is a trimmed excerpt of the real JSON that AddSwaggerGen() would produce for the GetById endpoint used throughout this post, the file a browser fetches at /swagger/v1/swagger.json, and the exact same file Swagger UI reads to render its interactive page.
{
"openapi": "3.0.1",
"info": {
"title": "Products API",
"version": "v1"
},
"paths": {
"/api/Products/{id}": {
"get": {
"tags": ["Products"],
"operationId": "GetById",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": { "type": "integer", "format": "int32" }
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ProductDto" }
}
}
},
"404": {
"description": "Not Found"
}
}
}
}
},
"components": {
"schemas": {
"ProductDto": {
"type": "object",
"properties": {
"id": { "type": "integer", "format": "int32" },
"name": { "type": "string", "nullable": true },
"price": { "type": "number", "format": "double" },
"stockQuantity": { "type": "integer", "format": "int32" }
}
}
}
}
}
Every piece of this document maps directly to something already written in the code, "paths" comes from the controller's routes and HTTP verbs, the "200" and "404" entries come straight from the ProducesResponseType attributes covered next, and the ProductDto schema under "components" is a direct reflection of the DTO class itself, one property per line. This is genuinely the entire point of the post: nothing here was typed by hand into a documentation tool, it was all generated from code that already existed for other reasons.
How the Documentation Actually Gets Generated
The key thing to understand: you don't hand-write this specification. It's generated automatically by reflecting over your existing controllers, DTOs, and attributes, which is exactly why it can't silently drift out of date the way a manually maintained wiki page does.
// Program.cs - the two lines that turn this on
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapControllers();
app.Run();
// Visiting /swagger now shows a full interactive UI,
// built entirely from the ProductsController and DTOs
// already written for the CRUD post - nothing new to write
The Attributes That Shape the Generated Docs
By default, Swagger generates something reasonable but generic. Specific attributes let you control exactly what shows up, the response shapes, the possible status codes, and human-readable descriptions.
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
[ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ProductDto>> GetById(int id)
{
var product = await _productService.GetByIdAsync(id);
if (product == null)
return NotFound();
return Ok(product);
}
}
These two ProducesResponseType attributes document both outcomes directly, a successful call returns a ProductDto shape with a 200, and a 404 is a documented, expected possibility, not a surprise. A frontend developer reading these generated docs never has to guess what a 404 from this specific endpoint means, or reverse-engineer it by triggering the error themselves.
// Adding a human-readable summary per endpoint
[HttpPost]
[SwaggerOperation(
Summary = "Creates a new product",
Description = "Validates the input and persists a new product record."
)]
public async Task<ActionResult<ProductDto>> Create(CreateProductDto dto)
{
var created = await _productService.CreateAsync(dto);
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
}
// Requires the Swashbuckle.AspNetCore.Annotations NuGet package
// for [SwaggerOperation] specifically
Validation Attributes Show Up in the Docs Automatically
Here's a nice callback to the CRUD post: CreateProductDto's validation attributes weren't just added there to reject bad requests server-side, they do double duty, since Swagger reads them too and documents the actual constraints directly.
public class CreateProductDto
{
[Required, MaxLength(200)]
public string Name { get; set; }
[Range(0.01, 1000000)]
public decimal Price { get; set; }
[Range(0, int.MaxValue)]
public int StockQuantity { get; set; }
}
// In the generated Swagger UI, the Name field now shows
// as required with a 200-character max, and Price shows
// its valid range - all from attributes that were ALREADY
// there for a completely different reason (server-side
// validation), now also serving as documentation
Documenting Security Schemes
Without configuration, Swagger UI has no "Authorize" button and no way to actually test an authenticated endpoint from the browser. Adding a security scheme fixes this.
builder.Services.AddSwaggerGen(options =>
{
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "Enter a valid JWT token"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] { }
}
});
});
// Swagger UI now shows an "Authorize" button - a caller
// can paste in a token once, and every subsequent
// "Try it out" request in the UI includes it automatically
Showing Multiple API Versions Side by Side
The same versioning concept from the APIM Part 2 post applies directly here, Swagger can present multiple documented versions of an API, letting a caller see exactly what changed between v1 and v2.
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = "Products API", Version = "v1" });
options.SwaggerDoc("v2", new OpenApiInfo { Title = "Products API", Version = "v2" });
});
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "Products API v1");
options.SwaggerEndpoint("/swagger/v2/swagger.json", "Products API v2");
});
// A dropdown in Swagger UI now lets a caller switch between
// documented versions directly, rather than needing separate
// documentation pages maintained independently
The Practical Payoff: Exporting Into Postman
A well-documented OpenAPI spec isn't just for humans reading a webpage, it's a structured document another tool can consume directly. Postman can import it and build a complete, ready-to-use collection automatically.
With the app running, the raw spec is available at /swagger/v1/swagger.json. In Postman, choose Import and paste that URL, or upload the downloaded JSON file directly. Postman then generates a full collection automatically, every endpoint from ProductsController, correct HTTP verbs, correct routes, even example request bodies shaped according to CreateProductDto's properties.
This is the direct bridge between writing good Swagger documentation and having a genuinely useful Postman collection, without manually building every request by hand.
Problem Scenario and Solving Strategy
The problem: a frontend team keeps asking what fields the Create Product endpoint actually expects, what a 404 means specifically on GetById, and what the exact shape of a successful response looks like. Answers currently live in a wiki page that was accurate three months ago and hasn't been touched since.
The strategy, step by step:
First, recognize this is a documentation freshness problem, not a communication problem, the wiki isn't wrong because nobody cares, it's wrong because nothing keeps it in sync with the actual code.
Second, enable Swagger generation if it isn't already, AddSwaggerGen and UseSwaggerUI, since this alone produces accurate docs for every route, verb, and DTO shape, generated directly from the real, currently-running code.
Third, add ProducesResponseType attributes to each action for every realistic outcome, 200, 404, 400, since this is what actually documents what a given status code means for this specific endpoint, not just that it's possible.
Fourth, confirm CreateProductDto's existing validation attributes are already doing double duty as documentation, no extra work needed here, just point the frontend team at the generated docs instead of the stale wiki page.
Finally, delete or clearly mark the old wiki page as deprecated, redirecting to the live Swagger UI, since the single source of truth is now the running application itself, not a separate document someone has to remember to update.
Key Lessons
OpenAPI is the specification, a structured document, while Swagger UI is one tool that renders it as a browsable page, the two terms get used interchangeably but describe different things.
The documentation is generated from code that already exists, controllers, DTOs, attributes, which is exactly why it can't silently go stale the way a manually maintained wiki page does.
ProducesResponseType documents both success and failure shapes explicitly, turning "this might return a 404" into a documented, expected part of the contract.
Validation attributes on a DTO do double duty, the same Required and Range attributes enforcing rules server-side also document those exact constraints automatically.
A security scheme definition is what actually makes the Authorize button in Swagger UI functional for testing authenticated endpoints directly in the browser.
The OpenAPI spec is machine-readable, not just human-readable, Postman and other tools can import it directly to generate a complete, accurate collection automatically.
What's Next
The next post covers Postman itself in depth, collections, environment variables, where secrets actually belong, auth headers, request building, status codes, and writing tests, including the exact import workflow referenced at the end of this post.
Summary
Swagger and OpenAPI solve a problem every API eventually has, documentation that's accurate the day it's written and wrong a month later. Generating the docs directly from the code, rather than writing them separately, is what actually keeps that from happening: a validation attribute, a response type, a security scheme, each one written once for a different practical reason, and each one automatically becoming part of the documentation too. The real payoff shows up at the boundary between tools, a well-documented API can hand its entire shape directly to something like Postman, turning "read the docs and manually build a request" into "import the spec and get a working collection immediately."
Originally published at TechStack Blog:
https://www.techstackblog.com/post.html?slug=swagger-openapi-explained-with-example
More from TechStack Blog: C# / .NET: https://www.techstackblog.com/category.html?cat=csharp
CS Fundamentals: https://www.techstackblog.com/category.html?cat=cs-fundamentals
Top comments (0)