| name | spring-boot-4x-api-documenter |
| description | Auto-generate OpenAPI 3 documentation for Spring Boot 4.x REST APIs using springdoc-openapi v3.0. Use when API endpoints change, controllers are modified, or user mentions API docs, Swagger, or OpenAPI. Triggers on @RestController changes, documentation requests, endpoint additions. Requires Spring Boot 4.0+ and Java 17+. |
| allowed-tools | Read, Write, Grep, Edit |
Spring Boot 4.x API Documenter Skill
Auto-generate OpenAPI 3 documentation for Spring Boot 4.x REST APIs using springdoc-openapi v3.0.0.
Note: This skill is for Spring Boot 4.x projects. For Spring Boot 3.x, use the
api-documenter-sb3xskill with springdoc-openapi v2.8.x.
When I Activate
- API endpoints added/modified in Spring Boot 4.x
- User mentions API docs, OpenAPI, Swagger, or springdoc
- @RestController files changed
- Request/response DTOs modified
- Documentation configuration needed
- Migration from springdoc-openapi v2.x to v3.0
- API versioning configuration (Spring Framework 7 feature)
What I Generate
OpenAPI 3.0/3.1 Specifications
- Endpoint descriptions from annotations and Javadoc
- Request/response schemas
- Authentication/security requirements
- Example payloads
- Error responses via @ControllerAdvice
- Grouped API definitions
Spring Boot 4.x Integration
- springdoc-openapi v3.0 dependency configuration
- Swagger UI (v5.30.1) and Scalar UI (v0.4.3) setup
- application.properties/yml configuration
- Security scheme definitions
- GroupedOpenApi bean configurations
- Initial API versioning support (Spring Framework 7)
Quick Start
Requirements
- Spring Boot: 4.0.0 or higher
- Java: 17 or higher (Jakarta EE 9)
- Spring Framework: 7.x
Adding springdoc-openapi to Spring Boot 4.x
Maven (WebMvc with Swagger UI):
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>3.0.0</version>
</dependency>
Maven (WebFlux with Swagger UI):
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>3.0.0</version>
</dependency>
Gradle (WebMvc with Swagger UI):
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.0'
Gradle (WebFlux with Swagger UI):
implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:3.0.0'
Default URLs After Setup
- Swagger UI:
http://localhost:8080/swagger-ui.html - OpenAPI JSON:
http://localhost:8080/v3/api-docs - OpenAPI YAML:
http://localhost:8080/v3/api-docs.yaml
Examples
Basic REST Controller Documentation
// You write:
@RestController
@RequestMapping("/api/users")
@Tag(name = "User Management", description = "APIs for managing users")
public class UserController {
/**
* Get user by ID
* @param id User's unique identifier
* @return User object if found
*/
@GetMapping("/{id}")
@Operation(summary = "Get user by ID", description = "Returns a single user")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "User found",
content = @Content(schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "404", description = "User not found")
})
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
/**
* Create new user
* @param user User data to create
* @return Created user with ID
*/
@PostMapping
@Operation(summary = "Create user", description = "Creates a new user")
@ApiResponse(responseCode = "201", description = "User created successfully")
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User created = userService.save(user);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
}
// I auto-generate OpenAPI spec with full documentation
DTO Schema Documentation
@Schema(description = "User entity representing a system user")
public class User {
@Schema(description = "Unique identifier", example = "12345", accessMode = Schema.AccessMode.READ_ONLY)
private Long id;
@Schema(description = "User's full name", example = "John Doe", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank
private String name;
@Schema(description = "User's email address", example = "john.doe@example.com", format = "email")
@Email
private String email;
@Schema(description = "Account creation timestamp", example = "2025-01-15T10:30:00Z")
private Instant createdAt;
@Schema(description = "User's role in the system", example = "ADMIN")
private UserRole role;
}
@Schema(enumAsRef = true, description = "User roles in the system")
public enum UserRole {
ADMIN, USER, READONLY
}
Multiple API Groups
@Configuration
public class OpenApiConfig {
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("public-api")
.displayName("Public API")
.pathsToMatch("/api/public/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin-api")
.displayName("Admin API")
.pathsToMatch("/api/admin/**")
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(AdminOnly.class))
.build();
}
@Bean
public GroupedOpenApi storeApi() {
String[] paths = {"/store/**"};
return GroupedOpenApi.builder()
.group("stores")
.pathsToMatch(paths)
.build();
}
}
Security Scheme Configuration
@Configuration
@OpenAPIDefinition(
info = @Info(
title = "My Application API",
version = "1.0.0",
description = "REST API documentation",
contact = @Contact(name = "API Support", email = "support@example.com"),
license = @License(name = "Apache 2.0", url = "https://www.apache.org/licenses/LICENSE-2.0")
),
servers = {
@Server(url = "https://api.example.com", description = "Production"),
@Server(url = "http://localhost:8080", description = "Development")
}
)
@SecurityScheme(
name = "bearerAuth",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT",
description = "JWT Authentication"
)
@SecurityScheme(
name = "apiKey",
type = SecuritySchemeType.APIKEY,
in = SecuritySchemeIn.HEADER,
paramName = "X-API-Key",
description = "API Key Authentication"
)
public class OpenApiSecurityConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("basicAuth",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("basic")));
}
}
Applying Security to Endpoints
@RestController
@RequestMapping("/api/secure")
@SecurityRequirement(name = "bearerAuth")
public class SecureController {
@GetMapping("/data")
@Operation(
summary = "Get secure data",
security = { @SecurityRequirement(name = "bearerAuth") }
)
public ResponseEntity<SecureData> getSecureData() {
return ResponseEntity.ok(secureService.getData());
}
@GetMapping("/admin")
@Operation(
summary = "Admin only endpoint",
security = {
@SecurityRequirement(name = "bearerAuth"),
@SecurityRequirement(name = "apiKey")
}
)
public ResponseEntity<AdminData> getAdminData() {
return ResponseEntity.ok(adminService.getData());
}
}
Functional Endpoints (WebMvc.fn/WebFlux)
@Configuration
public class RouterConfig {
@Bean
@RouterOperation(
beanClass = EmployeeService.class,
beanMethod = "findAllEmployees",
operation = @Operation(
operationId = "getAllEmployees",
summary = "Get all employees",
tags = {"employees"},
responses = {
@ApiResponse(responseCode = "200",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = Employee.class))))
}
)
)
RouterFunction<ServerResponse> getAllEmployeesRoute(EmployeeHandler handler) {
return route(GET("/employees").and(accept(MediaType.APPLICATION_JSON)), handler::findAll);
}
@Bean
@RouterOperations({
@RouterOperation(path = "/persons", method = RequestMethod.GET,
beanClass = PersonService.class, beanMethod = "getAll"),
@RouterOperation(path = "/persons/{id}", method = RequestMethod.GET,
beanClass = PersonService.class, beanMethod = "getById"),
@RouterOperation(path = "/persons", method = RequestMethod.POST,
beanClass = PersonService.class, beanMethod = "save")
})
RouterFunction<ServerResponse> personRoutes(PersonHandler handler) {
return RouterFunctions
.route(GET("/persons").and(accept(APPLICATION_JSON)), handler::findAll)
.andRoute(GET("/persons/{id}").and(accept(APPLICATION_JSON)), handler::findById)
.andRoute(POST("/persons").and(accept(APPLICATION_JSON)), handler::save);
}
}
Error Handling with @ControllerAdvice
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
@ApiResponse(responseCode = "404", description = "Entity not found",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public ErrorResponse handleNotFound(EntityNotFoundException ex) {
return new ErrorResponse("NOT_FOUND", ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ApiResponse(responseCode = "400", description = "Validation failed",
content = @Content(schema = @Schema(implementation = ValidationErrorResponse.class)))
public ValidationErrorResponse handleValidation(MethodArgumentNotValidException ex) {
List<FieldError> errors = ex.getBindingResult().getFieldErrors().stream()
.map(e -> new FieldError(e.getField(), e.getDefaultMessage()))
.collect(Collectors.toList());
return new ValidationErrorResponse("VALIDATION_FAILED", errors);
}
}
@Schema(description = "Error response structure")
public class ErrorResponse {
@Schema(description = "Error code", example = "NOT_FOUND")
private String code;
@Schema(description = "Error message", example = "User with ID 123 not found")
private String message;
}
Pageable Support
@GetMapping("/users")
@Operation(summary = "Get paginated users")
public Page<User> getUsers(
@ParameterObject Pageable pageable,
@Parameter(description = "Filter by name") @RequestParam(required = false) String name) {
return userService.findAll(name, pageable);
}
// Pageable parameters are automatically documented with:
// - page (default: 0)
// - size (default: 20)
// - sort (e.g., "name,asc")
File Upload Documentation
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "Upload a file")
public ResponseEntity<FileInfo> uploadFile(
@Parameter(description = "File to upload", required = true)
@RequestPart("file") MultipartFile file,
@Parameter(description = "File description")
@RequestParam(required = false) String description) {
FileInfo info = fileService.store(file, description);
return ResponseEntity.ok(info);
}
Configuration Reference
application.properties
# Swagger UI path customization
springdoc.swagger-ui.path=/swagger-ui.html
# OpenAPI docs path
springdoc.api-docs.path=/v3/api-docs
# Enable/disable endpoints
springdoc.api-docs.enabled=true
springdoc.swagger-ui.enabled=true
# Filter by packages
springdoc.packagesToScan=com.example.api.controller
# Filter by paths
springdoc.pathsToMatch=/api/**, /v1/**
springdoc.pathsToExclude=/internal/**
# Show actuator endpoints
springdoc.show-actuator=true
# Swagger UI customization
springdoc.swagger-ui.operationsSorter=alpha
springdoc.swagger-ui.tagsSorter=alpha
springdoc.swagger-ui.tryItOutEnabled=true
springdoc.swagger-ui.filter=true
springdoc.swagger-ui.docExpansion=none
springdoc.swagger-ui.defaultModelsExpandDepth=-1
springdoc.swagger-ui.displayRequestDuration=true
# CSRF support
springdoc.swagger-ui.csrf.enabled=true
# Disable petstore URL
springdoc.swagger-ui.disable-swagger-default-url=true
# OpenAPI version
springdoc.api-docs.version=openapi_3_1
# Pretty print output
springdoc.writer-with-default-pretty-printer=true
# Cache control
springdoc.cache.disabled=false
application.yml
springdoc:
api-docs:
path: /v3/api-docs
enabled: true
version: openapi_3_1
swagger-ui:
path: /swagger-ui.html
enabled: true
operationsSorter: alpha
tagsSorter: alpha
tryItOutEnabled: true
filter: true
docExpansion: none
displayRequestDuration: true
csrf:
enabled: true
disable-swagger-default-url: true
packages-to-scan: com.example.api.controller
paths-to-match: /api/**, /v1/**
show-actuator: true
cache:
disabled: false
writer-with-default-pretty-printer: true
# Group configurations via YAML
group-configs:
- group: public-api
paths-to-match: /api/public/**
- group: admin-api
paths-to-match: /api/admin/**
packages-to-scan: com.example.api.admin
Actuator Integration
# Expose on actuator port (different from app port)
springdoc.use-management-port=true
management.server.port=9090
management.endpoints.web.exposure.include=openapi, swagger-ui
# URLs become:
# http://localhost:9090/actuator/openapi
# http://localhost:9090/actuator/swagger-ui
Module Reference
Available Modules (v3.0.0)
| Module | Use Case | Bundled UI Version |
|---|---|---|
springdoc-openapi-starter-webmvc-ui |
WebMvc + Swagger UI | Swagger UI v5.30.1 |
springdoc-openapi-starter-webmvc-api |
WebMvc without UI | - |
springdoc-openapi-starter-webflux-ui |
WebFlux + Swagger UI | Swagger UI v5.30.1 |
springdoc-openapi-starter-webflux-api |
WebFlux without UI | - |
springdoc-openapi-starter-webmvc-scalar |
WebMvc + Scalar UI | Scalar v0.4.3 |
springdoc-openapi-starter-webflux-scalar |
WebFlux + Scalar UI | Scalar v0.4.3 |
Integrated Support (No Extra Dependencies)
- Spring Security (
@AuthenticationPrincipalhidden) - Spring HATEOAS (HAL links)
- Spring Data REST (
@RepositoryRestResource) - Spring Cloud Function Web
- Kotlin types
- Groovy types
- Javadoc comments (with therapi-runtime-javadoc)
Javadoc Support Setup
Maven:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.github.therapi</groupId>
<artifactId>therapi-runtime-javadoc-scribe</artifactId>
<version>0.15.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
<dependency>
<groupId>com.github.therapi</groupId>
<artifactId>therapi-runtime-javadoc</artifactId>
<version>0.15.0</version>
</dependency>
Build-Time Generation
Maven Plugin
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>-Dspring.application.admin.enabled=true</jvmArguments>
</configuration>
<executions>
<execution>
<goals>
<goal>start</goal>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-maven-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>integration-test</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
Run: mvn verify
Gradle Plugin
plugins {
id("org.springframework.boot") version "4.0.0"
id("org.springdoc.openapi-gradle-plugin") version "1.9.0"
}
Run: gradle clean generateOpenApiDocs
Migration Guides
From springdoc-openapi v2.x to v3.0 (Spring Boot 3.x to 4.x)
Prerequisites:
- Upgrade to Spring Boot 4.0.0+
- Upgrade to Spring Framework 7.x
- Ensure Java 17+ (Jakarta EE 9)
Step 1: Update dependencies
| v2.x Module | v3.0 Module |
|---|---|
springdoc-openapi-starter-webmvc-ui:2.8.x |
springdoc-openapi-starter-webmvc-ui:3.0.0 |
springdoc-openapi-starter-webflux-ui:2.8.x |
springdoc-openapi-starter-webflux-ui:3.0.0 |
springdoc-openapi-starter-webmvc-api:2.8.x |
springdoc-openapi-starter-webmvc-api:3.0.0 |
springdoc-openapi-starter-webflux-api:2.8.x |
springdoc-openapi-starter-webflux-api:3.0.0 |
Step 2: No breaking changes in configuration
- All existing
springdoc.*properties remain compatible - Existing
GroupedOpenApibeans continue to work - OpenAPI annotations unchanged
Step 3: New features available
- Initial API versioning support (Spring Framework 7)
- WebFlux static resources support
- Updated Swagger UI (v5.30.1) and Scalar (v0.4.3)
Known Issues:
- API versioning with
springdoc.api-docs.versionmay cause HTTP 400 errors (issue #3163) - Test thoroughly before production deployment
From SpringFox
- Remove SpringFox dependencies
- Add
springdoc-openapi-starter-webmvc-ui:3.0.0 - Replace annotations:
| SpringFox (Swagger 2) | springdoc (Swagger 3) |
|---|---|
@Api |
@Tag |
@ApiIgnore |
@Parameter(hidden = true) or @Hidden |
@ApiImplicitParam |
@Parameter |
@ApiModel |
@Schema |
@ApiModelProperty |
@Schema |
@ApiOperation(value, notes) |
@Operation(summary, description) |
@ApiParam |
@Parameter |
@ApiResponse(code, message) |
@ApiResponse(responseCode, description) |
- Replace Docket beans with GroupedOpenApi beans
From springdoc-openapi v1 (Legacy)
Replace modules:
| v1 Module | v3 Module |
|---|---|
springdoc-openapi-ui |
springdoc-openapi-starter-webmvc-ui:3.0.0 |
springdoc-openapi-webflux-ui |
springdoc-openapi-starter-webflux-ui:3.0.0 |
springdoc-openapi-webmvc-core |
springdoc-openapi-starter-webmvc-api:3.0.0 |
springdoc-openapi-common |
springdoc-openapi-starter-common:3.0.0 |
Update class imports:
| v1 Class | v2/v3 Class |
|---|---|
org.springdoc.core.GroupedOpenApi |
org.springdoc.core.models.GroupedOpenApi |
org.springdoc.core.SpringDocUtils |
org.springdoc.core.utils.SpringDocUtils |
org.springdoc.api.annotations.ParameterObject |
org.springdoc.core.annotations.ParameterObject |
Compatibility Matrix
| Spring Boot | Spring Framework | springdoc-openapi | Java |
|---|---|---|---|
| 4.0.x | 7.x | 3.0.x | 17+ |
| 3.5.x | 6.2.x | 2.8.x | 17+ |
| 3.4.x | 6.1.x | 2.7.x - 2.8.x | 17+ |
| 3.3.x | 6.0.x | 2.6.x | 17+ |
| 3.2.x | 6.0.x | 2.3.x - 2.5.x | 17+ |
| 3.1.x | 6.0.x | 2.2.x | 17+ |
| 3.0.x | 6.0.x | 2.0.x - 2.1.x | 17+ |
Important: springdoc-openapi v3.0.x requires Spring Boot 4.0+. For Spring Boot 3.x projects, use springdoc-openapi v2.8.x.
Common Issues & Solutions
Parameters not showing in OpenAPI spec
Add -parameters to Maven compiler:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
Welcome page broken after adding springdoc
Set custom swagger-ui path:
springdoc.swagger-ui.path=/swagger-ui/api-docs.html
Reverse proxy issues
server.forward-headers-strategy=framework
@Bean
ForwardedHeaderFilter forwardedHeaderFilter() {
return new ForwardedHeaderFilter();
}
@Controller not showing
Use @RestController or add configuration:
static {
SpringDocUtils.getConfig().addRestControllers(MyController.class);
}
Best Practices
- Use OpenAPI annotations - Enhance auto-generated docs with
@Operation,@Schema,@Parameter - Document DTOs - Add
@Schemato all request/response classes - Group APIs logically - Use
GroupedOpenApifor large applications - Secure documentation - Protect Swagger UI in production
- Use Javadoc - Enable therapi-runtime-javadoc for automatic description extraction
- Version your API - Include version in paths and document changes
- Provide examples - Use
exampleattribute in@Schemaannotations - Document errors - Use
@ControllerAdvicewith@ResponseStatus
Integration Points
With API Testing Tools
- Postman: Import OpenAPI spec directly
- Insomnia: Import from
/v3/api-docs - Bruno: Import OpenAPI 3 specs
With Documentation Sites
- Docusaurus: Use OpenAPI plugin
- MkDocs: Use swagger-markdown plugin
- Redoc: Render OpenAPI specs beautifully
- Stoplight: Full API design platform
With Code Generation
- OpenAPI Generator: Generate client SDKs
- Swagger Codegen: Generate servers/clients
Relationship with Other Skills
This Skill: Auto-configure springdoc-openapi for Spring Boot REST APIs @docs-writer Sub-Agent: Create comprehensive user guides and tutorials readme-updater Skill: Keep README in sync with API changes
Workflow
- I configure springdoc-openapi and generate OpenAPI spec
- Need user guide? Invoke @docs-writer sub-agent
- Sub-agent creates complete API documentation site
Sandboxing Compatibility
Works without sandboxing: Yes Works with sandboxing: Yes
- Filesystem: Writes configuration files
- Network: None required (build-time generation)
- Configuration: application.properties/yml changes
References
- springdoc-openapi GitHub
- springdoc-openapi v3.0.0 Release
- springdoc-openapi Demos
- springdoc.org Documentation
- OpenAPI 3.1 Specification
- Swagger UI Configuration
- Spring Boot 4.0 Documentation
- Spring Framework 7 Documentation
This skill uses springdoc-openapi v3.0.0 documentation as its knowledge base for Spring Boot 4.x API documentation generation.