In modern API-driven development, clear, accurate, and interactive documentation is no longer a luxury—it's a necessity. OpenAPI Specification (OAS) has emerged as the standard for describing RESTful APIs, while Swagger provides the tools to implement and visualize these specifications. This article explores how to leverage both in Java applications to create professional, maintainable API documentation.
Understanding the Relationship: OpenAPI vs. Swagger
It's crucial to understand the distinction between these commonly confused terms:
OpenAPI Specification (OAS):
- A standard (formerly known as Swagger Specification)
- A language-agnostic description format for REST APIs
- Defines the structure of your API in a YAML or JSON file
- The specification that describes what your API does
Swagger:
- A set of tools that implement the OpenAPI Specification
- Includes Swagger UI, Swagger Editor, Swagger Codegen, etc.
- The implementation that helps you work with OpenAPI
Analogy: OpenAPI is like the blueprint (the specification), while Swagger is the set of tools (hammer, saw, measuring tape) that help you build and work with that blueprint.
Why OpenAPI and Swagger?
- Single Source of Truth: Your API specification becomes the contract between frontend and backend teams
- Interactive Documentation: Consumers can try API calls directly from the documentation
- Automated Client Generation: Generate client SDKs in multiple languages
- Testing: Automate API testing based on the specification
- Design-First Approach: Design your API before writing code
Spring Boot 3 and Springdoc OpenAPI
With the decline of the original Springfox Swagger library, Springdoc OpenAPI has become the de facto standard for Spring Boot applications. It automatically generates OpenAPI 3 specifications from your Spring Web MVC or Spring WebFlux applications.
Key Dependencies (Maven):
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.5.0</version> </dependency>
For Spring Boot 3: (requires Java 17+)
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.5.0</version> </dependency>
Basic Configuration
Minimal Configuration:
Just adding the dependency is enough to get started. By default, you can access:
- Swagger UI:
http://localhost:8080/swagger-ui.html - OpenAPI JSON:
http://localhost:8080/v3/api-docs
Custom Configuration in application.yml:
springdoc: api-docs: path: /api-docs swagger-ui: path: /swagger-ui.html operations-sorter: method tags-sorter: alpha packages-to-scan: com.example.api default-consumes-media-type: application/json default-produces-media-type: application/json
Java 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 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("E-Commerce API")
.version("1.0.0")
.description("Complete E-Commerce Solution API")
.contact(new Contact()
.name("API Support")
.email("[email protected]")
.url("https://example.com"))
.license(new License()
.name("Apache 2.0")
.url("https://springdoc.org"))
.termsOfService("https://example.com/terms"));
}
}
Annotating Your Controllers
Springdoc automatically scans your controllers and generates documentation. However, you can enhance it with OpenAPI annotations.
Basic Controller Example:
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.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/users")
@Tag(name = "User Management", description = "APIs for managing users")
public class UserController {
@GetMapping
@Operation(
summary = "Get all users",
description = "Retrieves a list of all registered users with pagination"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Successfully retrieved users"),
@ApiResponse(responseCode = "401", description = "Unauthorized access"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
public List<User> getAllUsers(
@Parameter(description = "Page number starting from 0")
@RequestParam(defaultValue = "0") int page,
@Parameter(description = "Number of items per page")
@RequestParam(defaultValue = "20") int size) {
// Implementation
return userService.getAllUsers(page, size);
}
@GetMapping("/{id}")
@Operation(summary = "Get user by ID")
public User getUserById(
@Parameter(description = "User ID", required = true, example = "123")
@PathVariable Long id) {
return userService.getUserById(id);
}
@PostMapping
@Operation(summary = "Create a new user")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "User created successfully"),
@ApiResponse(responseCode = "400", description = "Invalid input data")
})
public User createUser(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "User object to create",
required = true
)
@RequestBody User user) {
return userService.createUser(user);
}
@PutMapping("/{id}")
@Operation(summary = "Update an existing user")
public User updateUser(
@Parameter(description = "User ID") @PathVariable Long id,
@RequestBody User user) {
return userService.updateUser(id, user);
}
@DeleteMapping("/{id}")
@Operation(summary = "Delete a user")
@ApiResponse(responseCode = "204", description = "User deleted successfully")
public void deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
}
}
Documenting Models/Schemas
Annotating Your DTOs/Entities:
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Schema(description = "User entity representing a system user")
public class User {
@Schema(description = "Unique identifier of the user", example = "123")
private Long id;
@Schema(
description = "User's full name",
requiredMode = Schema.RequiredMode.REQUIRED,
example = "John Doe"
)
@NotBlank
@Size(min = 2, max = 100)
private String name;
@Schema(
description = "User's email address",
requiredMode = Schema.RequiredMode.REQUIRED,
example = "[email protected]"
)
@NotBlank
@Email
private String email;
@Schema(
description = "User's role in the system",
allowableValues = {"ADMIN", "USER", "MODERATOR"},
example = "USER"
)
private String role;
@Schema(description = "User account creation timestamp", example = "2024-01-15T10:30:00Z")
private LocalDateTime createdAt;
// Constructors, getters, and setters
public User() {}
public User(Long id, String name, String email, String role) {
this.id = id;
this.name = name;
this.email = email;
this.role = role;
}
// Standard getters and setters...
}
Advanced Configuration and Security
Adding JWT Authentication:
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.security.SecurityRequirement;
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 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")));
}
}
Controller Method with Security:
@PostMapping("/secure-data")
@Operation(summary = "Access secure data", security = @SecurityRequirement(name = "bearerAuth"))
public ResponseEntity<String> getSecureData() {
return ResponseEntity.ok("This is secure data");
}
Grouping APIs
For large applications, you might want to group APIs:
Configuration Class:
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiGroupConfig {
@Bean
public GroupedOpenApi userApi() {
return GroupedOpenApi.builder()
.group("users")
.pathsToMatch("/api/v1/users/**")
.build();
}
@Bean
public GroupedOpenApi productApi() {
return GroupedOpenApi.builder()
.group("products")
.pathsToMatch("/api/v1/products/**")
.build();
}
@Bean
public GroupedOpenApi orderApi() {
return GroupedOpenApi.builder()
.group("orders")
.pathsToMatch("/api/v1/orders/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/api/admin/**")
.build();
}
}
Now you can access different groups:
http://localhost:8080/swagger-ui.html(default group)http://localhost:8080/swagger-ui.html?groups=usershttp://localhost:8080/swagger-ui.html?groups=products
Customizing Swagger UI
application.properties:
# Customize Swagger UI springdoc.swagger-ui.path=/api-docs springdoc.swagger-ui.operationsSorter=alpha springdoc.swagger-ui.tagsSorter=alpha springdoc.swagger-ui.docExpansion=none springdoc.swagger-ui.filter=true springdoc.swagger-ui.tryItOutEnabled=true springdoc.swagger-ui.displayRequestDuration=true # API Docs endpoint springdoc.api-docs.path=/api-docs/json # Packages to scan springdoc.packages-to-scan=com.example.api # Cache control springdoc.cache.disabled=true
Testing Your Documentation
After setting up, test your endpoints:
- Start your Spring Boot application
- Access Swagger UI:
http://localhost:8080/swagger-ui.html - View raw OpenAPI spec:
http://localhost:8080/v3/api-docs - View specific group:
http://localhost:8080/v3/api-docs/users
Try these features in Swagger UI:
- Execute API calls directly from the browser
- View request/response schemas
- Test authentication
- Download the OpenAPI specification
Best Practices
- Be Descriptive: Use meaningful summaries and descriptions
- Document Errors: Always document possible error responses
- Use Examples: Provide examples for request bodies and parameters
- Keep Updated: Ensure documentation matches your actual API behavior
- Versioning: Include API version in your documentation
- Security: Document authentication requirements clearly
- Pagination: Document pagination parameters consistently
// Good example with comprehensive documentation
@Operation(
summary = "Search products",
description = "Search products with filtering, sorting, and pagination. " +
"Supports partial text matching on name and description."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Products retrieved successfully"),
@ApiResponse(responseCode = "400", description = "Invalid search parameters"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
Production Considerations
Disable in Production:
# application-prod.yml springdoc: api-docs: enabled: false swagger-ui: enabled: false
Or Conditionally Enable:
@Configuration
@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true")
public class OpenApiConfig {
// Configuration only loaded when enabled
}
Conclusion
OpenAPI and Swagger provide a powerful combination for API documentation in Java:
- Springdoc OpenAPI seamlessly integrates with Spring Boot
- Automatic Generation reduces maintenance overhead
- Interactive Documentation improves developer experience
- Standardized Specifications enable API-first development
By implementing comprehensive OpenAPI documentation, you create:
- Better developer experience for API consumers
- Clear contracts between frontend and backend teams
- Automated testing and client generation capabilities
- Professional, maintainable API documentation
The investment in proper API documentation pays dividends throughout the development lifecycle, making your APIs more discoverable, testable, and consumable.