
Spring Boot Openapi Documentation
- 1.8k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
spring-boot-openapi-documentation configures SpringDoc OpenAPI 3.0 and Swagger UI for Spring Boot REST APIs.
About
The spring-boot-openapi-documentation skill teaches agents to generate comprehensive REST API documentation with SpringDoc OpenAPI 3.0 and Swagger UI for Spring Boot 3.x WebMvc or WebFlux apps. Workflow covers adding springdoc-openapi-starter dependencies, configuring application.yml paths for api-docs and swagger-ui, annotating controllers with Tag, Operation, ApiResponse, Parameter, Schema, and SecurityRequirement, and documenting models with validation metadata. It supports JWT, OAuth2, and Basic Auth security schemes, pageable endpoints via ParameterObject, programmatic OpenAPI bean customization, multiple API groups, error response documentation, and Kotlin APIs. Reference files split dependency setup, configuration, controller patterns, model schemas, security, pagination, and advanced customization. Agents should load only the references needed per task rather than every file at once.
- Documents SpringDoc starters for WebMvc and WebFlux with Swagger UI access paths.
- Covers Tag, Operation, ApiResponse, Schema, and SecurityRequirement annotations.
- Explains JWT, OAuth2, and Basic Auth security scheme configuration.
- Includes pagination docs with ParameterObject and Spring Data Pageable.
- Splits deep guidance across dependency, controller, model, and security references.
Spring Boot Openapi Documentation by the numbers
- 1,812 all-time installs (skills.sh)
- +59 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #182 of 1,901 Documentation skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
spring-boot-openapi-documentation capabilities & compatibility
- Capabilities
- springdoc setup · controller annotations · security schemes · pagination docs
- Use cases
- api development · documentation
What spring-boot-openapi-documentation says it does
SpringDoc OpenAPI automates generation of OpenAPI 3.0 documentation for Spring Boot projects with a Swagger UI web interface for exploring and testing APIs.
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill spring-boot-openapi-documentationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.8k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
How do I add interactive OpenAPI docs and Swagger UI to a Spring Boot 3 service?
Set up SpringDoc OpenAPI 3.0 and Swagger UI in Spring Boot 3.x with annotations, security schemes, pagination docs, and grouped API versions.
Who is it for?
Spring Boot 3.x teams documenting REST APIs with SpringDoc and Swagger UI.
Skip if: Non-Spring frameworks or runtime-only API gateways without controller annotations.
When should I use this skill?
User sets up SpringDoc, Swagger UI, OpenAPI annotations, or API security documentation.
What you get
Published /v3/api-docs JSON and Swagger UI with annotated controllers, models, and security schemes.
- GroupedOpenApi bean configuration
- Segmented Swagger UI groups
Files
Spring Boot OpenAPI Documentation with SpringDoc
Overview
SpringDoc OpenAPI automates generation of OpenAPI 3.0 documentation for Spring Boot projects with a Swagger UI web interface for exploring and testing APIs.
When to Use
- Set up SpringDoc OpenAPI in Spring Boot 3.x projects
- Generate OpenAPI 3.0 specifications for REST APIs
- Configure and customize Swagger UI
- Add detailed API documentation with annotations
- Document request/response models with validation
- Implement API security documentation (JWT, OAuth2, Basic Auth)
- Document pageable and sortable endpoints
- Add examples and schemas to API endpoints
- Customize OpenAPI definitions programmatically
- Support multiple API groups and versions
- Document error responses and exception handlers
- Add JSR-303 Bean Validation to API documentation
- Support Kotlin-based Spring Boot APIs
Quick Reference
| Concept | Description |
|---|---|
| Dependencies | springdoc-openapi-starter-webmvc-ui for WebMvc, springdoc-openapi-starter-webflux-ui for WebFlux |
| Configuration | application.yml with springdoc.api-docs.* and springdoc.swagger-ui.* properties |
| Access Points | OpenAPI JSON: /v3/api-docs, Swagger UI: /swagger-ui/index.html |
| Core Annotations | @Tag, @Operation, @ApiResponse, @Parameter, @Schema, @SecurityRequirement |
| Security | Configure security schemes in OpenAPI bean, apply with @SecurityRequirement |
| Pagination | Use @ParameterObject with Spring Data Pageable |
Instructions
1. Add Dependencies
Add SpringDoc starter for your application type (WebMvc or WebFlux). See dependency-setup.md for Maven/Gradle configuration.
2. Configure SpringDoc
Set basic configuration in application.yml:
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operationsSorter: methodSee configuration.md for advanced options.
3. Document Controllers
Use OpenAPI annotations to add descriptive information:
@RestController
@Tag(name = "Book", description = "Book management APIs")
public class BookController {
@Operation(summary = "Get book by ID")
@ApiResponse(responseCode = "200", description = "Book found")
@GetMapping("/{id}")
public Book findById(@PathVariable Long id) { }
}See controller-documentation.md for patterns.
4. Document Models
Apply @Schema annotations to DTOs:
@Schema(description = "Book entity")
public class Book {
@Schema(example = "1", accessMode = Schema.AccessMode.READ_ONLY)
private Long id;
@Schema(example = "Clean Code", required = true)
private String title;
}See model-documentation.md for validation patterns.
5. Configure Security
Set up security schemes in OpenAPI bean:
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("bearer-jwt", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
)
);
}Apply with @SecurityRequirement(name = "bearer-jwt") on controllers. See security-configuration.md.
6. Document Pagination
Use @ParameterObject for Spring Data Pageable:
@GetMapping("/paginated")
public Page<Book> findAll(@ParameterObject Pageable pageable) {
return repository.findAll(pageable);
}See pagination-support.md.
7. Test Documentation
Access Swagger UI at /swagger-ui/index.html to verify documentation completeness.
8. Customize for Production
Configure API grouping, versioning, and build plugins. See advanced-configuration.md and build-integration.md.
Best Practices
- Use descriptive operation summaries: Short (< 120 chars), clear statements
- Document all response codes: Include success (2xx), client errors (4xx), server errors (5xx)
- Add examples to request/response bodies: Use
@ExampleObjectfor realistic examples - Leverage JSR-303 validation annotations: SpringDoc auto-generates constraints from validation annotations
- Use `@ParameterObject` for complex parameters: Especially for Pageable, custom filter objects
- Group related endpoints with `@Tag`: Organize API by domain entities or features
- Document security requirements: Apply
@SecurityRequirementwhere authentication needed - Hide internal endpoints appropriately: Use
@Hiddenor create separate API groups - Customize Swagger UI for better UX: Enable filtering, sorting, try-it-out features
- Version your API documentation: Include version in OpenAPI Info
References
- [dependency-setup.md](references/dependency-setup.md) — Maven/Gradle dependencies and version selection
- [configuration.md](references/configuration.md) — Basic and advanced configuration options
- [controller-documentation.md](references/controller-documentation.md) — Controller and endpoint documentation patterns
- [model-documentation.md](references/model-documentation.md) — Entity, DTO, and validation documentation
- [security-configuration.md](references/security-configuration.md) — JWT, OAuth2, Basic Auth, API key configuration
- [pagination-support.md](references/pagination-support.md) — Pageable, Slice, and custom pagination patterns
- [advanced-configuration.md](references/advanced-configuration.md) — API groups, customizers, OpenAPI bean configuration
- [exception-handling.md](references/exception-handling.md) — Exception documentation and error response schemas
- [build-integration.md](references/build-integration.md) — Maven/Gradle plugins and CI/CD integration
- [complete-examples.md](references/complete-examples.md) — Full controller, entity, and configuration examples
- [annotations-reference.md](references/annotations-reference.md) — Complete annotation reference with attributes
- [springdoc-official.md](references/springdoc-official.md) — Official SpringDoc documentation
- [troubleshooting.md](references/troubleshooting.md) — Common issues and solutions
Constraints and Warnings
- Do not expose sensitive data in API examples or schema descriptions
- Keep OpenAPI annotations minimal to avoid cluttering controller code; use global configurations when possible
- Large API definitions can impact Swagger UI performance; consider grouping APIs by domain
- Schema generation may not work correctly with complex generic types; use explicit
@Schemaannotations - Avoid circular references in DTOs as they cause infinite recursion in schema generation
- Security schemes must be properly configured before using
@SecurityRequirementannotations - Hidden endpoints (
@Operation(hidden = true)) are still visible in code and may leak through other documentation tools
Examples
Basic Controller Documentation
@RestController
@Tag(name = "Books", description = "Book management APIs")
@RequestMapping("/api/books")
public class BookController {
@Operation(
summary = "Get book by ID",
description = "Retrieves detailed information about a specific book"
)
@ApiResponse(responseCode = "200", description = "Book found")
@ApiResponse(responseCode = "404", description = "Book not found")
@GetMapping("/{id}")
public Book getBook(@PathVariable Long id) {
return bookService.findById(id);
}
@Operation(summary = "Create new book")
@SecurityRequirement(name = "bearer-jwt")
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Book createBook(@Valid @RequestBody CreateBookRequest request) {
return bookService.create(request);
}
}Documented Model with Validation
@Schema(description = "Book entity")
public class Book {
@Schema(description = "Unique identifier", example = "1", accessMode = Schema.AccessMode.READ_ONLY)
private Long id;
@Schema(description = "Book title", example = "Clean Code", required = true)
@NotBlank
@Size(min = 1, max = 200)
private String title;
@Schema(description = "Author name", example = "Robert C. Martin")
@NotBlank
private String author;
@Schema(description = "Price in USD", example = "29.99", minimum = "0")
@NotNull
@DecimalMin("0.0")
private BigDecimal price;
}Security Configuration
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Book API")
.version("1.0.0")
.description("REST API for book management"))
.components(new Components()
.addSecuritySchemes("bearer-jwt", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT"))
.addSecuritySchemes("api-key", new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.name("X-API-Key")));
}Related Skills
spring-boot-rest-api-standards— REST API design standardsspring-boot-dependency-injection— Dependency injection patternsunit-test-controller-layer— Testing REST controllersspring-boot-actuator— Production monitoring and management
External Resources
Advanced SpringDoc Configuration
Multiple API Groups
Group by Path
import org.springdoc.core.models.GroupedOpenApi;
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("public")
.pathsToMatch("/api/public/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/api/admin/**")
.build();
}
@Bean
public GroupedOpenApi userApi() {
return GroupedOpenApi.builder()
.group("user")
.pathsToMatch("/api/user/**")
.build();
}Group by Package
@Bean
public GroupedOpenApi controllerGroup() {
return GroupedOpenApi.builder()
.group("controllers")
.packagesToScan("com.example.controller")
.build();
}
@Bean
public GroupedOpenApi controllerGroup2() {
return GroupedOpenApi.builder()
.group("vendor-controllers")
.packagesToScan("com.vendor.controller")
.build();
}Group with Custom Configuration
@Bean
public GroupedOpenApi customGroup() {
return GroupedOpenApi.builder()
.group("custom")
.pathsToMatch("/api/custom/**")
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(CustomApi.class))
.build();
}Custom Operation Customizer
Global Operation Customization
import org.springdoc.core.customizers.OperationCustomizer;
@Bean
public OperationCustomizer customizeOperation() {
return (operation, handlerMethod) -> {
// Add custom extension
operation.addExtension("x-custom-field", "custom-value");
// Add tag based on annotation
if (handlerMethod.getMethod().isAnnotationPresent(Deprecated.class)) {
operation.addTagsItem("deprecated");
}
// Customize summary
String className = handlerMethod.getBeanType().getSimpleName();
operation.setSummary(className + ": " + operation.getSummary());
return operation;
};
}Conditional Customization
@Bean
public OperationCustomizer authOperationCustomizer() {
return (operation, handlerMethod) -> {
// Add security requirement for methods with @RequireAuth
if (handlerMethod.hasMethodAnnotation(RequireAuth.class)) {
operation.addSecurityItem(new SecurityRequirement().addList("bearer-jwt"));
}
return operation;
};
}Hide Endpoints
Hide Single Endpoint
@Operation(hidden = true)
@GetMapping("/internal")
public String internalEndpoint() {
return "Hidden from docs";
}Hide Entire Controller
import io.swagger.v3.oas.annotations.Hidden;
@Hidden
@RestController
@RequestMapping("/internal")
public class InternalController {
// All endpoints hidden from documentation
}Conditional Hiding
@Bean
public OperationCustomizer conditionalHiding() {
return (operation, handlerMethod) -> {
// Hide endpoints based on profile
if (isProductionProfile()) {
if (handlerMethod.getMethod().getName().contains("Debug")) {
operation.setHidden(true);
}
}
return operation;
};
}Custom OpenAPI Bean
Complete OpenAPI Configuration
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Book Management API")
.description("Comprehensive API for managing books, authors, and publishers")
.version("v1.0.0")
.contact(new Contact()
.name("API Support")
.email("support@example.com")
.url("https://example.com/support")
)
.license(new License()
.name("MIT License")
.url("https://opensource.org/licenses/MIT")
)
)
.addServersItem(new Server()
.url("https://api.example.com")
.description("Production server")
)
.addServersItem(new Server()
.url("https://staging-api.example.com")
.description("Staging server")
)
.addServersItem(new Server()
.url("http://localhost:8080")
.description("Development server")
);
}Environment-Specific Configuration
@Value("${api.version:v1.0.0}")
private String apiVersion;
@Value("${api.title:My API}")
private String apiTitle;
@Profile("production")
@Bean
public OpenAPI prodOpenAPI() {
return new OpenAPI()
.info(new Info()
.title(apiTitle)
.version(apiVersion)
.description("Production API")
)
.addServersItem(new Server().url("https://api.example.com"));
}
@Profile("development")
@Bean
public OpenAPI devOpenAPI() {
return new OpenAPI()
.info(new Info()
.title(apiTitle + " (DEV)")
.version(apiVersion)
.description("Development API")
)
.addServersItem(new Server().url("http://localhost:8080"));
}Custom Server Configuration
Multiple Servers with Variables
@Bean
public OpenAPI serversOpenAPI() {
Server prodServer = new Server()
.url("https://{environment}.example.com:{port}/api")
.description("Production server")
.addVariable("environment", new ServerVariable()
.defaultValue("api")
.enumeration(Arrays.asList("api", "api-staging"))
.description("Server environment")
)
.addVariable("port", new ServerVariable()
.defaultValue("443")
.description("Server port")
);
return new OpenAPI().addServersItem(prodServer);
}Custom Tags
Dynamic Tag Configuration
@Bean
public OpenAPI customTagsOpenAPI() {
return new OpenAPI()
.tags(Arrays.asList(
new Tag()
.name("public")
.description("Publicly accessible endpoints")
.externalDocs(new ExternalDocumentation()
.description("Public API documentation")
.url("https://docs.example.com/public")
),
new Tag()
.name("admin")
.description("Administrative endpoints")
.externalDocs(new ExternalDocumentation()
.description("Admin guide")
.url("https://docs.example.com/admin")
)
));
}Custom Properties
Adding Custom Extensions
@Bean
public OperationCustomizer addCustomExtensions() {
return (operation, handlerMethod) -> {
// Add rate limit info
operation.addExtension("x-rate-limit", 100);
// Add cost info
operation.addExtension("x-cost", 1);
// Add deprecation notice
if (handlerMethod.getMethod().isAnnotationPresent(Deprecated.class)) {
operation.addExtension("x-deprecated-since", "v1.0");
operation.addExtension("x-removal-date", "2025-01-01");
}
return operation;
};
}Custom Response Headers
Documenting Response Headers
@Operation(
summary = "Get book with headers",
responses = {
@ApiResponse(
responseCode = "200",
description = "Book found",
headers = {
@Header(name = "X-RateLimit-Remaining", description = "Remaining API calls", schema = @Schema(type = "integer")),
@Header(name = "X-RateLimit-Reset", description = "Rate limit reset time", schema = @Schema(type = "string"))
}
)
}
)
@GetMapping("/{id}")
public Book getBook(@PathVariable Long id) {
return repository.findById(id).orElseThrow();
}WebFlux Configuration
Reactive Router Function Documentation
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
@Bean
public RouterFunction<ServerResponse> bookRouter(BookHandler handler) {
return RouterFunctions.route()
.GET("/api/books", handler::getAllBooks)
.GET("/api/books/{id}", handler::getBookById)
.POST("/api/books", handler::createBook)
.build();
}Kotlin Support
Kotlin DSL Configuration
@Bean
fun customOpenAPI(): OpenAPI {
return OpenAPI()
.info(Info()
.title("Kotlin API")
.version("1.0.0")
.description("API built with Kotlin and Spring Boot")
)
}SpringDoc OpenAPI Annotations Reference
Core Annotations
@Tag
Groups operations under a logical tag.
// Controller level
@RestController
@Tag(name = "Book", description = "Book management APIs")
public class BookController { }
// With external docs
@Tag(
name = "Public APIs",
description = "Publicly accessible endpoints",
externalDocs = @ExternalDocumentation(
description = "Public API Guide",
url = "https://docs.example.com/public"
)
)Attributes:
name: Tag identifierdescription: Tag descriptionexternalDocs: External documentation reference
@Operation
Describes a single API operation.
@Operation(
summary = "Get book by ID",
description = "Retrieve detailed information about a specific book",
operationId = "getBookById",
deprecated = false,
hidden = false,
tags = {"Book"},
externalDocs = @ExternalDocumentation(
description = "Book API Guide",
url = "https://docs.example.com/book"
)
)Attributes:
summary: Short summary (< 120 chars)description: Detailed descriptionoperationId: Unique operation IDdeprecated: Mark as deprecatedhidden: Hide from documentationtags: Override default tagssecurity: Security requirementsresponses: Response definitionsparameters: Parameter definitions
@ApiResponse / @ApiResponses
Documents HTTP response codes.
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "Successfully retrieved",
content = @Content(
schema = @Schema(implementation = Book.class),
mediaType = "application/json"
)
),
@ApiResponse(
responseCode = "404",
description = "Resource not found",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))
)
})Attributes:
responseCode: HTTP status codedescription: Response descriptioncontent: Response content schemaheaders: Response headersextensions: Custom extensions
@Parameter
Documents operation parameters.
public Book getBook(
@Parameter(
description = "Book ID",
required = true,
example = "1",
deprecated = false,
hidden = false,
allowEmptyValue = false,
allowReserved = true,
schema = @Schema(type = "integer", format = "int64"),
content = @Content(
schema = @Schema(implementation = Book.class),
examples = @ExampleObject(value = "1")
)
)
@PathVariable Long id
) { }Attributes:
description: Parameter descriptionrequired: Whether required (default: inferred)example: Example valuedeprecated: Mark as deprecatedhidden: Hide from docsallowEmptyValue: Allow empty stringallowReserved: Allow reserved characters (:/?#[]@!$&'()*+,;=)schema: Parameter schemacontent: Parameter contentexplode: Explode array/object parametersstyle: Parameter style (matrix, label, form, simple, spaceDelimited, pipeDelimited, deepObject)
@RequestBody (OpenAPI)
Documents request body (not to be confused with Spring's @RequestBody).
@PostMapping
public Book create(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Book to create",
required = true,
content = @Content(
schema = @Schema(implementation = Book.class),
examples = @ExampleObject(
name = "Example Book",
value = "{\"title\": \"Clean Code\", \"author\": \"Robert C. Martin\"}"
)
)
)
@Valid @RequestBody Book book
) { }@Schema
Documents model schemas.
@Schema(
description = "Book entity",
name = "Book",
type = "object",
required = true,
requiredMode = Schema.RequiredMode.REQUIRED,
nullable = false,
readOnly = false,
writeOnly = false,
example = "{\"id\": 1, \"title\": \"Clean Code\"}",
externalDocs = @ExternalDocumentation(
description = "Book Model Docs",
url = "https://docs.example.com/book-model"
),
implementation = Book.class,
not = Book.class,
oneOf = {Book.class, Magazine.class},
anyOf = {Book.class, Magazine.class},
allOf = {BaseEntity.class}
)
public class Book { }
// Field level
@Schema(
description = "Book title",
example = "Clean Code",
required = true,
minLength = 1,
maxLength = 200,
pattern = "^[a-zA-Z0-9 ]*$",
type = "string",
format = "string",
allowableValues = {"Fiction", "Non-Fiction", "Technical"},
defaultValue = "Untitled",
accessMode = Schema.AccessMode.READ_ONLY,
hidden = false
)
private String title;Attributes:
description: Field descriptionexample: Example valuerequired: Whether requiredtype: Data type (string, number, integer, boolean, array, object)format: Data format (e.g., email, date, date-time, uuid)minimum/maximum: Numeric constraintsminLength/maxLength: String length constraintspattern: Regex patternallowableValues: Enumerated valuesdefaultValue: Default valuereadOnly: Read-only propertywriteOnly: Write-only propertyaccessMode: READ_ONLY, WRITE_ONLY, READ_WRITEhidden: Hide from documentationimplementation: Implementation class for genericsnullable: Whether nullabledeprecated: Mark as deprecated
@SecurityRequirement
Applies security requirements.
// Controller level
@SecurityRequirement(name = "bearer-jwt")
@RestController
public class BookController { }
// Operation level
@Operation(
summary = "Secure endpoint",
security = @SecurityRequirement(name = "bearer-jwt")
)
// Multiple security schemes (OR logic)
@Operation(
security = {
@SecurityRequirement(name = "bearer-jwt"),
@SecurityRequirement(name = "api-key")
}
)
// No security (override)
@Operation(security = {})@Hidden
Hides from documentation.
// Hide endpoint
@Operation(hidden = true)
@GetMapping("/internal")
public String internal() { }
// Hide entire controller
@Hidden
@RestController
public class InternalController { }@ParameterObject
Documents complex objects as parameters.
@GetMapping("/paginated")
public Page<Book> getPaginated(
@ParameterObject Pageable pageable
) { }
// Works with Spring Data Pageable, custom filter objectsValidation Annotations (Auto-Documented)
Standard Bean Validation
@NotNull // Required field
@NotBlank // Required, non-empty string
@NotEmpty // Required, non-empty collection
@Size(min=1, max=200) // String/collection length
@Min(0) // Numeric minimum
@Max(1000) // Numeric maximum
@DecimalMin("0.0") // Decimal minimum
@DecimalMax("999.99") // Decimal maximum
@Pattern(regex="^[A-Z].*") // Regex pattern
@Email // Email validation
@Past // Date in the past
@PastOrPresent // Date today or in the past
@Future // Date in the future
@FutureOrPresent // Date today or in the future
@Positive // Positive number
@PositiveOrZero // Positive or zero
@Negative // Negative number
@NegativeOrZero // Negative or zero
@AssertTrue // Must be true
@AssertFalse // Must be falseAdvanced Annotations
@ArraySchema
Documents array schemas.
@Schema(
description = "List of books",
implementation = Book[].class
)
List<Book> books;
// Using ArraySchema
@ArraySchema(
schema = @Schema(implementation = Book.class),
arraySchema = @Schema(
description = "Array of books",
minItems = 0,
maxItems = 100,
uniqueItems = false
)
)
List<Book> books;@Content
Detailed content documentation.
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Book.class),
examples = {
@ExampleObject(
name = "Example 1",
value = "{\"title\": \"Clean Code\"}",
summary = "Simple example"
),
@ExampleObject(
name = "Example 2",
value = "{\"title\": \"Effective Java\"}",
summary = "Another example",
externalValue = "https://example.com/book-example.json"
)
}
)@ExampleObject
Example values.
@ExampleObject(
name = "Book Example",
value = "{\"id\": 1, \"title\": \"Clean Code\"}",
summary = "A simple book example",
externalValue = "https://example.com/examples/book.json"
)@ExternalDocumentation
External documentation references.
@ExternalDocumentation(
description = "Detailed API documentation",
url = "https://docs.example.com/api"
)Composition Annotations
@DiscriminatorObject
For polymorphic types.
@Schema(
discriminatorProperty = "type",
discriminatorMapping = {
@DiscriminatorMapping(value = "book", schema = Book.class),
@DiscriminatorMapping(value = "magazine", schema = Magazine.class)
}
)
public abstract class Publication { }Annotation Best Practices
1. Use descriptive summaries: Keep under 120 characters 2. Provide detailed descriptions: Explain behavior and use cases 3. Document all response codes: Include 2xx, 4xx, 5xx 4. Add examples: Provide realistic request/response examples 5. Leverage validation: Let Bean Validation annotations auto-document constraints 6. Group logically: Use @Tag to organize related endpoints 7. Be consistent: Use similar annotation patterns across controllers 8. Hide internal endpoints: Use @Hidden or separate API groups 9. Document security: Apply @SecurityRequirement appropriately 10. Document complex types: Use @Schema for nested objects and generics
Build Integration
Maven Plugin
OpenAPI Generation Plugin
<plugin>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-maven-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<phase>integration-test</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<apiDocsUrl>http://localhost:8080/v3/api-docs</apiDocsUrl>
<outputFileName>openapi.json</outputFileName>
<outputDir>${project.build.directory}</outputDir>
</configuration>
</plugin>Custom Configuration
<plugin>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-maven-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<apiDocsUrl>http://localhost:8080/v3/api-docs</apiDocsUrl>
<outputFileName>openapi.yaml</outputFileName>
<outputDir>${project.build.directory}/docs</outputDir>
<skip>false</skip>
<headers>
<Authorization>Bearer test-token</Authorization>
</headers>
</configuration>
</plugin>Multiple API Groups
<plugin>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-maven-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>generate-public-api</id>
<phase>verify</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<apiDocsUrl>http://localhost:8080/v3/api-docs/public</apiDocsUrl>
<outputFileName>public-api.json</outputFileName>
<outputDir>${project.build.directory}/docs</outputDir>
</configuration>
</execution>
<execution>
<id>generate-admin-api</id>
<phase>verify</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<apiDocsUrl>http://localhost:8080/v3/api-docs/admin</apiDocsUrl>
<outputFileName>admin-api.json</outputFileName>
<outputDir>${project.build.directory}/docs</outputDir>
</configuration>
</execution>
</executions>
</plugin>Gradle Plugin
Basic Gradle Configuration
plugins {
id 'org.springdoc.openapi-gradle-plugin' version '1.9.0'
}
openApi {
apiDocsUrl = "http://localhost:8080/v3/api-docs"
outputDir = file("$buildDir/docs")
outputFileName = "openapi.json"
}Custom Gradle Configuration
openapi {
apiDocsUrl.set("http://localhost:8080/v3/api-docs")
outputDir.set(file("$buildDir/docs"))
outputFileName.set("openapi.yaml")
groupedApiMappings.set([
"public": "http://localhost:8080/v3/api-docs/public",
"admin": "http://localhost:8080/v3/api-docs/admin"
])
}CI/CD Integration
GitHub Actions Workflow
name: Generate API Docs
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run application
run: |
mvn spring-boot:run &
sleep 30 # Wait for app to start
- name: Generate OpenAPI docs
run: mvn verify
- name: Upload API docs
uses: actions/upload-artifact@v3
with:
name: openapi-spec
path: target/openapi.jsonGitLab CI Pipeline
stages:
- build
- docs
build:
stage: build
script:
- mvn clean install
artifacts:
paths:
- target/*.jar
generate-docs:
stage: docs
services:
- name: app:latest
alias: api
script:
- apk add --no-cache curl
- curl http://api:8080/v3/api-docs -o openapi.json
artifacts:
paths:
- openapi.json
only:
- mainAutomated Testing
OpenAPI Specification Validation
import org.springdoc.core.utils.SpringDocUtils;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class OpenApiDocumentationTest {
@Autowired
private OpenApiContract openApiContract;
@Test
void validateOpenApiSpec() {
OpenAPI openAPI = openApiContract.getOpenApi();
assertNotNull(openAPI);
assertNotNull(openAPI.getInfo());
assertEquals("1.0.0", openAPI.getInfo().getVersion());
assertFalse(openAPI.getPaths().isEmpty());
}
@Test
void allPathsHaveDocumentation() {
OpenAPI openAPI = openApiContract.getOpenApi();
openAPI.getPaths().forEach((path, pathItem) -> {
pathItem.readOperationsMap().forEach((method, operation) -> {
assertNotNull(operation.getSummary(), "Missing summary for " + method + " " + path);
assertFalse(operation.getResponses().isEmpty(), "No responses for " + method + " " + path);
});
});
}
}Schema Validation Tests
@Test
void validateBookSchema() {
OpenAPI openAPI = openApiContract.getOpenApi();
Schema bookSchema = openAPI.getComponents().getSchemas().get("Book");
assertNotNull(bookSchema);
assertTrue(bookSchema.getProperties().containsKey("id"));
assertTrue(bookSchema.getProperties().containsKey("title"));
assertTrue(bookSchema.getProperties().containsKey("author"));
}Static Documentation Generation
Generate Swagger UI Static Files
# Using Maven
mvn verify
# Using Gradle
gradle openApi
# The generated files will be in:
# - target/openapi.json (Maven)
# - buildDir/docs/openapi.json (Gradle)Custom Output Directory
<plugin>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-maven-plugin</artifactId>
<configuration>
<outputDir>${project.basedir}/src/main/resources/static/docs</outputDir>
<outputFileName>swagger.json</outputFileName>
</configuration>
</plugin>Redoc Integration
Add Redoc Dependency
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>Access Redoc UI
After adding the dependency:
- Redoc UI:
http://localhost:8080/api-docs(Redoc styling) - Swagger UI:
http://localhost:8080/swagger-ui/index.html(original)
Custom Redoc Configuration
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("API Documentation")
.version("1.0.0")
);
}
@Configuration
public class RedocConfig {
@Bean
public IndexPageCustomizer indexPageCustomizer() {
return indexHtml -> indexHtml.replace(
"<title>",
"<link rel='stylesheet' href='/webjars/redoc/redoc.css'><script src='/webjars/redoc/redoc.standalone.js'></script><title>"
);
}
}Version Management
API Versioning Strategy
# application.yml
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
# Multiple API versions
springdoc:
group-configs:
- group: 'v1'
paths-to-match: /api/v1/**
- group: 'v2'
paths-to-match: /api/v2/**Generate Versioned Specs
# Generate v1 spec
curl http://localhost:8080/v3/api-docs/v1 > openapi-v1.json
# Generate v2 spec
curl http://localhost:8080/v3/api-docs/v2 > openapi-v2.jsonComplete REST Controller Example
Full-Featured Book Controller
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/books")
@Tag(name = "Book", description = "Book management APIs")
@SecurityRequirement(name = "bearer-jwt")
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@Operation(summary = "Get all books")
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "Found all books",
content = @Content(
mediaType = "application/json",
array = @ArraySchema(schema = @Schema(implementation = Book.class))
)
)
})
@GetMapping
public List<Book> getAllBooks() {
return bookService.getAllBooks();
}
@Operation(summary = "Get paginated books")
@GetMapping("/paginated")
public Page<Book> getBooksPaginated(@ParameterObject Pageable pageable) {
return bookService.getBooksPaginated(pageable);
}
@Operation(summary = "Get book by ID")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Book found"),
@ApiResponse(responseCode = "404", description = "Book not found")
})
@GetMapping("/{id}")
public Book getBookById(
@Parameter(description = "Book ID", required = true, example = "1")
@PathVariable Long id
) {
return bookService.getBookById(id);
}
@Operation(summary = "Create new book")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "Book created successfully"),
@ApiResponse(responseCode = "400", description = "Invalid input")
})
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Book createBook(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Book to create",
required = true,
content = @Content(
schema = @Schema(implementation = Book.class),
examples = @io.swagger.v3.oas.annotations.media.ExampleObject(
value = """
{
"title": "Clean Code",
"author": "Robert C. Martin",
"isbn": "978-0132350884",
"price": 29.99,
"publicationDate": "2008-08-01"
}
"""
)
)
)
@Valid @RequestBody Book book
) {
return bookService.createBook(book);
}
@Operation(summary = "Update book")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Book updated"),
@ApiResponse(responseCode = "404", description = "Book not found"),
@ApiResponse(responseCode = "400", description = "Invalid input")
})
@PutMapping("/{id}")
public Book updateBook(
@Parameter(description = "Book ID", required = true)
@PathVariable Long id,
@Valid @RequestBody Book book
) {
return bookService.updateBook(id, book);
}
@Operation(summary = "Delete book")
@ApiResponses(value = {
@ApiResponse(responseCode = "204", description = "Book deleted"),
@ApiResponse(responseCode = "404", description = "Book not found")
})
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteBook(@PathVariable Long id) {
bookService.deleteBook(id);
}
@Operation(summary = "Search books by title")
@GetMapping("/search")
public Page<Book> searchBooks(
@Parameter(description = "Search query", example = "Clean")
@RequestParam String query,
@ParameterObject Pageable pageable
) {
return bookService.searchBooks(query, pageable);
}
}Complete Book Entity
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.time.LocalDate;
@Entity
@Table(name = "books")
@Schema(description = "Book entity representing a published book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Schema(description = "Unique identifier", example = "1", accessMode = Schema.AccessMode.READ_ONLY)
private Long id;
@NotBlank(message = "Title is required")
@Size(min = 1, max = 200)
@Schema(description = "Book title", example = "Clean Code", required = true, maxLength = 200)
private String title;
@NotBlank(message = "Author is required")
@Schema(description = "Book author", example = "Robert C. Martin", required = true)
private String author;
@Pattern(regexp = "^(?:ISBN(?:-1[03])?:? )?(?=[0-9X]{10}$|(?=(?:[0-9]+[- ]){3})[- 0-9X]{13}$|97[89][0-9]{10}$|(?=(?:[0-9]+[- ]){4})[- 0-9]{17}$)(?:97[89][- ]?)?[0-9]{1,5}[- ]?[0-9]+[- ]?[0-9]+[- ]?[0-9X]$")
@Schema(description = "ISBN number", example = "978-0132350884")
private String isbn;
@Min(value = 0, message = "Price must be positive")
@Schema(description = "Book price in USD", example = "29.99", minimum = "0")
private BigDecimal price;
@Past(message = "Publication date must be in the past")
@Schema(description = "Publication date", example = "2008-08-01")
private LocalDate publicationDate;
@Schema(description = "Book description", example = "A handbook of agile software craftsmanship")
private String description;
@Email(message = "Publisher email must be valid")
@Schema(description = "Publisher contact email", example = "contact@publisher.com")
private String publisherEmail;
// Constructors, getters, setters...
}Complete Configuration Class
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenAPIConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("bearer-jwt", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("JWT authentication - Enter token without 'Bearer' prefix")
)
)
.info(new Info()
.title("Book Management API")
.description("Comprehensive API for managing books, authors, and publishers")
.version("v1.0.0")
.contact(new Contact()
.name("API Support")
.email("support@example.com")
.url("https://example.com/support")
)
.license(new License()
.name("MIT License")
.url("https://opensource.org/licenses/MIT")
)
);
}
}Complete Application Properties
# application.yml
spring:
application:
name: book-management-api
springdoc:
api-docs:
path: /api-docs
enabled: true
swagger-ui:
path: /swagger-ui.html
enabled: true
operationsSorter: method
tagsSorter: alpha
tryItOutEnabled: true
displayRequestDuration: true
displayOperationId: false
defaultModelsExpandDepth: 1
defaultModelExpandDepth: 1
packages-to-scan: com.example.controller
paths-to-match: /api/**
show-actuator: false
server:
port: 8080
logging:
level:
org.springdoc: DEBUGComplete Security Configuration
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}Maven pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>book-management-api</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<properties>
<java.version>17</java.version>
<springdoc.version>2.8.13</springdoc.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</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>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>SpringDoc Configuration
Basic Configuration
application.properties
# API Documentation Path
springdoc.api-docs.path=/api-docs
springdoc.api-docs.enabled=true
# Swagger UI Configuration
springdoc.swagger-ui.path=/swagger-ui.html
springdoc.swagger-ui.enabled=true
springdoc.swagger-ui.operationsSorter=method
springdoc.swagger-ui.tagsSorter=alpha
springdoc.swagger-ui.tryItOutEnabled=true
# Package and Path Filtering
springdoc.packages-to-scan=com.example.controller
springdoc.paths-to-match=/api/**application.yml
springdoc:
api-docs:
path: /api-docs
enabled: true
swagger-ui:
path: /swagger-ui.html
enabled: true
operationsSorter: method
tagsSorter: alpha
tryItOutEnabled: true
packages-to-scan: com.example.controller
paths-to-match: /api/**Access Endpoints
After configuration:
- OpenAPI JSON:
http://localhost:8080/v3/api-docs - OpenAPI YAML:
http://localhost:8080/v3/api-docs.yaml - Swagger UI:
http://localhost:8080/swagger-ui/index.html
Advanced Configuration Options
Disable Specific Features
# Disable Swagger UI
springdoc.swagger-ui.enabled=false
# Disable API docs
springdoc.api-docs.enabled=false
# Disable try-it-out
springdoc.swagger-ui.tryItOutEnabled=falseSort Options
- operationsSorter:
method(HTTP method),alpha(alphabetical) - tagsSorter:
alpha(alphabetical) - defaultModelsExpandDepth: Controls model expansion in UI
Filter by Package/Path
# Scan multiple packages
springdoc.packages-to-scan=com.example.controller,vendor.another.controller
# Match multiple paths
springdoc.paths-to-match=/api/**,/public/**
# Exclude paths
springdoc.paths-to-exclude=/actuator/**,/admin/**Controller Documentation Patterns
Basic Controller Documentation
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/books")
@Tag(name = "Book", description = "Book management APIs")
public class BookController {
@Operation(
summary = "Retrieve a book by ID",
description = "Get a Book object by specifying its ID. The response includes id, title, author and description."
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "Successfully retrieved book",
content = @Content(schema = @Schema(implementation = Book.class))
),
@ApiResponse(
responseCode = "404",
description = "Book not found"
)
})
@GetMapping("/{id}")
public Book findById(
@Parameter(description = "ID of book to retrieve", required = true)
@PathVariable Long id
) {
return repository.findById(id)
.orElseThrow(() -> new BookNotFoundException());
}
}Document Request Bodies
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.media.ExampleObject;
@Operation(summary = "Create a new book")
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Book createBook(
@RequestBody(
description = "Book to create",
required = true,
content = @Content(
schema = @Schema(implementation = Book.class),
examples = @ExampleObject(
value = """
{
"title": "Clean Code",
"author": "Robert C. Martin",
"isbn": "978-0132350884",
"description": "A handbook of agile software craftsmanship"
}
"""
)
)
)
Book book
) {
return repository.save(book);
}Multiple Response Types
@Operation(summary = "Search books")
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "Search completed",
content = @Content(
array = @ArraySchema(schema = @Schema(implementation = Book.class))
)
),
@ApiResponse(
responseCode = "400",
description = "Invalid search parameters",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))
)
})
@GetMapping("/search")
public List<Book> searchBooks(
@Parameter(description = "Search query")
@RequestParam String query
) {
return service.search(query);
}Document Parameters
@GetMapping("/filtered")
public List<Book> filterBooks(
@Parameter(description = "Title filter", example = "Clean Code")
@RequestParam(required = false) String title,
@Parameter(description = "Author filter", example = "Robert C. Martin")
@RequestParam(required = false) String author,
@Parameter(description = "Minimum publication year", example = "2000")
@RequestParam(required = false) Integer fromYear
) {
return service.filter(title, author, fromYear);
}Document Matrix Parameters
@Operation(summary = "Get book attributes")
@GetMapping("/{id}/attributes/{attributeType}")
public BookAttribute getAttribute(
@Parameter(description = "Book ID", required = true)
@PathVariable Long id,
@Parameter(description = "Attribute type (metadata, reviews, ratings)", required = true)
@PathVariable String attributeType
) {
return service.getAttribute(id, attributeType);
}Document Headers
@Operation(summary = "Get authenticated user profile")
@GetMapping("/profile")
public UserProfile getProfile(
@Parameter(description = "Authorization token", required = true, example = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
@RequestHeader("Authorization") String authorization
) {
return service.getProfile(authorization);
}SpringDoc Dependency Setup
Maven Dependencies
<!-- Standard WebMVC support -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.13</version>
</dependency>
<!-- Optional: therapi-runtime-javadoc for JavaDoc support -->
<dependency>
<groupId>com.github.therapi</groupId>
<artifactId>therapi-runtime-javadoc</artifactId>
<version>0.15.0</version>
<scope>provided</scope>
</dependency>
<!-- WebFlux support -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>2.8.13</version>
</dependency>Gradle Dependencies
// Standard WebMVC support
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13'
// Optional: therapi-runtime-javadoc for JavaDoc support
implementation 'com.github.therapi:therapi-runtime-javadoc:0.15.0'
// WebFlux support
implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.8.13'Version Selection
- Spring Boot 3.x: Use SpringDoc 2.x (e.g., 2.8.13)
- Spring Boot 2.x: Use SpringDoc 1.x
- Always check for the latest stable version at Maven Central
Exception Documentation
Global Exception Handler
Comprehensive Exception Handler
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import io.swagger.v3.oas.annotations.Operation;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BookNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
@Operation(hidden = true)
public ErrorResponse handleBookNotFound(BookNotFoundException ex) {
return new ErrorResponse("BOOK_NOT_FOUND", ex.getMessage());
}
@ExceptionHandler(ValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@Operation(hidden = true)
public ErrorResponse handleValidation(ValidationException ex) {
return new ErrorResponse("VALIDATION_ERROR", ex.getMessage());
}
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
@Operation(hidden = true)
public ErrorResponse handleAccessDenied(AccessDeniedException ex) {
return new ErrorResponse("ACCESS_DENIED", "Insufficient permissions");
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@Operation(hidden = true)
public ErrorResponse handleGeneric(Exception ex) {
return new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred");
}
}Error Response Schema
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "Standard error response")
public record ErrorResponse(
@Schema(description = "Error code", example = "BOOK_NOT_FOUND")
String code,
@Schema(description = "Human-readable error message", example = "Book with ID 123 not found")
String message,
@Schema(description = "Additional error details")
List<ValidationError> details,
@Schema(description = "Error timestamp", example = "2024-01-15T10:30:00Z")
LocalDateTime timestamp,
@Schema(description = "Request path that caused the error", example = "/api/books/123")
String path
) {
public ErrorResponse(String code, String message) {
this(code, message, List.of(), LocalDateTime.now(), "");
}
@Schema(description = "Validation error detail")
public record ValidationError(
@Schema(description = "Field name", example = "title")
String field,
@Schema(description = "Error message", example = "Title is required")
String message
) {}
}Document-Specific Error Responses
API Response with Error Codes
@Operation(
summary = "Get book by ID",
responses = {
@ApiResponse(
responseCode = "200",
description = "Book found",
content = @Content(schema = @Schema(implementation = Book.class))
),
@ApiResponse(
responseCode = "404",
description = "Book not found",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))
),
@ApiResponse(
responseCode = "401",
description = "Unauthorized",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))
),
@ApiResponse(
responseCode = "500",
description = "Internal server error",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))
)
}
)
@GetMapping("/{id}")
public Book getBook(@PathVariable Long id) {
return repository.findById(id).orElseThrow(() -> new BookNotFoundException(id));
}Custom Error Response Examples
@Operation(
summary = "Create book",
responses = {
@ApiResponse(
responseCode = "201",
description = "Book created"
),
@ApiResponse(
responseCode = "400",
description = "Validation failed",
content = @Content(
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(
value = """
{
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{"field": "title", "message": "Title is required"},
{"field": "isbn", "message": "Invalid ISBN format"}
],
"timestamp": "2024-01-15T10:30:00Z",
"path": "/api/books"
}
"""
)
)
)
}
)
@PostMapping
public Book createBook(@Valid @RequestBody Book book) {
return repository.save(book);
}Problem Details for HTTP APIs
RFC 7807 Problem Details
@Schema(description = "RFC 7807 Problem Details")
public record ProblemDetail(
@Schema(description = "Problem type URI", example = "https://example.com/probs/book-not-found")
String type,
@Schema(description = "Short problem title", example = "Book Not Found")
String title,
@Schema(description = "HTTP status code", example = "404")
int status,
@Schema(description = "Detailed problem description", example = "Book with ID 123 does not exist")
String detail,
@Schema(description = "Instance identifier", example = "/api/books/123")
String instance
) {}
@RestControllerAdvice
public class ProblemDetailExceptionHandler {
@ExceptionHandler(BookNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
@Operation(hidden = true)
public ProblemDetail handleNotFound(BookNotFoundException ex) {
return new ProblemDetail(
"https://example.com/probs/book-not-found",
"Book Not Found",
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
"/api/books/" + ex.getId()
);
}
}Document Constraint Violations
Bean Validation Error Documentation
@Schema(description = "Constraint violation detail")
public record ConstraintViolation(
@Schema(description = "Invalid field", example = "title")
String field,
@Schema(description = "Constraint that failed", example = "NotBlank")
String constraint,
@Schema(description = "Error message", example = "must not be blank")
String message,
@Schema(description = "Invalid value", example = "null")
String rejectedValue
) {}
@Schema(description = "Validation error response")
public record ValidationErrorResponse(
@Schema(description = "Error code", example = "VALIDATION_FAILED")
String code,
@Schema(description = "Validation errors")
List<ConstraintViolation> violations,
@Schema(description = "Total number of violations", example = "2")
int violationCount,
@Schema(description = "Timestamp", example = "2024-01-15T10:30:00Z")
LocalDateTime timestamp
) {}Business Exception Documentation
Custom Business Exceptions
public class InsufficientStockException extends RuntimeException {
private final Long bookId;
private final int requested;
private final int available;
public InsufficientStockException(Long bookId, int requested, int available) {
super(String.format("Insufficient stock for book %d: requested=%d, available=%d",
bookId, requested, available));
this.bookId = bookId;
this.requested = requested;
this.available = available;
}
// Getters...
}
@ExceptionHandler(InsufficientStockException.class)
@ResponseStatus(HttpStatus.CONFLICT)
@Operation(hidden = true)
public ErrorResponse handleInsufficientStock(InsufficientStockException ex) {
return new ErrorResponse(
"INSUFFICIENT_STOCK",
String.format("Only %d copies available, %d requested", ex.getAvailable(), ex.getRequested()),
Map.of(
"bookId", ex.getBookId(),
"requested", ex.getRequested(),
"available", ex.getAvailable()
)
);
}Exception Handling Best Practices
1. Hide exception handlers from docs: Use @Operation(hidden = true) 2. Document error responses: Include all possible error codes in @ApiResponse 3. Use consistent error format: Standard error response structure 4. Provide actionable messages: Help users understand and fix errors 5. Include request ID: For tracing and support 6. Don't expose sensitive data: Sanitize exception messages 7. Use appropriate HTTP status codes: Follow HTTP semantics
@Schema(description = "Error response with request tracking")
public record ErrorResponse(
String code,
String message,
Object details,
LocalDateTime timestamp,
String path,
@Schema(description = "Request ID for support", example = "abc-123-xyz")
String requestId
) {}Model Documentation Patterns
Entity with Validation
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
@Entity
@Schema(description = "Book entity representing a published book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Schema(description = "Unique identifier", example = "1", accessMode = Schema.AccessMode.READ_ONLY)
private Long id;
@NotBlank(message = "Title is required")
@Size(min = 1, max = 200)
@Schema(description = "Book title", example = "Clean Code", required = true, maxLength = 200)
private String title;
@NotBlank(message = "Author is required")
@Schema(description = "Book author", example = "Robert C. Martin", required = true)
private String author;
@Pattern(regexp = "^(?:ISBN(?:-1[03])?:? )?(?=[0-9X]{10}$|(?=(?:[0-9]+[- ]){3})[- 0-9X]{13}$|97[89][0-9]{10}$|(?=(?:[0-9]+[- ]){4})[- 0-9]{17}$)(?:97[89][- ]?)?[0-9]{1,5}[- ]?[0-9]+[- ]?[0-9]+[- ]?[0-9X]$")
@Schema(description = "ISBN number", example = "978-0132350884")
private String isbn;
@Min(value = 0, message = "Price must be positive")
@Schema(description = "Book price in USD", example = "29.99", minimum = "0")
private BigDecimal price;
@Past(message = "Publication date must be in the past")
@Schema(description = "Publication date", example = "2008-08-01")
private LocalDate publicationDate;
@Email(message = "Publisher email must be valid")
@Schema(description = "Publisher contact email", example = "contact@publisher.com")
private String publisherEmail;
// Constructors, getters, setters...
}Nested Objects
@Schema(description = "Book with publisher details")
public class BookDetail {
@Schema(description = "Book information")
private Book book;
@Schema(description = "Publisher information")
private Publisher publisher;
@Schema(description = "Publication details")
private PublicationInfo publicationInfo;
}
@Schema(description = "Publisher entity")
public class Publisher {
@Schema(example = "Prentice Hall")
private String name;
@Schema(example = "contact@pearson.com")
private String email;
}Enum Documentation
public enum BookStatus {
@Schema(description = "Book is available for purchase")
AVAILABLE,
@Schema(description = "Book is out of stock")
OUT_OF_STOCK,
@Schema(description = "Book is discontinued")
DISCONTINUED
}
@Schema(description = "Book entity")
public class Book {
@Schema(description = "Current book status", example = "AVAILABLE")
private BookStatus status;
}Hidden Fields
@Schema(hidden = true)
private String internalField;
@JsonIgnore
@Schema(accessMode = Schema.AccessMode.READ_ONLY)
private LocalDateTime createdAt;
@Schema(description = "Password hash (write-only)", accessMode = Schema.AccessMode.WRITE_ONLY)
private String password;Read-Only Properties
@Schema(description = "Creation timestamp", accessMode = Schema.AccessMode.READ_ONLY, example = "2024-01-15T10:30:00Z")
private LocalDateTime createdAt;
@Schema(description = "Last update timestamp", accessMode = Schema.AccessMode.READ_ONLY, example = "2024-01-15T10:30:00Z")
private LocalDateTime updatedAt;Array and Collection Fields
@Schema(description = "List of book tags")
private List<String> tags;
@Schema(description = "Map of book metadata")
private Map<String, String> metadata;
@Schema(description = "Set of book categories")
private Set<Category> categories;Polymorphic Types
@Schema(description = "Payment method (one of: creditCard, paypal, bankTransfer)")
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = CreditCardPayment.class, name = "creditCard"),
@JsonSubTypes.Type(value = PayPalPayment.class, name = "paypal"),
@JsonSubTypes.Type(value = BankTransferPayment.class, name = "bankTransfer")
})
public abstract class PaymentMethod {
@Schema(example = "100.00")
protected BigDecimal amount;
}Required vs Optional Fields
@Schema(description = "User profile")
public class UserProfile {
@NotNull
@Schema(description = "User first name", example = "John", required = true)
private String firstName;
@NotNull
@Schema(description = "User last name", example = "Doe", required = true)
private String lastName;
@Schema(description = "User middle name (optional)", example = "William")
private String middleName;
@Schema(description = "User nickname (optional)", example = "Johnny")
private String nickname;
}Pagination Documentation
Spring Data Pageable Support
Basic Pageable Parameter
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@Operation(summary = "Get paginated list of books")
@GetMapping("/paginated")
public Page<Book> findAllPaginated(
@ParameterObject Pageable pageable
) {
return repository.findAll(pageable);
}This generates parameters:
page: Page number (0-based)size: Page sizesort: Sort criteria (field,direction)
Custom Pageable Documentation
@Operation(summary = "Get paginated books with custom defaults")
@GetMapping("/paginated")
public Page<Book> getBooksPaginated(
@ParameterObject
@Parameter(
description = "Pagination parameters (default: page=0, size=20, sort=id,asc)",
example = "page=0&size=20&sort=title,asc"
)
Pageable pageable
) {
return repository.findAll(pageable);
}Pageable with @ParameterObject
@GetMapping("/search")
public Page<Book> searchBooks(
@Parameter(description = "Search query")
@RequestParam String query,
@ParameterObject
Pageable pageable
) {
return repository.searchByTitle(query, pageable);
}Custom Page Response
Page Metadata
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
@Schema(description = "Paginated response wrapper")
public record PagedResponse<T>(
@Schema(description = "List of items")
List<T> content,
@Schema(description = "Current page number (0-based)", example = "0")
int currentPage,
@Schema(description = "Total number of pages", example = "5")
int totalPages,
@Schema(description = "Total number of items", example = "100")
long totalItems,
@Schema(description = "Number of items per page", example = "20")
int pageSize,
@Schema(description = "Whether this is the first page", example = "true")
boolean isFirst,
@Schema(description = "Whether this is the last page", example = "false")
boolean isLast
) {
public static <T> PagedResponse<T> from(Page<T> page) {
return new PagedResponse<>(
page.getContent(),
page.getNumber(),
page.getTotalPages(),
page.getTotalElements(),
page.getSize(),
page.isFirst(),
page.isLast()
);
}
}
@Operation(summary = "Get books with custom pagination")
@GetMapping("/paged")
public PagedResponse<Book> getPagedBooks(
@ParameterObject Pageable pageable
) {
Page<Book> page = repository.findAll(pageable);
return PagedResponse.from(page);
}Slice Documentation
Using Slice for Large Datasets
import org.springframework.data.domain.Slice;
@Operation(summary = "Get books as slice (no count query)")
@GetMapping("/sliced")
public Slice<Book> getBookSlice(
@Parameter(description = "Page number (0-based)", example = "0")
@RequestParam(defaultValue = "0") int page,
@Parameter(description = "Page size", example = "20")
@RequestParam(defaultValue = "20") int size
) {
return repository.findAll(PageRequest.of(page, size));
}Custom Pagination Objects
Custom Pagination DTO
@Schema(description = "Pagination request")
public record PaginationRequest(
@Schema(description = "Page number (0-based)", example = "0", minValue = "0")
@Min(0)
int page,
@Schema(description = "Page size", example = "20", minValue = "1", maxValue = "100")
@Min(1)
@Max(100)
int size,
@Schema(description = "Sort field", example = "title")
String sortField,
@Schema(description = "Sort direction", example = "asc", allowableValues = {"asc", "desc"})
String sortDirection
) {
public Pageable toPageable() {
Sort.Direction direction = Sort.Direction.fromString(sortDirection);
return PageRequest.of(page, size, Sort.by(direction, sortField));
}
}
@Operation(summary = "Get books with custom pagination")
@PostMapping("/paginated-custom")
public Page<Book> getBooksCustomPagination(
@RequestBody PaginationRequest request
) {
return repository.findAll(request.toPageable());
}Pagination with Filters
Filtered Pageable Endpoints
@Operation(summary = "Search books with pagination and filters")
@GetMapping("/search")
public Page<Book> searchBooks(
@Parameter(description = "Title filter")
@RequestParam(required = false) String title,
@Parameter(description = "Author filter")
@RequestParam(required = false) String author,
@Parameter(description = "Minimum price")
@RequestParam(required = false) BigDecimal minPrice,
@Parameter(description = "Maximum price")
@RequestParam(required = false) BigDecimal maxPrice,
@ParameterObject
Pageable pageable
) {
return repository.searchBooks(title, author, minPrice, maxPrice, pageable);
}Pagination Best Practices
1. Set reasonable defaults: page=0, size=20 2. Limit max page size: Prevent performance issues (max 100) 3. Document sort options: List sortable fields in description 4. Use Slice for large datasets: Avoid expensive count queries 5. Include pagination metadata: Help clients navigate results 6. Consider cursor-based pagination: For infinite scroll scenarios
@Operation(
summary = "Get paginated books",
description = """
Returns paginated list of books.
**Parameters:**
- `page`: Page number (0-based, default: 0)
- `size`: Items per page (1-100, default: 20)
- `sort`: Sort field and direction (e.g., `title,asc` or `price,desc`)
**Sortable fields:** id, title, author, price, publicationDate
"""
)
@GetMapping("/paginated")
public Page<Book> getPaginatedBooks(
@ParameterObject
@Parameter(description = "Pagination and sorting parameters")
Pageable pageable
) {
return repository.findAll(pageable);
}Security Configuration for API Documentation
JWT Bearer Authentication
Configuration Class
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenAPISecurityConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("bearer-jwt", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("JWT authentication - Enter token without 'Bearer' prefix")
)
);
}
}Apply to Controllers
@RestController
@RequestMapping("/api/books")
@SecurityRequirement(name = "bearer-jwt")
@Tag(name = "Book", description = "Protected book management APIs")
public class BookController {
// All endpoints require JWT authentication
}Apply to Specific Endpoints
@RestController
@RequestMapping("/api/books")
public class BookController {
@GetMapping("/public")
@Operation(summary = "Public endpoint - no auth required")
public List<Book> getPublicBooks() {
return service.getPublicBooks();
}
@GetMapping("/protected")
@Operation(summary = "Protected endpoint", security = @SecurityRequirement(name = "bearer-jwt"))
public List<Book> getProtectedBooks() {
return service.getProtectedBooks();
}
}OAuth2 Configuration
Authorization Code Flow
import io.swagger.v3.oas.models.security.OAuthFlow;
import io.swagger.v3.oas.models.security.OAuthFlows;
import io.swagger.v3.oas.models.security.Scopes;
@Bean
public OpenAPI oauth2OpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("oauth2", new SecurityScheme()
.type(SecurityScheme.Type.OAUTH2)
.flows(new OAuthFlows()
.authorizationCode(new OAuthFlow()
.authorizationUrl("https://auth.example.com/oauth/authorize")
.tokenUrl("https://auth.example.com/oauth/token")
.scopes(new Scopes()
.addString("read", "Read access to resources")
.addString("write", "Write access to resources")
.addString("admin", "Administrative access")
)
)
)
)
);
}Client Credentials Flow
@Bean
public OpenAPI clientCredentialsOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("oauth2-client-creds", new SecurityScheme()
.type(SecurityScheme.Type.OAUTH2)
.flows(new OAuthFlows()
.clientCredentials(new OAuthFlow()
.tokenUrl("https://auth.example.com/oauth/token")
.scopes(new Scopes()
.addString("api.read", "Read API access")
.addString("api.write", "Write API access")
)
)
)
)
);
}Basic Authentication
@Bean
public OpenAPI basicAuthOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("basicAuth", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("basic")
.description("Basic HTTP authentication")
)
);
}
@RestController
@SecurityRequirement(name = "basicAuth")
public class AdminController {
// Endpoints protected by Basic Auth
}API Key Authentication
@Bean
public OpenAPI apiKeyOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("api-key", new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.name("X-API-Key")
.description("API key in header")
)
);
}Multiple Security Schemes
@Bean
public OpenAPI multipleSecuritySchemes() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("bearer-jwt", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
)
.addSecuritySchemes("api-key", new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.name("X-API-Key")
)
);
}Apply Multiple Schemes (OR logic)
@Operation(
summary = "Endpoint with multiple auth options",
security = {
@SecurityRequirement(name = "bearer-jwt"),
@SecurityRequirement(name = "api-key")
}
)
@GetMapping("/secure")
public ResponseEntity<?> secureEndpoint() {
return ResponseEntity.ok().build();
}Conditional Security Requirements
@Operation(
summary = "Public endpoint (no security)",
security = {}
)
@GetMapping("/public")
public String publicEndpoint() {
return "Public access";
}
@Operation(
summary = "Admin only",
security = @SecurityRequirement(name = "bearer-jwt")
)
@GetMapping("/admin")
public String adminEndpoint() {
return "Admin access";
}Security Scheme Best Practices
1. Use descriptive descriptions: Help users understand how to format their tokens 2. Specify token format: Include "JWT" or "Bearer" in bearer format 3. Document scopes clearly: Explain what each OAuth scope allows 4. Hide sensitive endpoints: Use @Hidden on auth-related endpoints 5. Test in Swagger UI: Verify auth flows work before documenting 6. Use environment-specific URLs: Different auth URLs for dev/staging/prod
@Value("${springdoc.oauth2.auth-url:https://auth.example.com/oauth/authorize}")
private String authUrl;
@Bean
public OpenAPI environmentAwareOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("oauth2", new SecurityScheme()
.type(SecurityScheme.Type.OAUTH2)
.flows(new OAuthFlows()
.authorizationCode(new OAuthFlow()
.authorizationUrl(authUrl)
.tokenUrl("${springdoc.oauth2.token-url}")
.scopes(new Scopes()
.addString("read", "Read access")
)
)
)
)
);
}SpringDoc OpenAPI Official Documentation
Overview
SpringDoc OpenAPI is a Java library that automates API documentation generation for Spring Boot projects. It examines applications at runtime to infer API semantics based on Spring configurations and annotations.
Key Features
- OpenAPI 3 support with Spring Boot v3 (Java 17 & Jakarta EE 9)
- Swagger UI integration for interactive API documentation
- Scalar support as an alternative UI
- Multiple endpoint support with grouping capabilities
- Security integration with Spring Security and OAuth2
- Functional endpoints support for WebFlux and WebMvc.fn
Dependencies
Maven (Spring Boot 3.x)
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.13</version>
</dependency>Gradle (Spring Boot 3.x)
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13'WebFlux Support
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>2.8.13</version>
</dependency>Default Endpoints
After adding the dependency:
- OpenAPI JSON:
http://localhost:8080/v3/api-docs - OpenAPI YAML:
http://localhost:8080/v3/api-docs.yaml - Swagger UI:
http://localhost:8080/swagger-ui/index.html
Compatibility Matrix
| Spring Boot Version | SpringDoc OpenAPI Version |
|---|---|
| 3.4.x | 2.7.x - 2.8.x |
| 3.3.x | 2.6.x |
| 3.2.x | 2.3.x - 2.5.x |
| 3.1.x | 2.2.x |
| 3.0.x | 2.0.x - 2.1.x |
Basic Configuration
application.properties
# Custom API docs path
springdoc.api-docs.path=/api-docs
# Custom Swagger UI path
springdoc.swagger-ui.path=/swagger-ui-custom.html
# Sort operations by HTTP method
springdoc.swagger-ui.operationsSorter=method
# Sort tags alphabetically
springdoc.swagger-ui.tagsSorter=alpha
# Enable/disable Swagger UI
springdoc.swagger-ui.enabled=true
# Disable springdoc-openapi endpoints
springdoc.api-docs.enabled=false
# Show actuator endpoints in documentation
springdoc.show-actuator=true
# Packages to scan
springdoc.packages-to-scan=com.example.controller
# Paths to match
springdoc.paths-to-match=/api/**,/public/**
# Default response messages
springdoc.default-produces-media-type=application/json
springdoc.default-consumes-media-type=application/jsonapplication.yml
springdoc:
api-docs:
path: /api-docs
enabled: true
swagger-ui:
path: /swagger-ui.html
enabled: true
operationsSorter: method
tagsSorter: alpha
tryItOutEnabled: true
filter: true
displayRequestDuration: true
packages-to-scan: com.example.controller
paths-to-match: /api/**
show-actuator: falseOpenAPI Information Configuration
Programmatic Configuration
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenAPIConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Book API")
.version("1.0")
.description("REST API for managing books")
.termsOfService("https://example.com/terms")
.contact(new Contact()
.name("API Support")
.url("https://example.com/support")
.email("support@example.com"))
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0.html")))
.servers(List.of(
new Server().url("http://localhost:8080").description("Development server"),
new Server().url("https://api.example.com").description("Production server")
));
}
}Controller Documentation
Basic Controller Documentation
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/books")
@Tag(name = "Book", description = "Book management APIs")
public class BookController {
private final BookRepository repository;
public BookController(BookRepository repository) {
this.repository = repository;
}
@Operation(
summary = "Retrieve a book by ID",
description = "Get a Book object by specifying its ID. The response is Book object with id, title, author and description."
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "Successfully retrieved book",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = Book.class)
)
),
@ApiResponse(
responseCode = "404",
description = "Book not found",
content = @Content
),
@ApiResponse(
responseCode = "500",
description = "Internal server error",
content = @Content
)
})
@GetMapping("/{id}")
public Book findById(
@Parameter(description = "ID of book to retrieve", required = true)
@PathVariable Long id
) {
return repository.findById(id)
.orElseThrow(() -> new BookNotFoundException());
}
}Request Body Documentation
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.media.ExampleObject;
@Operation(summary = "Create a new book")
@ApiResponses(value = {
@ApiResponse(
responseCode = "201",
description = "Book created successfully",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = Book.class)
)
),
@ApiResponse(responseCode = "400", description = "Invalid input provided")
})
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Book createBook(
@RequestBody(
description = "Book to create",
required = true,
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = Book.class),
examples = @ExampleObject(
value = """
{
"title": "Clean Code",
"author": "Robert C. Martin",
"isbn": "978-0132350884",
"description": "A handbook of agile software craftsmanship"
}
"""
)
)
)
@org.springframework.web.bind.annotation.RequestBody Book book
) {
return repository.save(book);
}Model Documentation
Entity with Validation Annotations
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
@Entity
@Schema(description = "Book entity representing a published book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Schema(description = "Unique identifier", example = "1", accessMode = Schema.AccessMode.READ_ONLY)
private Long id;
@NotBlank(message = "Title is required")
@Size(min = 1, max = 200)
@Schema(description = "Book title", example = "Clean Code", required = true, maxLength = 200)
private String title;
@NotBlank(message = "Author is required")
@Size(min = 1, max = 100)
@Schema(description = "Book author", example = "Robert C. Martin", required = true)
private String author;
@Pattern(regexp = "^(?:ISBN(?:-1[03])?:? )?(?=[0-9X]{10}$|(?=(?:[0-9]+[- ]){3})[- 0-9X]{13}$|97[89][0-9]{10}$|(?=(?:[0-9]+[- ]){4})[- 0-9]{17}$)(?:97[89][- ]?)?[0-9]{1,5}[- ]?[0-9]+[- ]?[0-9]+[- ]?[0-9X]$")
@Schema(description = "ISBN number", example = "978-0132350884")
private String isbn;
// Constructor, getters, setters
}Hidden Fields
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(hidden = true)
private String internalField;
@JsonIgnore
@Schema(accessMode = Schema.AccessMode.READ_ONLY)
private LocalDateTime createdAt;Security Documentation
JWT Bearer Authentication
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.security.SecurityScheme;
@Configuration
public class OpenAPISecurityConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("bearer-jwt", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("JWT authentication")
)
);
}
}
// On controller or method level
@SecurityRequirement(name = "bearer-jwt")
@GetMapping("/secure")
public String secureEndpoint() {
return "Secure data";
}Basic Authentication
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("basicAuth", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("basic")
)
);
}OAuth2 Configuration
import io.swagger.v3.oas.models.security.OAuthFlow;
import io.swagger.v3.oas.models.security.OAuthFlows;
import io.swagger.v3.oas.models.security.Scopes;
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("oauth2", new SecurityScheme()
.type(SecurityScheme.Type.OAUTH2)
.flows(new OAuthFlows()
.authorizationCode(new OAuthFlow()
.authorizationUrl("https://auth.example.com/oauth/authorize")
.tokenUrl("https://auth.example.com/oauth/token")
.scopes(new Scopes()
.addString("read", "Read access")
.addString("write", "Write access")
)
)
)
)
);
}API Key Authentication
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("api-key", new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.name("X-API-Key")
)
);
}Pageable and Sorting Documentation
Spring Data Pageable Support
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@Operation(summary = "Get paginated list of books")
@GetMapping("/paginated")
public Page<Book> findAllPaginated(
@ParameterObject Pageable pageable
) {
return repository.findAll(pageable);
}This automatically generates documentation for:
page: Page number (0-indexed)size: Page sizesort: Sorting criteria (e.g., "title,asc")
Advanced Features
Multiple API Groups
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("public")
.pathsToMatch("/api/public/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/api/admin/**")
.build();
}Access groups at:
/v3/api-docs/public/v3/api-docs/admin
Hiding Endpoints
@Operation(hidden = true)
@GetMapping("/internal")
public String internalEndpoint() {
return "Hidden from docs";
}
// Or hide entire controller
@Hidden
@RestController
public class InternalController {
// All endpoints hidden
}Custom Operation Customizer
import org.springdoc.core.customizers.OperationCustomizer;
@Bean
public OperationCustomizer customizeOperation() {
return (operation, handlerMethod) -> {
operation.addExtension("x-custom-field", "custom-value");
return operation;
};
}Filtering Packages and Paths
@Bean
public GroupedOpenApi apiGroup() {
return GroupedOpenApi.builder()
.group("api")
.packagesToScan("com.example.controller")
.pathsToMatch("/api/**")
.pathsToExclude("/api/internal/**")
.build();
}Kotlin Support
Kotlin Data Class Documentation
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
@Entity
data class Book(
@field:Schema(description = "Unique identifier", accessMode = Schema.AccessMode.READ_ONLY)
@Id
val id: Long = 0,
@field:NotBlank
@field:Size(min = 1, max = 200)
@field:Schema(description = "Book title", example = "Clean Code", required = true)
val title: String = "",
@field:NotBlank
@field:Schema(description = "Author name", example = "Robert Martin")
val author: String = ""
)
@RestController
@RequestMapping("/api/books")
@Tag(name = "Book", description = "Book management APIs")
class BookController(private val repository: BookRepository) {
@Operation(summary = "Get all books")
@ApiResponses(value = [
ApiResponse(
responseCode = "200",
description = "Found books",
content = [Content(
mediaType = "application/json",
array = ArraySchema(schema = Schema(implementation = Book::class))
)]
),
ApiResponse(responseCode = "404", description = "No books found", content = [Content()])
])
@GetMapping
fun getAllBooks(): List<Book> = repository.findAll()
}Maven and Gradle Plugins
Maven Plugin for Generating OpenAPI
<plugin>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-maven-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<phase>integration-test</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<apiDocsUrl>http://localhost:8080/v3/api-docs</apiDocsUrl>
<outputFileName>openapi.json</outputFileName>
<outputDir>${project.build.directory}</outputDir>
</configuration>
</plugin>Gradle Plugin
plugins {
id 'org.springdoc.openapi-gradle-plugin' version '1.9.0'
}
openApi {
apiDocsUrl = "http://localhost:8080/v3/api-docs"
outputDir = file("$buildDir/docs")
outputFileName = "openapi.json"
}Migration from SpringFox
Replace SpringFox dependencies and update annotations:
@Api→@Tag@ApiOperation→@Operation@ApiParam→@Parameter- Remove
Docketbeans, useGroupedOpenApiinstead
Common Issues and Solutions
Parameter Names Not Appearing
Add -parameters compiler flag (Spring Boot 3.2+):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>Swagger UI Shows "Unable to render definition"
Ensure ByteArrayHttpMessageConverter is registered when overriding converters:
converters.add(new ByteArrayHttpMessageConverter());
converters.add(new MappingJackson2HttpMessageConverter());Endpoints Not Appearing
Check:
springdoc.packages-to-scanconfigurationspringdoc.paths-to-matchconfiguration- Endpoints aren't marked with
@Hidden
Security Configuration Issues
Permit SpringDoc endpoints in Spring Security:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.anyRequest().authenticated()
)
.build();
}External References
Troubleshooting SpringDoc OpenAPI
Common Issues and Solutions
Parameter Names Not Appearing
Problem: Parameter names are not showing up in the generated API documentation.
Solution: Add -parameters compiler flag (Spring Boot 3.2+):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>Gradle equivalent:
tasks.withType(JavaCompile).configureEach {
options.compilerArgs += ["-parameters"]
}Swagger UI Shows "Unable to render definition"
Problem: Swagger UI displays error "Unable to render definition".
Solution: Ensure ByteArrayHttpMessageConverter is registered when overriding converters:
converters.add(new ByteArrayHttpMessageConverter());
converters.add(new MappingJackson2HttpMessageConverter());Alternative approach: Check for missing message converter configuration in your WebMvcConfigurer or similar configuration.
Endpoints Not Appearing in Documentation
Problem: API endpoints are not showing up in the generated OpenAPI specification.
Solution: Check these common issues:
1. Package scanning configuration:
# Ensure this is set correctly
springdoc.packages-to-scan=com.example.controller
# Or multiple packages
springdoc.packages-to-scan=com.example.controller,com.example.service2. Path matching configuration:
# Ensure paths match your endpoints
springdoc.paths-to-match=/api/**,public/**3. Hidden endpoints: Verify endpoints aren't marked with @Hidden annotation.
4. Component scanning: Ensure controllers are in packages that are component-scanned by Spring Boot.
Security Configuration Issues
Problem: Spring Security blocks access to Swagger UI and OpenAPI endpoints.
Solution: Permit SpringDoc endpoints in Spring Security:
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.anyRequest().authenticated()
)
.build();
}
}Maven/Gradle Build Issues
Problem: Build fails due to conflicting SpringDoc dependencies.
Solution: Ensure correct version compatibility:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.13</version>
</dependency>For WebFlux applications:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>2.8.13</version>
</dependency>JavaDoc Integration Issues
Problem: JavaDoc comments are not appearing in the API documentation.
Solution: Add the therapi-runtime-javadoc dependency:
<dependency>
<groupId>com.github.therapi</groupId>
<artifactId>therapi-runtime-javadoc</artifactId>
<version>0.15.0</version>
<scope>provided</scope>
</dependency>Kotlin Integration Issues
Problem: Kotlin classes or functions are not properly documented.
Solution: Use @field: annotation prefix for Kotlin properties:
@field:Schema(description = "Book title", example = "Clean Code")
@field:NotBlank
val title: String = ""Custom Serialization Issues
Problem: Custom serialized fields are not appearing in the API documentation.
Solution: Ensure proper Jackson configuration:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}Performance Issues
Problem: SpringDoc causes performance issues during startup.
Solution: 1. Use specific package scanning instead of scanning the entire classpath 2. Use path exclusions to filter out unwanted endpoints 3. Consider using grouped OpenAPI definitions
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("public")
.packagesToScan("com.example.controller.public")
.pathsToMatch("/api/public/**")
.pathsToExclude("/api/internal/**")
.build();
}Version Compatibility Issues
Problem: SpringDoc works in development but not in production.
Solution: 1. Ensure consistent Spring Boot and SpringDoc versions 2. Check for environment-specific configurations 3. Verify production environment matches development setup
# Production-specific configuration
springdoc.swagger-ui.enabled=true
springdoc.api-docs.enabled=true
springdoc.show-actuator=trueError Response Documentation
Problem: Custom error responses are not properly documented.
Solution: Use @Operation(hidden = true) on exception handlers and define proper error response schemas:
@ExceptionHandler(BookNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
@Operation(hidden = true)
public ErrorResponse handleBookNotFound(BookNotFoundException ex) {
return new ErrorResponse("BOOK_NOT_FOUND", ex.getMessage());
}
@Schema(description = "Error response")
public record ErrorResponse(
@Schema(description = "Error code", example = "BOOK_NOT_FOUND")
String code,
@Schema(description = "Error message", example = "Book with ID 123 not found")
String message
) {}Debugging Tips
1. Check OpenAPI JSON directly: Access http://localhost:8080/v3/api-docs to see the raw OpenAPI specification 2. Enable debug logging: Add logging.level.org.springdoc=DEBUG to application.properties 3. Validate OpenAPI specification: Use online validators like Swagger Editor 4. Check SpringDoc version: Ensure you're using a recent version with bug fixes
Performance Optimization
1. Reduce scope: Use specific package scanning and path matching 2. Cache configurations: Reuse OpenAPI configurations where possible 3. Group endpoints: Use multiple grouped OpenAPI definitions instead of one large specification 4. Disable unnecessary features: Turn off features you don't use (e.g., actuator integration)
# Performance optimizations
springdoc.swagger-ui.enabled=true
springdoc.api-docs.enabled=true
springdoc.show-actuator=false
springdoc.writer-default-response-tags=false
springdoc.default-consumes-media-type=application/json
springdoc.default-produces-media-type=application/jsonRelated skills
Forks & variants (1)
Spring Boot Openapi Documentation has 1 known copy in the catalog totaling 21 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 21 installs
How it compares
Choose spring-boot-openapi-documentation over generic OpenAPI skills when the stack is Spring Boot with SpringDoc and you need grouped endpoint documentation.
FAQ
Which dependency enables Swagger UI on WebMvc?
springdoc-openapi-starter-webmvc-ui for Spring MVC applications.
Where is Swagger UI served?
Default access at /swagger-ui/index.html with OpenAPI JSON at /v3/api-docs.
How are secured endpoints documented?
Configure security schemes in the OpenAPI bean and apply @SecurityRequirement on operations.
Is Spring Boot Openapi Documentation safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.