Spring Boot Topic
Spring Boot OpenAPI Swagger
1. Introduction to OpenAPI and Swagger
OpenAPI Specification (formerly Swagger) is a standard for documenting RESTful APIs. Spring Boot integrates with Swagger through Springdoc OpenAPI to automatically generate API documentation.
Benefits:
-
Automatic Documentation: Generate API docs from code
-
Interactive Testing: Test APIs directly from documentation
-
Client SDK Generation: Generate client code in multiple languages
-
API Design First: Design APIs before implementation
2. Setup and Dependencies
Maven Dependencies
<dependencies>
<!-- SpringDoc OpenAPI UI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<!-- Optional: OpenAPI Security -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-common</artifactId>
<version>2.3.0</version>
</dependency>
<!-- Validation for better documentation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
Gradle Dependencies
dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
3. Basic Configuration
3.1 Application Properties
# application.properties
springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=/swagger-ui.html
springdoc.swagger-ui.operations-sorter=method
springdoc.swagger-ui.tags-sorter=alpha
springdoc.swagger-ui.disable-swagger-default-url=true
springdoc.swagger-ui.display-request-duration=true
springdoc.swagger-ui.doc-expansion=none
# API Info
springdoc.info.title=Spring Boot API Documentation
springdoc.info.description=Comprehensive REST API for Spring Boot Application
springdoc.info.version=1.0.0
springdoc.info.contact.name=API Support
springdoc.info.contact.email=support@example.com
springdoc.info.contact.url=https://www.example.com
springdoc.info.license.name=Apache 2.0
springdoc.info.license.url=https://www.apache.org/licenses/LICENSE-2.0
# Packages to scan
springdoc.packages-to-scan=com.example.controller
springdoc.paths-to-match=/api/**
# Cache control
springdoc.cache.disabled=false
# Show actuator endpoints
springdoc.show-actuator=true
3.2 YAML Configuration
# application.yml
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operations-sorter: method
tags-sorter: alpha
display-request-duration: true
doc-expansion: none
disable-swagger-default-url: true
enabled: true
info:
title: Spring Boot API Documentation
description: Comprehensive REST API for Spring Boot Application
version: 1.0.0
contact:
name: API Support
email: support@example.com
url: https://www.example.com
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0
packages-to-scan: com.example.controller
paths-to-match: /api/**
cache:
disabled: false
show-actuator: true
3.3 Java Configuration
@Configuration
@OpenAPIDefinition(
info = @Info(
title = "Spring Boot API",
version = "1.0.0",
description = "Comprehensive REST API for Spring Boot Application",
contact = @Contact(
name = "API Support",
email = "support@example.com",
url = "https://www.example.com"
),
license = @License(
name = "Apache 2.0",
url = "https://www.apache.org/licenses/LICENSE-2.0"
),
termsOfService = "https://www.example.com/terms"
),
servers = {
@Server(
url = "http://localhost:8080",
description = "Development Server"
),
@Server(
url = "https://api.example.com",
description = "Production Server"
)
},
externalDocs = @ExternalDocumentation(
description = "Spring Boot Documentation",
url = "https://spring.io/projects/spring-boot"
)
)
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Spring Boot API")
.version("1.0.0")
.description("Comprehensive REST API for Spring Boot Application")
.termsOfService("https://www.example.com/terms")
.contact(new Contact()
.name("API Support")
.email("support@example.com")
.url("https://www.example.com"))
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0")))
.externalDocs(new ExternalDocumentation()
.description("Spring Boot Documentation")
.url("https://spring.io/projects/spring-boot"))
.servers(List.of(
new Server()
.url("http://localhost:8080")
.description("Development Server"),
new Server()
.url("https://api.example.com")
.description("Production Server")
))
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
.components(new Components()
.addSecuritySchemes("bearerAuth", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT"))
.addSchemas("ApiResponse", new Schema<ApiResponse>()
.type("object")
.addProperty("success", new BooleanSchema())
.addProperty("message", new StringSchema())
.addProperty("data", new Schema<>()))
.addSchemas("ErrorResponse", new Schema<ErrorResponse>()
.type("object")
.addProperty("timestamp", new DateTimeSchema())
.addProperty("status", new IntegerSchema())
.addProperty("error", new StringSchema())
.addProperty("message", new StringSchema())
.addProperty("path", new StringSchema())));
}
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("public")
.pathsToMatch("/api/public/**")
.build();
}
@Bean
public GroupedOpenApi userApi() {
return GroupedOpenApi.builder()
.group("users")
.pathsToMatch("/api/users/**")
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(PreAuthorize.class))
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/api/admin/**")
.build();
}
@Bean
public GroupedOpenApi actuatorApi() {
return GroupedOpenApi.builder()
.group("actuator")
.pathsToMatch("/actuator/**")
.build();
}
}
4. Annotating Controllers
4.1 Basic Controller Documentation
@RestController
@RequestMapping("/api/users")
@Tag(
name = "User Management",
description = "Operations pertaining to user management in the system"
)
@Validated
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Operation(
summary = "Get all users",
description = "Retrieve a paginated list of all users in the system",
responses = {
@ApiResponse(
responseCode = "200",
description = "Successfully retrieved users",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = PagedResponse.class)
)
),
@ApiResponse(
responseCode = "401",
description = "Unauthorized - authentication required",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))
),
@ApiResponse(
responseCode = "403",
description = "Forbidden - insufficient permissions",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))
)
}
)
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<PagedResponse<User>> getAllUsers(
@Parameter(
description = "Page number (0-based)",
example = "0",
in = ParameterIn.QUERY
) @RequestParam(defaultValue = "0") int page,
@Parameter(
description = "Number of items per page",
example = "10",
in = ParameterIn.QUERY
) @RequestParam(defaultValue = "10") int size,
@Parameter(
description = "Sort by field",
example = "username",
in = ParameterIn.QUERY
) @RequestParam(defaultValue = "id") String sortBy,
@Parameter(
description = "Sort direction",
schema = @Schema(allowableValues = {"asc", "desc"}),
example = "asc",
in = ParameterIn.QUERY
) @RequestParam(defaultValue = "asc") String direction) {
Pageable pageable = PaginationUtils.createPageable(page, size, sortBy, direction);
Page<User> userPage = userService.getAllUsers(pageable);
PagedResponse<User> response = PaginationUtils.createPagedResponse(userPage);
return ResponseEntity.ok(response);
}
@Operation(
summary = "Get user by ID",
description = "Retrieve a specific user by their unique identifier",
parameters = {
@Parameter(
name = "id",
description = "User ID",
required = true,
example = "1",
in = ParameterIn.PATH
)
}
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "User found successfully",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = User.class)
)
),
@ApiResponse(
responseCode = "404",
description = "User not found",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))
),
@ApiResponse(
responseCode = "400",
description = "Invalid user ID format",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))
)
})
@GetMapping("/{id}")
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public ResponseEntity<User> getUserById(
@PathVariable @Min(1) Long id) {
User user = userService.getUserById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
return ResponseEntity.ok(user);
}
@Operation(
summary = "Create a new user",
description = "Register a new user in the system"
)
@PostMapping
public ResponseEntity<User> createUser(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "User object that needs to be created",
required = true,
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = CreateUserRequest.class)
)
)
@Valid @RequestBody CreateUserRequest request) {
User user = new User(request.getUsername(), request.getEmail(), request.getPassword());
User createdUser = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}
@Operation(
summary = "Update user",
description = "Update an existing user's information"
)
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or (hasRole('USER') and #id == principal.id)")
public ResponseEntity<User> updateUser(
@Parameter(description = "User ID") @PathVariable Long id,
@RequestBody @Valid UpdateUserRequest request) {
User user = userService.getUserById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
User updatedUser = userService.updateUser(user);
return ResponseEntity.ok(updatedUser);
}
@Operation(
summary = "Delete user",
description = "Delete a user from the system"
)
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteUser(
@Parameter(description = "User ID") @PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
@Operation(
summary = "Search users",
description = "Search users by various criteria with pagination"
)
@GetMapping("/search")
public ResponseEntity<PagedResponse<User>> searchUsers(
@Parameter(description = "Username to search for")
@RequestParam(required = false) String username,
@Parameter(description = "Email to search for")
@RequestParam(required = false) String email,
@Parameter(description = "Active status filter")
@RequestParam(required = false) Boolean active,
@Parameter(description = "Page number")
@RequestParam(defaultValue = "0") int page,
@Parameter(description = "Page size")
@RequestParam(defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
UserSearchCriteria criteria = new UserSearchCriteria(username, email, active);
Page<User> userPage = userService.searchUsers(criteria, pageable);
PagedResponse<User> response = PaginationUtils.createPagedResponse(userPage);
return ResponseEntity.ok(response);
}
}
4.2 Authentication Controller
@RestController
@RequestMapping("/api/auth")
@Tag(
name = "Authentication",
description = "Authentication and authorization endpoints"
)
@Validated
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider tokenProvider;
@Operation(
summary = "User login",
description = "Authenticate user and return JWT token"
)
@PostMapping("/login")
public ResponseEntity<JwtAuthenticationResponse> login(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Login credentials",
required = true,
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = LoginRequest.class)
)
)
@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsernameOrEmail(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.generateToken(authentication);
JwtAuthenticationResponse response = new JwtAuthenticationResponse(
jwt, "Bearer", tokenProvider.getExpirationDateFromToken(jwt).getTime()
);
return ResponseEntity.ok(response);
}
@Operation(
summary = "User registration",
description = "Register a new user account"
)
@PostMapping("/register")
public ResponseEntity<?> register(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "User registration details",
required = true,
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = SignUpRequest.class)
)
)
@Valid @RequestBody SignUpRequest signUpRequest) {
if (userRepository.existsByUsername(signUpRequest.getUsername())) {
return ResponseEntity.badRequest()
.body(new ApiResponse(false, "Username is already taken!"));
}
if (userRepository.existsByEmail(signUpRequest.getEmail())) {
return ResponseEntity.badRequest()
.body(new ApiResponse(false, "Email Address already in use!"));
}
User user = new User(signUpRequest.getUsername(), signUpRequest.getEmail(), signUpRequest.getPassword());
user.setPassword(passwordEncoder.encode(user.getPassword()));
User result = userRepository.save(user);
return ResponseEntity.ok(new ApiResponse(true, "User registered successfully"));
}
@Operation(
summary = "Refresh token",
description = "Refresh JWT token using refresh token"
)
@PostMapping("/refresh-token")
public ResponseEntity<JwtAuthenticationResponse> refreshToken(
@Valid @RequestBody TokenRefreshRequest request) {
// Token refresh logic
String newJwt = tokenProvider.refreshToken(request.getRefreshToken());
JwtAuthenticationResponse response = new JwtAuthenticationResponse(
newJwt, "Bearer", tokenProvider.getExpirationDateFromToken(newJwt).getTime()
);
return ResponseEntity.ok(response);
}
}
5. DTO Classes with Schema Annotations
5.1 Request/Response DTOs
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "User creation request")
public class CreateUserRequest {
@Schema(
description = "Username for the new user",
example = "john_doe",
requiredMode = Schema.RequiredMode.REQUIRED,
minLength = 3,
maxLength = 50
)
@NotBlank
@Size(min = 3, max = 50)
private String username;
@Schema(
description = "Email address",
example = "john.doe@example.com",
requiredMode = Schema.RequiredMode.REQUIRED,
pattern = "^[A-Za-z0-9+_.-]+@(.+)$"
)
@NotBlank
@Email
@Size(max = 100)
private String email;
@Schema(
description = "Password",
example = "SecurePassword123!",
requiredMode = Schema.RequiredMode.REQUIRED,
minLength = 6,
maxLength = 100
)
@NotBlank
@Size(min = 6, max = 100)
@Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$",
message = "Password must contain at least one digit, one lowercase, one uppercase, and one special character")
private String password;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "User update request")
public class UpdateUserRequest {
@Schema(description = "Username", example = "john_doe_updated")
@NotBlank
@Size(min = 3, max = 50)
private String username;
@Schema(description = "Email address", example = "john.updated@example.com")
@NotBlank
@Email
@Size(max = 100)
private String email;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Login request")
public class LoginRequest {
@Schema(
description = "Username or email address",
example = "john_doe or john.doe@example.com",
requiredMode = Schema.RequiredMode.REQUIRED
)
@NotBlank
private String usernameOrEmail;
@Schema(
description = "Password",
example = "SecurePassword123!",
requiredMode = Schema.RequiredMode.REQUIRED
)
@NotBlank
private String password;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "JWT authentication response")
public class JwtAuthenticationResponse {
@Schema(description = "JWT access token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
private String accessToken;
@Schema(description = "Token type", example = "Bearer")
private String tokenType = "Bearer";
@Schema(description = "Token expiration in milliseconds", example = "86400000")
private Long expiresIn;
@Schema(description = "Refresh token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
private String refreshToken;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "API response wrapper")
public class ApiResponse {
@Schema(description = "Success status", example = "true")
private Boolean success;
@Schema(description = "Response message", example = "Operation completed successfully")
private String message;
@Schema(description = "Response data")
private Object data;
public ApiResponse(Boolean success, String message) {
this.success = success;
this.message = message;
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Error response")
public class ErrorResponse {
@Schema(description = "Timestamp of the error", example = "2024-01-15T10:30:00.000Z")
private LocalDateTime timestamp;
@Schema(description = "HTTP status code", example = "400")
private int status;
@Schema(description = "Error type", example = "Bad Request")
private String error;
@Schema(description = "Error message", example = "Invalid input parameters")
private String message;
@Schema(description = "Request path", example = "/api/users/123")
private String path;
public ErrorResponse(int status, String error, String message, String path) {
this.timestamp = LocalDateTime.now();
this.status = status;
this.error = error;
this.message = message;
this.path = path;
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Paginated response")
public class PagedResponse<T> {
@Schema(description = "List of items in the current page")
private List<T> content;
@Schema(description = "Current page number (0-based)", example = "0")
private int pageNumber;
@Schema(description = "Number of items per page", example = "10")
private int pageSize;
@Schema(description = "Total number of elements", example = "150")
private long totalElements;
@Schema(description = "Total number of pages", example = "15")
private int totalPages;
@Schema(description = "Is this the last page?", example = "false")
private boolean last;
@Schema(description = "Is this the first page?", example = "true")
private boolean first;
@Schema(description = "Is the content empty?", example = "false")
private boolean empty;
@Schema(description = "Number of elements in current page", example = "10")
private int numberOfElements;
}
5.2 Entity Classes with Schema
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "User entity")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Schema(description = "User unique identifier", example = "1")
private Long id;
@Column(nullable = false, unique = true, length = 50)
@Schema(
description = "Username",
example = "john_doe",
requiredMode = Schema.RequiredMode.REQUIRED
)
private String username;
@Column(nullable = false, unique = true, length = 100)
@Schema(
description = "Email address",
example = "john.doe@example.com",
requiredMode = Schema.RequiredMode.REQUIRED
)
private String email;
@Schema(description = "User active status", example = "true")
private boolean active = true;
@CreationTimestamp
@Column(name = "created_at")
@Schema(description = "Creation timestamp")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
@Schema(description = "Last update timestamp")
private LocalDateTime updatedAt;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
@Schema(description = "User roles")
private Set<Role> roles = new HashSet<>();
// Constructors
public User(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}
}
@Entity
@Table(name = "roles")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Role entity")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Schema(description = "Role unique identifier", example = "1")
private Long id;
@Enumerated(EnumType.STRING)
@Column(length = 20)
@Schema(
description = "Role name",
example = "ROLE_USER",
allowableValues = {"ROLE_USER", "ROLE_ADMIN", "ROLE_MODERATOR"}
)
private RoleName name;
}
6. Advanced Configuration
6.1 Security Configuration for Swagger
@Configuration
public class SwaggerSecurityConfig {
@Bean
public OpenAPI customOpenAPIWithSecurity() {
final String securitySchemeName = "bearerAuth";
return new OpenAPI()
.addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
.components(new Components()
.addSecuritySchemes(securitySchemeName, new SecurityScheme()
.name(securitySchemeName)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("JWT Authorization header using the Bearer scheme. Example: \\\"Authorization: Bearer {token}\\\""))
.addSchemas("ApiError", new Schema<ApiError>()
.type("object")
.addProperty("timestamp", new DateTimeSchema())
.addProperty("status", new IntegerSchema())
.addProperty("error", new StringSchema())
.addProperty("message", new StringSchema())
.addProperty("path", new StringSchema())))
.info(new Info()
.title("Spring Boot API")
.version("1.0.0")
.description("""
This is a comprehensive REST API for Spring Boot application.
## Authentication
The API uses JWT for authentication. To authenticate:
1. Call `/api/auth/login` with your credentials
2. Copy the `accessToken` from the response
3. Click the **Authorize** button and enter: `Bearer {token}`
## Error Handling
All errors follow a consistent format with appropriate HTTP status codes.
"""));
}
@Bean
public OpenApiCustomiser openApiCustomiser() {
return openApi -> {
// Add global headers
openApi.getPaths().forEach((path, pathItem) -> {
pathItem.readOperations().forEach(operation -> {
// Add common parameters
operation.addParametersItem(new Parameter()
.name("X-Correlation-ID")
.in("header")
.description("Correlation ID for request tracking")
.required(false)
.schema(new StringSchema()));
// Add common responses
operation.getResponses().addApiResponse("500", new ApiResponse()
.description("Internal Server Error")
.content(new Content().addMediaType("application/json",
new MediaType().schema(new Schema<ErrorResponse>()))));
});
});
};
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/api/admin/**")
.addOperationCustomizer((operation, handlerMethod) -> {
operation.addSecurityItem(new SecurityRequirement().addList("bearerAuth"));
return operation;
})
.build();
}
}
6.2 Custom Operation Filter
@Component
public class SecurityOperationFilter implements OperationCustomizer {
@Override
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
// Add security requirement for methods with PreAuthorize annotation
if (handlerMethod.hasMethodAnnotation(PreAuthorize.class)) {
PreAuthorize preAuthorize = handlerMethod.getMethodAnnotation(PreAuthorize.class);
String expression = preAuthorize.value();
// Extract roles from expression
if (expression.contains("hasRole")) {
operation.setDescription((operation.getDescription() == null ? "" : operation.getDescription() +
"\n\n**Required Roles:** " + extractRoles(expression)));
}
operation.addSecurityItem(new SecurityRequirement().addList("bearerAuth"));
}
// Add method level descriptions
if (handlerMethod.hasMethodAnnotation(Operation.class)) {
Operation methodOperation = handlerMethod.getMethodAnnotation(Operation.class);
if (operation.getDescription() == null && !methodOperation.description().isEmpty()) {
operation.setDescription(methodOperation.description());
}
}
return operation;
}
private String extractRoles(String expression) {
// Simple extraction - in real scenario, use proper parsing
if (expression.contains("ADMIN")) return "ADMIN";
if (expression.contains("USER")) return "USER";
return "AUTHENTICATED";
}
}
7. Testing Swagger Documentation
7.1 Integration Test
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {
"springdoc.api-docs.path=/api-docs",
"springdoc.swagger-ui.path=/swagger-ui.html"
})
class SwaggerIntegrationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void whenGetApiDocs_thenReturnsOpenAPIDocument() {
ResponseEntity<String> response = restTemplate.getForEntity(
"http://localhost:" + port + "/api-docs", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).contains("openapi");
}
@Test
void whenGetSwaggerUI_thenReturnsHtmlPage() {
ResponseEntity<String> response = restTemplate.getForEntity(
"http://localhost:" + port + "/swagger-ui.html", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).contains("Swagger UI");
}
@Test
void whenGetApiDocsJson_thenContainsExpectedEndpoints() {
ResponseEntity<String> response = restTemplate.getForEntity(
"http://localhost:" + port + "/api-docs", String.class);
String responseBody = response.getBody();
assertThat(responseBody).contains("/api/users");
assertThat(responseBody).contains("/api/auth");
assertThat(responseBody).contains("bearerAuth");
}
}
7.2 Controller Documentation Test
@WebMvcTest(UserController.class)
@Import({UserService.class, OpenApiConfig.class})
@AutoConfigureRestDocs(outputDir = "target/generated-snippets")
class UserControllerDocumentationTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserRepository userRepository;
@Test
void getUserByIdDocumentation() throws Exception {
User user = new User(1L, "john_doe", "john@example.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
mockMvc.perform(get("/api/users/{id}", 1L)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("john_doe"))
.andDo(document("get-user-by-id",
pathParameters(
parameterWithName("id").description("User ID")
),
responseFields(
fieldWithPath("id").description("User ID"),
fieldWithPath("username").description("Username"),
fieldWithPath("email").description("Email address"),
fieldWithPath("active").description("Active status"),
fieldWithPath("createdAt").description("Creation timestamp"),
fieldWithPath("updatedAt").description("Last update timestamp"),
fieldWithPath("roles").description("User roles")
)
));
}
}
8. Production Configuration
8.1 Environment-specific Configuration
# application-prod.yml
springdoc:
swagger-ui:
enabled: false # Disable Swagger UI in production
api-docs:
enabled: false # Disable API docs endpoint in production
# Or enable with authentication
springdoc:
swagger-ui:
enabled: true
path: /docs
operations-sorter: method
tags-sorter: alpha
api-docs:
path: /api-docs
show-actuator: false
8.2 Conditional Configuration
@Configuration
@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true")
public class SwaggerProdConfig {
@Value("${springdoc.swagger-ui.path:/swagger-ui.html}")
private String swaggerPath;
@Bean
@Profile("!prod")
public OpenAPI devOpenAPI() {
return new OpenAPI()
.info(new Info().title("Development API").version("1.0.0"));
}
@Bean
@Profile("prod")
public OpenAPI prodOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Production API")
.version("1.0.0")
.description("Production API - sensitive endpoints are hidden"))
.addSecurityItem(new SecurityRequirement().addList("apiKey"))
.components(new Components()
.addSecuritySchemes("apiKey", new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.name("X-API-KEY")));
}
}
9. Customizing Swagger UI
9.1 Custom Swagger UI Configuration
@Configuration
public class SwaggerUIConfig {
@Bean
public SwaggerUiConfigProperties swaggerUiConfigProperties() {
SwaggerUiConfigProperties properties = new SwaggerUiConfigProperties();
properties.setDocExpansion(DocExpansion.NONE);
properties.setDisplayOperationId(true);
properties.setDisplayRequestDuration(true);
properties.setOperationsSorter(OperationsSorter.METHOD);
properties.setTagsSorter(TagsSorter.ALPHA);
properties.setValidatorUrl(null); // Disable validator
properties.setPersistAuthorization(true);
return properties;
}
@Bean
public SwaggerUiOAuthProperties swaggerUiOAuthProperties() {
SwaggerUiOAuthProperties properties = new SwaggerUiOAuthProperties();
properties.setClientId("swagger-ui");
properties.setClientSecret("secret");
properties.setUsePkceWithAuthorizationCodeGrant(true);
return properties;
}
}
9.2 Custom Swagger UI Page
@Controller
public class CustomSwaggerController {
@Value("${springdoc.swagger-ui.path:/swagger-ui.html}")
private String swaggerPath;
@GetMapping("/api/docs")
public String customSwaggerPage() {
return "redirect:" + swaggerPath;
}
@GetMapping("/api")
public String apiHome() {
return """
<!DOCTYPE html>
<html>
<head>
<title>API Documentation</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.container { max-width: 800px; margin: 0 auto; }
.button { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; }
</style>
</head>
<body>
<div class="container">
<h1>API Documentation</h1>
<p>Welcome to the Spring Boot API documentation.</p>
<p>
<a href="/swagger-ui.html" class="button">Interactive API Docs</a>
<a href="/api-docs" class="button" style="background: #28a745;">OpenAPI Spec</a>
</p>
</div>
</body>
</html>
""";
}
}
This extensive guide covers Spring Boot OpenAPI Swagger integration from basic setup to advanced customization, including security, testing, and production configurations. Swagger provides excellent API documentation and testing capabilities that enhance developer experience and API consumption.
Comments (0)
No comments yet. Be the first to comment!