Abbreviations
| Abbreviation | Full name | Meaning here |
|---|---|---|
| API | Application Programming Interface | The HTTP endpoints your controllers expose |
| OAS | OpenAPI Specification | The vendor-neutral standard for describing HTTP APIs |
| REST | Representational State Transfer | The resource-oriented HTTP style most Spring controllers follow |
| HTTP | Hypertext Transfer Protocol | The protocol the API and the documentation are served over |
| JSON / YAML | JavaScript Object Notation / YAML Ain't Markup Language | The two formats an OpenAPI document is written in |
| UI | User Interface | Swagger UI, the browser page that renders the document |
| MVC | Model--View--Controller | Spring Web MVC, the servlet-based web stack |
| DTO | Data Transfer Object | Request/response classes such as CreateBookRequest |
| JWT | JSON Web Token | The bearer token format declared in the security scheme |
| CI | Continuous Integration | The build pipeline that can export and diff the spec |
| ISBN | International Standard Book Number | Field used in the sample model |
1. The short answer
Add one dependency --- springdoc-openapi-starter-webmvc-ui --- and a Spring Boot application gets:
/v3/api-docs--- an OpenAPI 3.1 JSON document generated from your code (controllers, DTOs, validation annotations) at runtime;/v3/api-docs.yaml--- the same document as YAML;/swagger-ui.html--- Swagger UI, an interactive page that renders the document and lets you send real requests.
Everything after that --- titles, descriptions, examples, error responses, security schemes, groups --- is optional polish added with annotations, one OpenAPI bean, and springdoc.* properties.
Everything in this article was built and run against Spring Boot 4.1.1 + springdoc-openapi 3.1.1 on Java 17; the JSON excerpts are real output.
2. Swagger, OpenAPI, springfox, springdoc --- sorting out the names
| Name | What it is |
|---|---|
| Swagger | The original API-description format (Swagger 2.0) and today a family of SmartBear tools: Swagger UI, Swagger Editor, Swagger Codegen. |
| OpenAPI | Swagger 2.0 donated to the Linux Foundation and renamed in 2016. OAS 3.0 (2017) and 3.1 (2021) are the current versions. The format is OpenAPI; the tools are still called Swagger. |
| Swagger UI | The HTML/JavaScript page that turns an OpenAPI document into browsable, clickable documentation. |
| springfox | The old Spring integration (@EnableSwagger2, Docket). Last release 3.0.0 in 2020; it does not work with Spring Boot 3+ (Jakarta EE namespace). Do not use it for new code. |
| springdoc-openapi | The maintained Spring integration. Reads Spring's own metadata, emits OpenAPI 3, bundles Swagger UI. This is what this article uses. |
3. How springdoc works
#mermaid-svg-JS50rEKJ3mgvUZId{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JS50rEKJ3mgvUZId .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JS50rEKJ3mgvUZId .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JS50rEKJ3mgvUZId .error-icon{fill:#552222;}#mermaid-svg-JS50rEKJ3mgvUZId .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JS50rEKJ3mgvUZId .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JS50rEKJ3mgvUZId .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JS50rEKJ3mgvUZId .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JS50rEKJ3mgvUZId .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JS50rEKJ3mgvUZId .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JS50rEKJ3mgvUZId .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JS50rEKJ3mgvUZId .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JS50rEKJ3mgvUZId .marker.cross{stroke:#333333;}#mermaid-svg-JS50rEKJ3mgvUZId svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JS50rEKJ3mgvUZId p{margin:0;}#mermaid-svg-JS50rEKJ3mgvUZId .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JS50rEKJ3mgvUZId .cluster-label text{fill:#333;}#mermaid-svg-JS50rEKJ3mgvUZId .cluster-label span{color:#333;}#mermaid-svg-JS50rEKJ3mgvUZId .cluster-label span p{background-color:transparent;}#mermaid-svg-JS50rEKJ3mgvUZId .label text,#mermaid-svg-JS50rEKJ3mgvUZId span{fill:#333;color:#333;}#mermaid-svg-JS50rEKJ3mgvUZId .node rect,#mermaid-svg-JS50rEKJ3mgvUZId .node circle,#mermaid-svg-JS50rEKJ3mgvUZId .node ellipse,#mermaid-svg-JS50rEKJ3mgvUZId .node polygon,#mermaid-svg-JS50rEKJ3mgvUZId .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JS50rEKJ3mgvUZId .rough-node .label text,#mermaid-svg-JS50rEKJ3mgvUZId .node .label text,#mermaid-svg-JS50rEKJ3mgvUZId .image-shape .label,#mermaid-svg-JS50rEKJ3mgvUZId .icon-shape .label{text-anchor:middle;}#mermaid-svg-JS50rEKJ3mgvUZId .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JS50rEKJ3mgvUZId .rough-node .label,#mermaid-svg-JS50rEKJ3mgvUZId .node .label,#mermaid-svg-JS50rEKJ3mgvUZId .image-shape .label,#mermaid-svg-JS50rEKJ3mgvUZId .icon-shape .label{text-align:center;}#mermaid-svg-JS50rEKJ3mgvUZId .node.clickable{cursor:pointer;}#mermaid-svg-JS50rEKJ3mgvUZId .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JS50rEKJ3mgvUZId .arrowheadPath{fill:#333333;}#mermaid-svg-JS50rEKJ3mgvUZId .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JS50rEKJ3mgvUZId .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JS50rEKJ3mgvUZId .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JS50rEKJ3mgvUZId .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JS50rEKJ3mgvUZId .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JS50rEKJ3mgvUZId .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JS50rEKJ3mgvUZId .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JS50rEKJ3mgvUZId .cluster text{fill:#333;}#mermaid-svg-JS50rEKJ3mgvUZId .cluster span{color:#333;}#mermaid-svg-JS50rEKJ3mgvUZId div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JS50rEKJ3mgvUZId .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JS50rEKJ3mgvUZId rect.text{fill:none;stroke-width:0;}#mermaid-svg-JS50rEKJ3mgvUZId .icon-shape,#mermaid-svg-JS50rEKJ3mgvUZId .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JS50rEKJ3mgvUZId .icon-shape p,#mermaid-svg-JS50rEKJ3mgvUZId .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JS50rEKJ3mgvUZId .icon-shape .label rect,#mermaid-svg-JS50rEKJ3mgvUZId .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JS50rEKJ3mgvUZId .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JS50rEKJ3mgvUZId .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JS50rEKJ3mgvUZId :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Spring Boot application
fetches
@RestController
mappings
DTOs + Bean Validation
@NotBlank, @Size...
swagger annotations
@Operation, @Schema
OpenAPI bean
- springdoc.* properties
springdoc
scanner
/v3/api-docs
(JSON / YAML)
/swagger-ui/index.html
(static assets)
Browser
Client generators,
Postman, gateways
This diagram answers: where does the OpenAPI document come from, and who consumes it?
springdoc asks Spring MVC for its RequestMappingHandlerMapping --- the same table Spring uses to route requests --- so paths, HTTP methods, parameters and return types come straight from your real mappings. It then inspects DTOs with the swagger-core model resolver (Jackson-aware) and folds in validation and documentation annotations. The document is built lazily on the first request and cached.
4. Step 1 --- add the dependency
Choose the artifact that matches your web stack and Spring Boot line:
| Spring Boot | springdoc line | Web MVC artifact | WebFlux artifact |
|---|---|---|---|
| 4.1.x | 3.1.x | springdoc-openapi-starter-webmvc-ui |
springdoc-openapi-starter-webflux-ui |
| 4.0.x | 3.0.x | same | same |
| 3.x | 2.8.x | same | same |
| 2.x (legacy) | 1.8.0 (final) | springdoc-openapi-ui |
springdoc-openapi-webflux-ui |
Versions checked on Maven Central on 2026-10-22. Each springdoc line is built against one Spring Boot generation --- mixing them (for example springdoc 2.x on Boot 4) fails at startup.
The -ui starters include Swagger UI; the -api variants (springdoc-openapi-starter-webmvc-api) produce only /v3/api-docs, for services where the UI is hosted elsewhere.
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>3.1.1</version>
</dependency>
</dependencies>
(Spring Boot 4 renamed spring-boot-starter-web to spring-boot-starter-webmvc; the old name still works as a deprecated alias. On Boot 3, keep spring-boot-starter-web.)
Gradle:
kotlin
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:3.1.1")
Start the application. With zero further configuration, these work:
| URL | Returns |
|---|---|
/v3/api-docs |
OpenAPI document (JSON) |
/v3/api-docs.yaml |
OpenAPI document (YAML) |
/v3/api-docs/{group} |
One group's document (see §8) |
/swagger-ui.html |
302 redirect to /swagger-ui/index.html |
/v3/api-docs/swagger-config |
Settings that Swagger UI loads on start-up |
5. Step 2 --- describe the API as a whole
One OpenAPI bean sets document-level metadata: title, version, description, license, and security schemes.
java
@Configuration
public class OpenApiConfig {
@Bean
OpenAPI booksOpenApi() {
return new OpenAPI()
.info(new Info()
.title("Books API")
.version("v1")
.description("Catalogue service for the book store")
.license(new License().name("Apache 2.0")))
.components(new Components().addSecuritySchemes("bearerAuth",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")))
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"));
}
}
The security scheme puts an Authorize button in Swagger UI; a token pasted there is sent as Authorization: Bearer ... on every "Try it out" request. addSecurityItem applies it to all operations; to apply it to selected endpoints only, drop that line and annotate those methods with @SecurityRequirement(name = "bearerAuth").
The same metadata can also be declared with @OpenAPIDefinition and @SecurityScheme annotations on any @Configuration class; the bean form is easier when values come from configuration.
6. Step 3 --- document controllers and models
springdoc already knows the paths and types. Annotations add the human parts: summaries, descriptions, examples and non-200 responses.
java
@RestController
@RequestMapping("/api/books")
@Tag(name = "Books", description = "Manage the book catalogue")
public class BookController {
@GetMapping("/{id}")
@Operation(summary = "Get a book by id")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Book found"),
@ApiResponse(responseCode = "404", description = "No book with that id",
content = @Content(schema = @Schema(implementation = ProblemDetail.class)))
})
public ResponseEntity<Book> get(@Parameter(description = "Book id", example = "42")
@PathVariable Long id) { ... }
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "Create a book")
@ApiResponse(responseCode = "201", description = "Book created")
@ApiResponse(responseCode = "400", description = "Validation failed",
content = @Content(schema = @Schema(implementation = ProblemDetail.class)))
public Book create(@Valid @RequestBody CreateBookRequest request) { ... }
}
java
@Schema(description = "A book in the catalogue")
public record Book(
@Schema(description = "Server-assigned id", example = "42",
accessMode = Schema.AccessMode.READ_ONLY)
Long id,
@Schema(description = "Book title", example = "Effective Java")
String title,
@Schema(description = "13-digit ISBN", example = "9780134685991")
String isbn) {
}
@Schema(description = "Payload for creating a book")
public record CreateBookRequest(
@NotBlank @Size(max = 200) @Schema(example = "Effective Java")
String title,
@NotBlank @Pattern(regexp = "\\d{13}") @Schema(example = "9780134685991")
String isbn) {
}
The annotations come from io.swagger.v3.oas.annotations.* (swagger-core), pulled in by the starter.
| Annotation | Placed on | Adds |
|---|---|---|
@Tag |
Controller | Groups operations under a heading in Swagger UI |
@Operation |
Handler method | summary, description, operationId, hidden, deprecated |
@Parameter |
Method parameter | Description, example, required for path/query/header parameters |
@ApiResponse(s) |
Handler method | Additional status codes and their body schemas |
@Schema |
DTO class or field | Description, example, accessMode, allowableValues, implementation |
@Hidden |
Controller or method | Leaves it out of the document |
Bean Validation flows into the schema
This is the real generated schema for CreateBookRequest:
json
"CreateBookRequest": {
"type": "object",
"description": "Payload for creating a book",
"properties": {
"title": { "type": "string", "example": "Effective Java", "maxLength": 200, "minLength": 0 },
"isbn": { "type": "string", "example": "9780134685991", "minLength": 1, "pattern": "\\d{13}" }
},
"required": ["isbn", "title"]
}
@NotBlank/@NotNull→ the field is listed inrequired;@Size(max = 200)→maxLength: 200;@Pattern→pattern;@Min/@Max→minimum/maximum.
Watch the title field: @NotBlank alone produces minLength: 1 (as on isbn), but @Size(max = 200) contributes its default min = 0, which wins. If the minimum matters to clients, write @Size(min = 1, max = 200).
Likewise, accessMode = READ_ONLY becomes "readOnly": true on Book.id, telling client generators not to send it on create.
7. Step 4 --- document errors honestly
A spec that only shows the happy path misleads its clients. Spring Framework's ProblemDetail (RFC 9457 "Problem Details for HTTP APIs") gives every error the same shape, and the @ApiResponse(... implementation = ProblemDetail.class) lines above advertise it.
Make sure the runtime matches the documentation --- turn on Spring Boot's built-in problem-details handling:
yaml
spring:
mvc:
problemdetails:
enabled: true
With it, a failed validation returns (verified):
json
{"detail":"Invalid request content.","instance":"/api/books","status":400,"title":"Bad Request"}
Without it, Boot's default error body (timestamp, status, error, path) is returned, which does not match a ProblemDetail schema. A @RestControllerAdvice that extends ResponseEntityExceptionHandler achieves the same and lets you add field-level errors.
8. Step 5 --- group large APIs
One service often exposes public endpoints, admin endpoints and internal callbacks. GroupedOpenApi splits them into separate documents, chosen from a drop-down in Swagger UI:
java
@Bean
GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("public")
.pathsToMatch("/api/**")
.build();
}
@Bean
GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/admin/**")
.build();
}
Each group is served at /v3/api-docs/{group}. Groups can also filter by packagesToScan, pathsToExclude, or a custom OpenApiCustomizer. Once any group bean exists, Swagger UI offers only the groups in its drop-down; /v3/api-docs itself still serves the full, ungrouped document.
9. Useful springdoc.* properties
yaml
springdoc:
api-docs:
path: /v3/api-docs # where the JSON is served
swagger-ui:
path: /swagger-ui.html # entry URL of the UI
operations-sorter: method # sort operations by HTTP method
tags-sorter: alpha # sort tags alphabetically
try-it-out-enabled: true # open the "Try it out" panels by default
packages-to-scan: com.example.books
default-produces-media-type: application/json
| Property | Effect |
|---|---|
springdoc.api-docs.enabled |
false removes /v3/api-docs (returns 404) |
springdoc.swagger-ui.enabled |
false removes Swagger UI |
springdoc.packages-to-scan / paths-to-match |
Limit what the default document includes |
springdoc.show-actuator |
Include Actuator endpoints in the document |
springdoc.api-docs.version |
openapi_3_1 (default in springdoc 2.8+/3.x) or openapi_3_0 for tools that cannot read 3.1 yet |
10. Spring Security
If Spring Security is on the classpath, the documentation endpoints are locked like everything else. Open them explicitly (in non-production profiles):
java
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.anyRequest().authenticated());
// ... JWT resource server, etc.
return http.build();
}
All three patterns are needed: the HTML entry point, the static assets under /swagger-ui/, and the JSON the page fetches (including /v3/api-docs/swagger-config).
11. Production concerns
-
Decide whether the docs should exist in production. For an internal service, a full catalogue of endpoints and models is free reconnaissance for an attacker. Common choice: enable in
dev/test, disable inprod:yaml# application-prod.yml springdoc: api-docs: enabled: false swagger-ui: enabled: falseFor a public API, keep the spec but publish it deliberately (developer portal, gateway), not by accident.
-
Behind a reverse proxy or load balancer , the generated
serversentry ("url": "http://localhost:18082", "description": "Generated server url"in the test run) is derived from the incoming request, so it may show the internal host orhttp. Setserver.forward-headers-strategy=frameworksoX-Forwarded-*headers are honoured, or declareserversexplicitly in theOpenAPIbean. -
Start-up cost is small --- the document is built on first request --- but very large APIs can take noticeable time on that first call; groups help.
12. Code-first, design-first, and the spec as a build artefact
springdoc is code-first : the code is the source of truth and the spec is derived. The alternative is design-first : write openapi.yaml by hand, review it, and generate Spring interfaces from it with openapi-generator (spring generator). Either way, treat the spec as a versioned artefact:
#mermaid-svg-0NXm9hPJtzxjvR2X{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0NXm9hPJtzxjvR2X .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0NXm9hPJtzxjvR2X .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0NXm9hPJtzxjvR2X .error-icon{fill:#552222;}#mermaid-svg-0NXm9hPJtzxjvR2X .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0NXm9hPJtzxjvR2X .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0NXm9hPJtzxjvR2X .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0NXm9hPJtzxjvR2X .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0NXm9hPJtzxjvR2X .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0NXm9hPJtzxjvR2X .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0NXm9hPJtzxjvR2X .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0NXm9hPJtzxjvR2X .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0NXm9hPJtzxjvR2X .marker.cross{stroke:#333333;}#mermaid-svg-0NXm9hPJtzxjvR2X svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0NXm9hPJtzxjvR2X p{margin:0;}#mermaid-svg-0NXm9hPJtzxjvR2X .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0NXm9hPJtzxjvR2X .cluster-label text{fill:#333;}#mermaid-svg-0NXm9hPJtzxjvR2X .cluster-label span{color:#333;}#mermaid-svg-0NXm9hPJtzxjvR2X .cluster-label span p{background-color:transparent;}#mermaid-svg-0NXm9hPJtzxjvR2X .label text,#mermaid-svg-0NXm9hPJtzxjvR2X span{fill:#333;color:#333;}#mermaid-svg-0NXm9hPJtzxjvR2X .node rect,#mermaid-svg-0NXm9hPJtzxjvR2X .node circle,#mermaid-svg-0NXm9hPJtzxjvR2X .node ellipse,#mermaid-svg-0NXm9hPJtzxjvR2X .node polygon,#mermaid-svg-0NXm9hPJtzxjvR2X .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0NXm9hPJtzxjvR2X .rough-node .label text,#mermaid-svg-0NXm9hPJtzxjvR2X .node .label text,#mermaid-svg-0NXm9hPJtzxjvR2X .image-shape .label,#mermaid-svg-0NXm9hPJtzxjvR2X .icon-shape .label{text-anchor:middle;}#mermaid-svg-0NXm9hPJtzxjvR2X .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0NXm9hPJtzxjvR2X .rough-node .label,#mermaid-svg-0NXm9hPJtzxjvR2X .node .label,#mermaid-svg-0NXm9hPJtzxjvR2X .image-shape .label,#mermaid-svg-0NXm9hPJtzxjvR2X .icon-shape .label{text-align:center;}#mermaid-svg-0NXm9hPJtzxjvR2X .node.clickable{cursor:pointer;}#mermaid-svg-0NXm9hPJtzxjvR2X .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0NXm9hPJtzxjvR2X .arrowheadPath{fill:#333333;}#mermaid-svg-0NXm9hPJtzxjvR2X .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0NXm9hPJtzxjvR2X .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0NXm9hPJtzxjvR2X .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0NXm9hPJtzxjvR2X .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0NXm9hPJtzxjvR2X .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0NXm9hPJtzxjvR2X .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0NXm9hPJtzxjvR2X .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0NXm9hPJtzxjvR2X .cluster text{fill:#333;}#mermaid-svg-0NXm9hPJtzxjvR2X .cluster span{color:#333;}#mermaid-svg-0NXm9hPJtzxjvR2X div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0NXm9hPJtzxjvR2X .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0NXm9hPJtzxjvR2X rect.text{fill:none;stroke-width:0;}#mermaid-svg-0NXm9hPJtzxjvR2X .icon-shape,#mermaid-svg-0NXm9hPJtzxjvR2X .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0NXm9hPJtzxjvR2X .icon-shape p,#mermaid-svg-0NXm9hPJtzxjvR2X .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0NXm9hPJtzxjvR2X .icon-shape .label rect,#mermaid-svg-0NXm9hPJtzxjvR2X .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0NXm9hPJtzxjvR2X .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0NXm9hPJtzxjvR2X .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0NXm9hPJtzxjvR2X :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} mvn verify
(springdoc-openapi-maven-plugin)
Controllers + DTOs
openapi.json
in build output
CI: diff against
last release
openapi-generator
TS / Kotlin / Swift clients
API portal /
gateway import
This diagram answers: once springdoc produces a spec, how does it feed the rest of the delivery pipeline?
springdoc-openapi-maven-plugin (1.5) starts the app during integration-test, downloads /v3/api-docs, and writes it to target/openapi.json; it needs spring-boot-maven-plugin's start/stop goals around it. A CI (Continuous Integration) job that diffs this file against the previous release (for example with openapi-diff) catches breaking changes --- a renamed field, a newly required parameter --- before clients do. (The plugin set-up is shown for orientation; it was not run for this article.)
13. Migrating from springfox
| springfox | springdoc / swagger-core |
|---|---|
@EnableSwagger2, Docket bean |
Nothing to enable; GroupedOpenApi bean for groups |
@Api |
@Tag |
@ApiOperation(value = "...", notes = "...") |
@Operation(summary = "...", description = "...") |
@ApiParam |
@Parameter |
@ApiModel / @ApiModelProperty |
@Schema |
@ApiResponse(code = 404, message = "...") |
@ApiResponse(responseCode = "404", description = "...") |
@ApiIgnore |
@Hidden (or @Parameter(hidden = true)) |
/v2/api-docs, /swagger-ui/ |
/v3/api-docs, /swagger-ui.html |
Remove every io.springfox dependency first --- having both on the classpath causes confusing start-up failures.
14. Common mistakes
| Symptom | Cause | Fix |
|---|---|---|
App fails at start with NoSuchMethodError / ClassNotFoundException |
springdoc line does not match the Spring Boot generation | Use the version table in §4 |
/swagger-ui.html returns 401/403 |
Spring Security blocks the UI or its JSON | Permit all three patterns in §10 |
| UI loads but shows "Failed to load API definition" | JSON path blocked, or a proxy rewrites paths | Check /v3/api-docs/swagger-config and /v3/api-docs directly |
Schema shows minLength: 0 on a @NotBlank field |
@Size without min |
@Size(min = 1, max = ...) |
| Documented 400 body doesn't match reality | Default Boot error JSON, not ProblemDetail |
spring.mvc.problemdetails.enabled=true or a ResponseEntityExceptionHandler |
Request DTO shows server-set fields like id |
One record used for both request and response | Separate request/response DTOs, or accessMode = READ_ONLY |
| Code generator rejects the spec | Tool only supports OAS 3.0 | springdoc.api-docs.version=openapi_3_0 |
15. Conclusion
- OpenAPI is the format; Swagger UI is the viewer; springdoc-openapi is the Spring integration. springfox is obsolete.
- One starter dependency gives a live OpenAPI 3.1 document at
/v3/api-docsand Swagger UI at/swagger-ui.html, derived from your actual mappings, types and validation rules. - Add value deliberately: an
OpenAPIbean for metadata and security,@Operation/@ApiResponsefor intent and error cases,@Schemafor examples,GroupedOpenApifor large services --- and make the runtime error format match what you document. - Treat the spec as a product: lock it down or disable it in production, export it in CI, diff it between releases, and generate clients from it.
References
- springdoc-openapi documentation: https://springdoc.org/
- springdoc-openapi on GitHub: https://github.com/springdoc/springdoc-openapi
- OpenAPI Specification 3.1: https://spec.openapis.org/oas/v3.1.0
- swagger-core annotations: https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Annotations
- RFC 9457, Problem Details for HTTP APIs: https://www.rfc-editor.org/rfc/rfc9457
- Related: Spring Boot Startup Lifecycle