
Spring Boot Openapi Documentation
- 21 installs
- 318 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit-claude-code
This is a copy of spring-boot-openapi-documentation by giuseppe-trisciuoglio - installs and ranking accrue to the original listing.
Helps with backend & apis tasks.
About
spring-boot-openapi-documentation is a Claude Code skill for backend & apis. It helps solo builders move faster with AI-assisted development.
- spring-boot-openapi-documentation
- Backend & APIs
- AI-coding skill
Spring Boot Openapi Documentation by the numbers
- 21 all-time installs (skills.sh)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit-claude-code --skill spring-boot-openapi-documentationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 21 |
|---|---|
| repo stars | ★ 318 |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit-claude-code ↗ |
What it does
Helps with backend & apis tasks.
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/json