By WebNest Studio Engineering Team
Spring Boot Tutorial
API Documentation with OpenAPI (springdoc-openapi)
As a REST API grows, keeping documentation in sync by hand becomes unreliable. springdoc-openapi is a library that inspects your Spring MVC controllers, request/response DTOs, and validation annotations at runtime and automatically generates an OpenAPI (formerly Swagger) specification — plus a browsable Swagger UI page — without you writing separate documentation files.
What OpenAPI Is
OpenAPI is a standard, language-neutral format (JSON or YAML) for describing an HTTP API: its paths, parameters, request and response bodies, status codes and security. Because the format is standard, one file serves many tools. Swagger UI turns it into interactive documentation, code generators build typed clients in TypeScript, Kotlin or Python from it, API gateways import it, and contract-testing tools check that the running API still matches it. "Swagger" was the name of version 2 of the specification; since version 3 it is called OpenAPI, and Swagger is the name of the tooling around it.
Adding springdoc-openapi
Adding the springdoc-openapi-starter-webmvc-ui dependency is enough to get a working setup: once the app starts, the raw OpenAPI JSON is available at /v3/api-docs (and YAML at /v3/api-docs.yaml), and an interactive documentation UI is served at /swagger-ui.html, which redirects to /swagger-ui/index.html. There you can browse every endpoint, see its parameters and response shapes, and even send test requests directly from the browser. Use springdoc 3.x with Spring Boot 4 (3.1.1 is current); the 2.x line is for Spring Boot 3. springdoc 3 generates an OpenAPI 3.1 document.
What springdoc Reads from Your Code
Without a single annotation, springdoc already knows a lot. It reads the paths and HTTP methods from @GetMapping, @PostMapping and friends, path and query parameters from @PathVariable and @RequestParam, and the body types from @RequestBody and the return type. It also turns Bean Validation annotations into schema rules: @NotBlank or @NotNull marks the field as required, @Size(max = 80) becomes maxLength: 80, and @DecimalMin("0.01") becomes minimum: 0.01. Your validation rules and your documentation therefore cannot drift apart, because they are the same annotations.
Enriching the Generated Documentation
The base generation is inferred purely from your code, but annotations let you add human-readable detail: @Tag groups a controller's endpoints under one heading, @Operation(summary, description) describes a controller method, @ApiResponse documents specific status codes and their meaning, @Parameter explains a parameter, and @Schema on DTO fields describes individual fields and gives example values. This turns a bare technical listing into documentation another team (or a frontend developer) can actually use without reading your source code. For the API as a whole, put @OpenAPIDefinition(info = @Info(title, version, description)) on any configuration class.
Documenting Security Requirements
For an API protected by JWT or another auth scheme, declare the scheme once with @SecurityScheme(name = "bearerAuth", type = SecuritySchemeType.HTTP, scheme = "bearer", bearerFormat = "JWT"). Then mark protected operations with security = @SecurityRequirement(name = "bearerAuth") inside @Operation, or put @SecurityRequirement on the whole controller. Swagger UI then shows an "Authorize" button and a lock icon on those operations; a developer pastes in a token once and it is sent with every test request. This only documents the security; Spring Security still has to enforce it.
Turning It Off in Production
The springdoc endpoints are on by default, and springdoc logs a WARN at startup to remind you. For an internal API, disable both in the production profile with springdoc.api-docs.enabled=false and springdoc.swagger-ui.enabled=false; both URLs then return 404. If the documentation must stay available, protect the paths /v3/api-docs/** and /swagger-ui/** with Spring Security instead. You can also move them with springdoc.api-docs.path and springdoc.swagger-ui.path.
Examples
Annotated controller, DTOs and API metadata (tested with springdoc 3.1.1 on Spring Boot 4.1.1)
<!-- pom.xml -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>3.1.1</version>
</dependency>
---
@SpringBootApplication
@OpenAPIDefinition(info = @Info(title = "Webnest Product API", version = "1.4.0",
description = "Catalog endpoints used by the storefront"))
@SecurityScheme(name = "bearerAuth", type = SecuritySchemeType.HTTP,
scheme = "bearer", bearerFormat = "JWT")
public class App {
public static void main(String[] args) { SpringApplication.run(App.class, args); }
}
---
@RestController
@RequestMapping("/api/products")
@Tag(name = "Products", description = "Operations for managing catalog products")
public class ProductController {
public record ProductResponse(
@Schema(description = "Product ID", example = "5") Long id,
@Schema(description = "Display name", example = "Wireless Mouse") String name,
@Schema(description = "Price in USD", example = "19.99") BigDecimal price) {}
public record CreateProductRequest(
@Schema(description = "Display name", example = "Wireless Mouse")
@NotBlank @Size(max = 80) String name,
@Schema(description = "Price in USD", example = "19.99")
@DecimalMin("0.01") BigDecimal price) {}
@Operation(summary = "Get a product by ID")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Product found"),
@ApiResponse(responseCode = "404", description = "Product not found",
content = @Content) // no body for 404
})
@GetMapping("/{id}")
public ResponseEntity<ProductResponse> getById(
@Parameter(description = "ID of the product to retrieve") @PathVariable Long id) {
if (id > 100) return ResponseEntity.notFound().build();
return ResponseEntity.ok(new ProductResponse(id, "Wireless Mouse", new BigDecimal("19.99")));
}
@Operation(summary = "Create a product", security = @SecurityRequirement(name = "bearerAuth"))
@ApiResponse(responseCode = "201", description = "Product created")
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ProductResponse create(@Valid @RequestBody CreateProductRequest request) {
return new ProductResponse(6L, request.name(), request.price());
}
}
GET /swagger-ui.html -> 302 redirect to /swagger-ui/index.html (Swagger UI)
GET /v3/api-docs.yaml -> 200 (the same spec as YAML)
GET /v3/api-docs -> (shortened)
{
"openapi": "3.1.0",
"info": {"title": "Webnest Product API", "description": "Catalog endpoints used by the storefront", "version": "1.4.0"},
"tags": [{"name": "Products", "description": "Operations for managing catalog products"}],
"paths": {
"/api/products": {"post": {"summary": "Create a product", "operationId": "create",
"requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/CreateProductRequest"}}}, "required": true},
"responses": {"201": {"description": "Product created", ...}},
"security": [{"bearerAuth": []}]}},
"/api/products/{id}": {"get": {"summary": "Get a product by ID", "operationId": "getById",
"parameters": [{"name": "id", "in": "path", "description": "ID of the product to retrieve",
"required": true, "schema": {"type": "integer", "format": "int64"}}],
"responses": {"200": {"description": "Product found", "content": {"*/*": {"schema": {"$ref": "#/components/schemas/ProductResponse"}}}},
"404": {"description": "Product not found"}}}}
},
"components": {
"schemas": {
"CreateProductRequest": {"type": "object", "required": ["name"], "properties": {
"name": {"type": "string", "description": "Display name", "example": "Wireless Mouse", "maxLength": 80, "minLength": 0},
"price": {"type": "number", "description": "Price in USD", "example": 19.99, "minimum": 0.01}}},
"ProductResponse": {...}
},
"securitySchemes": {"bearerAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}}
}
}
Why the 404 response needs content = @Content
// Without content = @Content:
@ApiResponse(responseCode = "404", description = "Product not found")
"404": {"description": "Product not found",
"content": {"*/*": {"schema": {"$ref": "#/components/schemas/ProductResponse"}}}}
(springdoc copies the method's return type onto every documented response, so the docs
claim a 404 returns a product. An empty @Content removes the body from that response.)
Disabling the docs in the prod profile
# application-prod.properties
springdoc.api-docs.enabled=false
springdoc.swagger-ui.enabled=false
Without these settings springdoc logs at startup:
WARN ... SpringDocAppInitializer : SpringDoc /v3/api-docs endpoint is enabled by default. To disable it in production, set the property 'springdoc.api-docs.enabled=false'
With them:
GET /v3/api-docs -> 404
GET /swagger-ui.html -> 404
Common Mistakes
- Assuming springdoc-openapi requires manually writing a YAML/JSON spec file — it generates the spec automatically from your existing controllers and DTOs.
- Leaving the Swagger UI publicly exposed in production without any access restriction, which reveals your full API surface to anyone.
- Not annotating DTOs with @Schema descriptions, leaving the generated docs technically complete but not actually explanatory for consumers.
- Confusing springdoc-openapi (current, active library) with the older springfox-swagger2, which is largely unmaintained for modern Spring Boot versions.
- Using springdoc 2.x on Spring Boot 4. The 2.x line targets Boot 3; Boot 4 needs springdoc 3.x.
- Documenting an error status with @ApiResponse but no content = @Content, so the docs show the success body under 404 or 400.
- Thinking @SecurityRequirement protects the endpoint. It only tells readers a token is needed; Spring Security does the actual checking.
Exercise
A teammate is about to make this mistake: Assuming springdoc-openapi requires manually writing a YAML/JSON spec file — it generates the spec automatically from your existing controllers and DTOs. Use What OpenAPI Is to review the decision. Write a corrected implementation or design sketch that follows springdoc-openapi generates an OpenAPI spec and Swagger UI automatically from your controllers and DTOs., then name one edge case you would check.
Show hints
- Start with What OpenAPI Is and compare your approach with Annotated controller, DTOs and API metadata (tested with springdoc 3.1.1 on Spring Boot 4.1.1). Identify the decision or behavior the lesson recommends.
- Walk through your scenario step by step. Confirm that it follows springdoc-openapi generates an OpenAPI spec and Swagger UI automatically from your controllers and DTOs. and that it would catch the failure described by Assuming springdoc-openapi requires manually writing a YAML/JSON spec file — it generates the spec automatically from your existing controllers and DTOs..
Quick quiz
1. Which choice best describes the recommended approach in API Documentation with OpenAPI (springdoc-openapi)?
- A. Assuming springdoc-openapi requires manually writing a YAML/JSON spec file — it generates the spec automatically from your existing controllers and DTOs.
- B. Leaving the Swagger UI publicly exposed in production without any access restriction, which reveals your full API surface to anyone.
- C. springdoc-openapi generates an OpenAPI spec and Swagger UI automatically from your controllers and DTOs.
Show answer
C. This is the recommended approach: springdoc-openapi generates an OpenAPI spec and Swagger UI automatically from your controllers and DTOs. Avoid: Assuming springdoc-openapi requires manually writing a YAML/JSON spec file — it generates the spec automatically from your existing controllers and DTOs.
Interview questions
Why does this principle matter in API Documentation with OpenAPI (springdoc-openapi): springdoc-openapi generates an OpenAPI spec and Swagger UI automatically from your controllers and DTOs.
Show answer
springdoc-openapi generates an OpenAPI spec and Swagger UI automatically from your controllers and DTOs. Apply this in What OpenAPI Is; the example Annotated controller, DTOs and API metadata (tested with springdoc 3.1.1 on Spring Boot 4.1.1) shows one way to do it. Avoid Assuming springdoc-openapi requires manually writing a YAML/JSON spec file — it generates the spec automatically from your existing controllers and DTOs. and explain how your implementation or design prevents that failure.
Key Points to Remember
- springdoc-openapi generates an OpenAPI spec and Swagger UI automatically from your controllers and DTOs.
- /v3/api-docs exposes the raw spec (/v3/api-docs.yaml for YAML); /swagger-ui.html opens the interactive browsable UI.
- Bean Validation annotations such as @NotBlank, @Size and @DecimalMin show up as required, maxLength and minimum in the schema.
- @Tag, @Operation, @ApiResponse, @Parameter and @Schema annotations enrich the generated documentation with human-readable detail; @OpenAPIDefinition sets the title and version.
- @SecurityScheme plus @SecurityRequirement adds the Authorize button and marks protected operations.
- Disable the docs in production with springdoc.api-docs.enabled=false and springdoc.swagger-ui.enabled=false, or protect them with Spring Security.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a JDK project with their libraries on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering