Files
2025-11-29 18:28:34 +08:00

17 KiB

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/json

application.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: false

OpenAPI 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 size
  • sort: 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 Docket beans, use GroupedOpenApi instead

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-scan configuration
  • springdoc.paths-to-match configuration
  • 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