Spring Boot e Spring Cloud
Documentação de APIs com springdoc-openapi: OpenAPI 3 e Swagger UI no Spring Boot
Intermediário7 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
Você vai conseguir publicar uma descrição OpenAPI 3 e Swagger UI para um serviço Spring Boot 3 com springdoc-openapi, enriquecê-la com anotações de operação, schema e segurança onde a inferência não basta, dividi-la em grupos, exportá-la em tempo de build para gerar clientes e impedir que ela minta sobre a sua API.
O que você vai aprender
- Explicar o que a especificação OpenAPI descreve e como o springdoc-openapi a deriva do Spring MVC em tempo de execução
- Adicionar o springdoc-openapi a um projeto Spring Boot 3 e configurar os caminhos do api-docs e do Swagger UI
- Documentar operações, respostas, schemas e parâmetros com as anotações do swagger-core
- Descrever a autenticação com security schemes e documentar as respostas de erro de forma consistente
- Exportar a descrição em tempo de build e gerar clientes tipados a partir dela
Uma API sem descrição é uma API que os clientes aprendem na base da tentativa e erro. A especificação OpenAPI oferece um contrato legível por máquina que ferramentas transformam em documentação interativa, bibliotecas clientes tipadas, testes de contrato e configuração de gateway. O springdoc-openapi produz esse contrato a partir de uma aplicação Spring Boot em tempo de execução, inspecionando os controllers, de modo que a descrição acompanha o código em vez de se distanciar dele. Esta página mostra como configurá-lo no Spring Boot 3, onde a inferência para e as anotações começam, como descrever segurança e erros, e como tornar o arquivo gerado parte do seu build em vez de uma curiosidade de runtime.
O que o OpenAPI descreve
Um documento OpenAPI (JSON ou YAML) lista os caminhos de uma API, as operações em cada caminho, seus parâmetros, corpos de requisição e respostas, os schemas desses corpos, os security schemes que a API aceita e metadados como título, versão, contato e servidores. A versão 3.0 foi lançada em 2017 e é a que a maior parte das ferramentas espera; a 3.1 alinhou o schema object ao JSON Schema 2020-12. O springdoc 2.x emite 3.0 por padrão e 3.1 quando springdoc.api-docs.version=openapi_3_1 está definido.
"Swagger" sobrevive como nome das ferramentas em torno da especificação: o Swagger UI renderiza um documento como um console navegável, e as anotações do swagger-core (io.swagger.v3.oas.annotations) são a forma de acrescentar as informações que as assinaturas Java não carregam. O springdoc-openapi é a ponte entre a visão que o Spring tem dos seus controllers e esses artefatos; é um projeto comunitário, não faz parte do portfólio Spring, e virou a escolha padrão de fato desde que o Springfox parou de acompanhar a evolução.
Configuração no Spring Boot 3
Uma única dependência para aplicações Spring MVC:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.9</version>
</dependency>
Use springdoc-openapi-starter-webflux-ui para WebFlux e as variantes -api (springdoc-openapi-starter-webmvc-api) quando quiser a descrição sem o Swagger UI. A linha 2.x é a do Spring Boot 3; a 1.x era voltada ao Boot 2 e aos pacotes javax.
Suba a aplicação e você terá:
/v3/api-docs— a descrição em JSON,/v3/api-docs.yamlem YAML/swagger-ui/index.html— o Swagger UI lendo essa descrição
A configuração mais comum fica em springdoc.*:
springdoc:
api-docs:
path: /openapi # default /v3/api-docs
swagger-ui:
path: /docs # default /swagger-ui.html
operations-sorter: method
tags-sorter: alpha
try-it-out-enabled: false
paths-to-match: /api/** # ignore health checks, error pages, etc.
show-actuator: false
default-produces-media-type: application/json
paths-to-match e packages-to-scan restringem o que aparece; sem eles, todo handler mapping do contexto é documentado, inclusive o /error do Boot. Com Spring Security, libere explicitamente /v3/api-docs/** e /swagger-ui/** (ou seus caminhos personalizados); caso contrário a interface carrega, mas a requisição da descrição é redirecionada para uma página de login.
O que é inferido e o que não é
O springdoc percorre o RequestMappingHandlerMapping e deriva a maior parte do documento sem nenhuma anotação:
| Inferido de | Aparece como |
|---|---|
Caminhos e métodos HTTP de @RequestMapping | Caminhos e operações |
@PathVariable, @RequestParam, @RequestHeader | Parâmetros, com required vindo da anotação |
Tipo do @RequestBody | Schema do corpo da requisição |
Tipo de retorno ou ResponseEntity<T> | Schema da resposta para o status padrão |
@ResponseStatus | Código de status da resposta |
Restrições do Jakarta Bean Validation (@NotNull, @Size, @Min, @Pattern, @Email…) | required, minLength, minimum, pattern, format do schema |
Anotações do Jackson (@JsonProperty, @JsonIgnore) | Nomes de propriedades e omissões |
Records, enums, java.time, Optional, Page<T> | Schemas; Pageable vira os query parameters page/size/sort |
Métodos de @RestControllerAdvice com @ResponseStatus | Respostas adicionais em todas as operações |
O que ele não tem como saber: resumos e descrições legíveis por pessoas, quais status de erro uma operação específica retorna, exemplos, depreciação, quais parâmetros são mutuamente exclusivos e como se autenticar. Esse é o papel das anotações.
Enriquecendo com anotações
As anotações vêm junto com o starter. O conjunto útil é pequeno:
@RestController
@RequestMapping("/api/orders")
@Tag(name = "Orders", description = "Create and track customer orders")
class OrderController {
@Operation(
summary = "Get an order by id",
description = "Returns the order with its lines and current status. Cancelled orders are still returned."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "The order"),
@ApiResponse(responseCode = "404", description = "No order with that id",
content = @Content(mediaType = "application/problem+json",
schema = @Schema(implementation = ProblemDetail.class)))
})
@GetMapping("/{id}")
OrderResponse get(
@Parameter(description = "Order identifier", example = "10042")
@PathVariable long id) {
return OrderResponse.from(orders.get(id));
}
@Operation(summary = "Place an order")
@ApiResponse(responseCode = "201", description = "Order created",
headers = @Header(name = "Location", description = "URL of the new order"))
@PostMapping
ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest request) { /* ... */ }
}
O detalhamento no nível de schema fica nos DTOs:
@Schema(description = "A request to place an order")
public record CreateOrderRequest(
@Schema(description = "Identifier of an existing customer", example = "42")
@NotNull Long customerId,
@ArraySchema(arraySchema = @Schema(description = "At least one line"))
@NotEmpty List<@Valid OrderLine> lines) {
public record OrderLine(
@Schema(example = "ABC-12345", pattern = "[A-Z]{3}-\\d{5}") @Sku String sku,
@Schema(minimum = "1", maximum = "999") @Min(1) @Max(999) int quantity) {}
}
Repare na divisão de tarefas: as restrições de validação já produzem required, minimum e maximum, então o @Schema acrescenta descrições e exemplos em vez de repetir regras. @Hidden remove uma operação, um parâmetro ou uma propriedade; @Parameter(hidden = true) esconde um único parâmetro, como um Principal injetado; deprecated = true no @Operation marca um endpoint para ser aposentado.
Como o documento é gerado a partir de beans vivos, os erros aparecem na hora no Swagger UI. Rode a aplicação localmente depois de adicionar anotações e leia o resultado como um consumidor leria: um @ApiResponse que anuncia 404 numa operação cujo handler nunca lança exceção é pior do que nenhuma anotação.
Metadados da aplicação
Título, versão, contato e servidores vêm de um bean OpenAPI ou de propriedades:
@Bean
OpenAPI shopApi(BuildProperties build) {
return new OpenAPI()
.info(new Info()
.title("Shop API")
.version(build.getVersion()) // from build-info, not a hard-coded string
.description("Orders, customers and catalogue for the shop front end")
.license(new License().name("Proprietary")))
.servers(List.of(new Server().url("https://api.example.com")));
}
Pegar a versão de BuildProperties (gerado pelo plugin do Boot para Maven ou Gradle) faz a versão da descrição avançar a cada release sem que alguém precise lembrar de editar uma string.
Security schemes
Uma API protegida por OAuth 2.0 ou por bearer token precisa declarar isso; do contrário, o "Try it out" do Swagger UI falha com 401 e os clientes gerados não têm como anexar credenciais. Declare o scheme uma vez e faça referência a ele:
@Configuration
@SecurityScheme(
name = "bearer-jwt",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT",
description = "Access token issued by the identity provider"
)
@OpenAPIDefinition(security = @SecurityRequirement(name = "bearer-jwt")) // applies to every operation
class OpenApiSecurityConfig {}
Para os fluxos OAuth 2.0, type = SecuritySchemeType.OAUTH2 com flows = @OAuthFlows(authorizationCode = @OAuthFlow(authorizationUrl = ..., tokenUrl = ..., scopes = ...)) permite que o Swagger UI execute o fluxo authorization code contra o seu provedor de identidade; springdoc.swagger-ui.oauth.client-id informa o client. Operações públicas ficam de fora com @SecurityRequirements (no plural, vazio) no método. A integração do springdoc com o Spring Security, incluída no starter e ativa sempre que o Spring Security está no classpath, esconde os parâmetros @AuthenticationPrincipal para que não apareçam como parâmetros da requisição.
Documentando os erros uma única vez
Se a sua API retorna problem details da RFC 9457 (veja o guia de tratamento de erros), você não vai querer repetir as respostas 400 e 500 em cada operação. Um bean OperationCustomizer as adiciona globalmente:
@Bean
OperationCustomizer commonErrorResponses() {
return (operation, handlerMethod) -> {
ApiResponses responses = operation.getResponses();
responses.addApiResponse("400", problemResponse("Validation failed or malformed request"));
responses.addApiResponse("500", problemResponse("Unexpected server error"));
return operation;
};
}
private static io.swagger.v3.oas.models.responses.ApiResponse problemResponse(String description) {
return new io.swagger.v3.oas.models.responses.ApiResponse()
.description(description)
.content(new Content().addMediaType("application/problem+json",
new MediaType().schema(new Schema<>().$ref("#/components/schemas/ProblemDetail"))));
}
Existem customizers em vários níveis (OpenApiCustomizer para o documento inteiro, OperationCustomizer, ParameterCustomizer, PropertyCustomizer), e eles são a ferramenta certa para tudo o que é transversal; as anotações ficam para as particularidades de uma operação.
Grupos e APIs grandes
Um único documento para um serviço com uma API pública, uma API interna de administração e uma API para parceiros é difícil de ler e vaza endpoints internos para consumidores externos. O GroupedOpenApi faz a divisão; cada grupo é servido em /v3/api-docs/{group} e pode ser selecionado no Swagger UI:
@Bean
GroupedOpenApi publicApi() {
return GroupedOpenApi.builder().group("public").pathsToMatch("/api/public/**").build();
}
@Bean
GroupedOpenApi adminApi() {
return GroupedOpenApi.builder().group("admin").pathsToMatch("/api/admin/**")
.addOperationCustomizer((op, method) -> op.addTagsItem("admin")).build();
}
O mesmo pode ser expresso com as propriedades springdoc.group-configs[0].group. Num cenário de microsserviços, um gateway pode agregar o /v3/api-docs de cada serviço num único Swagger UI por meio de springdoc.swagger-ui.urls, e os consumidores ganham um só ponto de entrada sem que os serviços precisem conhecer uns aos outros.
Transformando a descrição em artefato de build
Uma descrição que só existe num servidor em execução é difícil de comparar, revisar ou entregar a geradores de código. O springdoc-openapi-maven-plugin sobe a aplicação durante a fase integration-test, baixa /v3/api-docs e grava openapi.json (ou YAML) no diretório de build; o plugin do Gradle faz o mesmo com uma task generateOpenApiDocs. Faça commit do resultado, e um pull request que altera a API passa a mostrar a mudança no contrato ao lado do código.
Esse arquivo alimenta o OpenAPI Generator (openapi-generator-maven-plugin) para produzir clientes tipados em TypeScript, Java, Kotlin, Python e outras linguagens, e pode ser publicado num portal de APIs ou num gateway. Ele também é a entrada dos testes de contrato e das ferramentas de detecção de breaking changes que comparam duas versões de uma descrição.
Erros comuns
Documentar o caminho de erro com @ResponseStatus nas classes de exceção. O springdoc lê @ResponseStatus nos métodos @ExceptionHandler das classes de advice e adiciona essas respostas a todas as operações, independentemente de a operação poder gerá-las ou não. Em vez disso, use anotações @ApiResponse pontuais ou um customizer.
Deixar o paths-to-match padrão. Actuator, /error e qualquer servlet esquecido aparecem no documento público.
Expor o Swagger UI numa API de produção sem autenticação. A interface em si é inofensiva; o botão "Try it out" ligado a credenciais de produção não é. Proteja-a ou desative-a nesse ambiente.
Confiar nos schemas inferidos para tipos polimórficos. Uma List<Payment> em que Payment é uma interface produz um schema vazio. Use @Schema(oneOf = {...}) com um discriminator, ou @JsonTypeInfo e @JsonSubTypes do Jackson, que o springdoc entende.
Travar uma versão antiga do springdoc. Cada versão minor do Spring Boot costuma exigir uma versão correspondente do springdoc; o site do projeto traz a tabela de compatibilidade. Uma incompatibilidade aparece como um documento vazio ou uma falha na inicialização em SpringDocConfiguration.
Descrever uma API que nunca existiu. Anotações que prometem campos, status ou parâmetros que o código não implementa são o caminho mais rápido para os consumidores perderem a confiança na documentação. Confira o Swagger UI contra respostas reais depois de cada mudança.
Principais pontos
- OpenAPI é o contrato de API padrão legível por máquina; o springdoc-openapi o gera em tempo de execução a partir de controllers Spring MVC ou WebFlux e serve o Swagger UI por cima.
- O Spring Boot 3 exige o springdoc 2.x (
springdoc-openapi-starter-webmvc-ui); o Springfox não é uma opção. - A inferência cobre caminhos, parâmetros, corpos, restrições de validação e anotações do Jackson;
@Operation,@ApiResponse,@Schemae@Parameteracrescentam o contexto humano que as ferramentas não conseguem inferir. - Declare os security schemes uma vez com
@SecuritySchemee aplique-os globalmente para que a interface e os clientes gerados consigam se autenticar. - Use customizers para conteúdo transversal, como respostas de erro com problem details, e
GroupedOpenApipara publicar documentos separados para públicos diferentes. - Exporte a descrição em tempo de build, faça commit dela e gere clientes a partir dela, para que o contrato seja revisado como código.
Fontes e leituras complementares
Perguntas frequentes
- O springdoc-openapi é a mesma coisa que o Springfox?
- Não. O Springfox era a biblioteca mais antiga, que gerava descrições Swagger 2, e não teve nenhuma versão compatível com o Spring Boot 3. O springdoc-openapi é um projeto comunitário separado, feito para o OpenAPI 3; a versão 2.x é voltada ao Spring Boot 3 e usa as anotações io.swagger.v3. Migrar significa trocar a dependência e renomear anotações, como @ApiOperation para @Operation e @ApiModelProperty para @Schema.
- Onde o Swagger UI fica disponível no Spring Boot 3 com springdoc?
- Por padrão em /swagger-ui/index.html; /swagger-ui.html redireciona para lá. A descrição bruta fica em /v3/api-docs em JSON e em /v3/api-docs.yaml em YAML. Os dois caminhos são configuráveis com springdoc.swagger-ui.path e springdoc.api-docs.path, e ambos precisam ser liberados na sua configuração do Spring Security se o resto da API estiver protegido.
- A descrição deve ser gerada a partir do código ou escrita primeiro?
- O code-first, que é o que o springdoc faz, mantém a descrição honesta porque ela é derivada do que de fato roda, e combina com times que são donos das duas pontas. O contract-first, em que o arquivo OpenAPI é escrito à mão e as interfaces são geradas a partir dele, entrega um contrato estável aos consumidores mais cedo e funciona melhor entre times. Muitos projetos fazem code-first e versionam o arquivo exportado para revisar as mudanças.
- Posso desligar o springdoc em produção?
- Sim: springdoc.api-docs.enabled=false e springdoc.swagger-ui.enabled=false removem os dois endpoints, e você pode vinculá-los a um profile. Muitos times mantêm a descrição disponível atrás de autenticação, porque ela é útil para os consumidores, mas escondem o Swagger UI, já que um console interativo apontado para produção é um jeito prático de cometer erros.