Spring Boot y Spring Cloud

Documentar APIs con springdoc-openapi: OpenAPI 3 y Swagger UI en Spring Boot

Intermedio7 min de lecturaActualizado

Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés

Serás capaz de publicar una descripción OpenAPI 3 y Swagger UI para un servicio Spring Boot 3 con springdoc-openapi, completarla con anotaciones de operación, esquema y seguridad donde la inferencia no llega, dividirla en grupos, exportarla en tiempo de build para generar clientes y evitar que mienta sobre tu API.

Lo que aprenderás

  • Explicar qué describe la especificación OpenAPI y cómo springdoc-openapi la deriva de Spring MVC en tiempo de ejecución
  • Añadir springdoc-openapi a un proyecto Spring Boot 3 y configurar las rutas de api-docs y de Swagger UI
  • Documentar operaciones, respuestas, esquemas y parámetros con las anotaciones de swagger-core
  • Describir la autenticación con security schemes y documentar las respuestas de error de forma coherente
  • Exportar la descripción en tiempo de build y generar clientes tipados a partir de ella

Una API sin descripción es una API que los clientes aprenden a base de prueba y error. La especificación OpenAPI te da un contrato legible por máquinas que las herramientas convierten en documentación interactiva, bibliotecas cliente tipadas, tests de contrato y configuración de gateway. springdoc-openapi produce ese contrato a partir de una aplicación Spring Boot en tiempo de ejecución, inspeccionando los controladores, así que la descripción sigue al código en lugar de ir alejándose de él. Esta página muestra cómo configurarlo en Spring Boot 3, dónde termina la inferencia y empiezan las anotaciones, cómo describir la seguridad y los errores, y cómo convertir el fichero generado en parte de tu build en vez de una curiosidad de runtime.

Qué describe OpenAPI

Un documento OpenAPI (JSON o YAML) enumera las rutas de una API, las operaciones de cada ruta, sus parámetros, cuerpos de petición y respuestas, los esquemas de esos cuerpos, los security schemes que acepta la API y metadatos como título, versión, contacto y servidores. La versión 3.0 se publicó en 2017 y es la que espera la mayoría de las herramientas; la 3.1 alineó el schema object con JSON Schema 2020-12. springdoc 2.x emite 3.0 por defecto y 3.1 cuando se define springdoc.api-docs.version=openapi_3_1.

«Swagger» sobrevive como nombre de las herramientas que rodean la especificación: Swagger UI muestra un documento como una consola navegable, y las anotaciones de swagger-core (io.swagger.v3.oas.annotations) son la forma de añadir la información que las firmas Java no llevan. springdoc-openapi es el puente entre la visión que Spring tiene de tus controladores y esos artefactos; es un proyecto comunitario, no forma parte del portfolio de Spring, y es la opción de facto desde que Springfox dejó de mantenerse al día.

Configuración en Spring Boot 3

Una sola dependencia para aplicaciones Spring MVC:

<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
  <version>2.8.9</version>
</dependency>

Usa springdoc-openapi-starter-webflux-ui para WebFlux, y las variantes -api (springdoc-openapi-starter-webmvc-api) cuando quieras la descripción sin Swagger UI. La línea 2.x es la de Spring Boot 3; la 1.x estaba orientada a Boot 2 y a los paquetes javax.

Arranca la aplicación y tendrás:

  • /v3/api-docs — la descripción en JSON, /v3/api-docs.yaml en YAML
  • /swagger-ui/index.html — Swagger UI leyendo esa descripción

La configuración habitual está bajo 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 y packages-to-scan restringen lo que aparece; sin ellos se documenta cada handler mapping del contexto, incluido el /error de Boot. Con Spring Security, permite explícitamente /v3/api-docs/** y /swagger-ui/** (o tus rutas personalizadas); de lo contrario, la interfaz carga, pero la petición de la descripción se redirige a una página de login.

Qué se infiere y qué no

springdoc recorre el RequestMappingHandlerMapping y deriva la mayor parte del documento sin ninguna anotación:

Se infiere deAparece como
Rutas y métodos HTTP de @RequestMappingRutas y operaciones
@PathVariable, @RequestParam, @RequestHeaderParámetros, con required tomado de la anotación
Tipo del @RequestBodyEsquema del cuerpo de la petición
Tipo de retorno o ResponseEntity<T>Esquema de respuesta para el estado por defecto
@ResponseStatusCódigo de estado de la respuesta
Restricciones de Jakarta Bean Validation (@NotNull, @Size, @Min, @Pattern, @Email…)required, minLength, minimum, pattern, format del esquema
Anotaciones de Jackson (@JsonProperty, @JsonIgnore)Nombres de propiedades y omisiones
Records, enums, java.time, Optional, Page<T>Esquemas; Pageable se convierte en los query parameters page/size/sort
Métodos de @RestControllerAdvice con @ResponseStatusRespuestas adicionales en todas las operaciones

Lo que no puede saber: resúmenes y descripciones legibles por personas, qué estados de error devuelve una operación concreta, ejemplos, deprecación, qué parámetros son mutuamente excluyentes y cómo autenticarse. Ese es el trabajo de las anotaciones.

Enriquecer con anotaciones

Las anotaciones vienen con el starter. El conjunto útil es pequeño:

@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) { /* ... */ }
}

El detalle a nivel de esquema va en los DTO:

@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) {}
}

Fíjate en el reparto de tareas: las restricciones de validación ya producen required, minimum y maximum, así que @Schema añade descripciones y ejemplos en vez de repetir reglas. @Hidden elimina una operación, un parámetro o una propiedad; @Parameter(hidden = true) oculta un único parámetro, como un Principal inyectado; deprecated = true en @Operation marca un endpoint para su retirada.

Como el documento se genera a partir de beans vivos, los errores aparecen de inmediato en Swagger UI. Ejecuta la aplicación en local después de añadir anotaciones y lee el resultado como lo haría un consumidor: una @ApiResponse que anuncia un 404 en una operación cuyo handler nunca lanza nada es peor que no tener ninguna anotación.

Metadatos a nivel de aplicación

Título, versión, contacto y servidores salen de un bean OpenAPI o de propiedades:

@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")));
}

Tomar la versión de BuildProperties (generado por el plugin de Boot para Maven o Gradle) hace que la versión de la descripción avance con cada release sin que nadie tenga que acordarse de editar una cadena.

Security schemes

Una API protegida con OAuth 2.0 o un bearer token tiene que decirlo; si no, el «Try it out» de Swagger UI falla con un 401 y los clientes generados no tienen forma de adjuntar credenciales. Declara el scheme una vez y referéncialo:

@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 los flujos OAuth 2.0, type = SecuritySchemeType.OAUTH2 con flows = @OAuthFlows(authorizationCode = @OAuthFlow(authorizationUrl = ..., tokenUrl = ..., scopes = ...)) permite que Swagger UI ejecute el flujo authorization code contra tu proveedor de identidad; springdoc.swagger-ui.oauth.client-id aporta el cliente. Las operaciones públicas se excluyen con @SecurityRequirements (en plural y vacía) en el método. La integración de springdoc con Spring Security, incluida en el starter y activa siempre que Spring Security esté en el classpath, oculta los parámetros @AuthenticationPrincipal para que no aparezcan como parámetros de la petición.

Documentar los errores una sola vez

Si tu API devuelve problem details de la RFC 9457 (consulta la guía de gestión de errores), no querrás repetir las respuestas 400 y 500 en cada operación. Un bean OperationCustomizer las añade de forma global:

@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"))));
}

Hay customizers a varios niveles (OpenApiCustomizer para el documento completo, OperationCustomizer, ParameterCustomizer, PropertyCustomizer) y son la herramienta adecuada para todo lo transversal; las anotaciones quedan para lo específico de una operación.

Grupos y APIs grandes

Un único documento para un servicio con una API pública, una API interna de administración y una API para partners es difícil de leer y expone endpoints internos a consumidores externos. GroupedOpenApi lo divide; cada grupo se sirve en /v3/api-docs/{group} y se puede seleccionar en 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();
}

Lo mismo puede expresarse con las propiedades springdoc.group-configs[0].group. En un ecosistema de microservicios, un gateway puede agregar el /v3/api-docs de cada servicio en un solo Swagger UI mediante springdoc.swagger-ui.urls, de modo que los consumidores tienen un único punto de entrada sin que los servicios sepan unos de otros.

Convertir la descripción en un artefacto de build

Una descripción que solo existe en un servidor en marcha es difícil de comparar, revisar o pasar a generadores de código. El springdoc-openapi-maven-plugin arranca la aplicación durante la fase integration-test, descarga /v3/api-docs y escribe openapi.json (o YAML) en el directorio de build; el plugin de Gradle hace lo mismo con una tarea generateOpenApiDocs. Haz commit del resultado y una pull request que cambie la API mostrará el cambio en el contrato junto al código.

Ese fichero alimenta OpenAPI Generator (openapi-generator-maven-plugin) para producir clientes tipados en TypeScript, Java, Kotlin, Python y otros lenguajes, y puede publicarse en un portal de APIs o en un gateway. También es la entrada de los tests de contrato y de las herramientas de detección de breaking changes que comparan dos versiones de una descripción.

Errores habituales

Documentar el camino de error con @ResponseStatus en las clases de excepción. springdoc lee @ResponseStatus en los métodos @ExceptionHandler de las clases de advice y añade esas respuestas a todas las operaciones, puedan o no producirlas. Usa en su lugar anotaciones @ApiResponse específicas o un customizer.

Dejar paths-to-match por defecto. Actuator, /error y cualquier servlet que se te haya olvidado aparecen en el documento público.

Exponer Swagger UI en una API de producción sin autenticación. La interfaz en sí es inofensiva; el botón «Try it out» conectado a credenciales de producción no lo es. Protégela o desactívala en ese entorno.

Fiarse de los esquemas inferidos para tipos polimórficos. Una List<Payment> en la que Payment es una interfaz produce un esquema vacío. Usa @Schema(oneOf = {...}) con un discriminator, o @JsonTypeInfo y @JsonSubTypes de Jackson, que springdoc entiende.

Fijar una versión antigua de springdoc. Cada versión menor de Spring Boot suele requerir una versión de springdoc a juego; la web del proyecto publica la tabla de compatibilidad. Un desajuste se manifiesta como un documento vacío o un fallo de arranque en SpringDocConfiguration.

Describir una API que nunca existió. Las anotaciones que prometen campos, estados o parámetros que el código no implementa son la forma más rápida de que los consumidores dejen de confiar en la documentación. Revisa Swagger UI frente a respuestas reales después de cada cambio.

Ideas clave

  • OpenAPI es el contrato de API estándar legible por máquinas; springdoc-openapi lo genera en tiempo de ejecución a partir de controladores Spring MVC o WebFlux y sirve Swagger UI encima.
  • Spring Boot 3 necesita springdoc 2.x (springdoc-openapi-starter-webmvc-ui); Springfox no es una opción.
  • La inferencia cubre rutas, parámetros, cuerpos, restricciones de validación y anotaciones de Jackson; @Operation, @ApiResponse, @Schema y @Parameter añaden el contexto humano que las herramientas no pueden inferir.
  • Declara los security schemes una vez con @SecurityScheme y aplícalos globalmente para que la interfaz y los clientes generados puedan autenticarse.
  • Usa customizers para el contenido transversal, como las respuestas de error con problem details, y GroupedOpenApi para publicar documentos separados para públicos distintos.
  • Exporta la descripción en tiempo de build, haz commit de ella y genera clientes a partir de ella, para que el contrato se revise como el código.

Fuentes y lecturas recomendadas

Preguntas frecuentes

¿springdoc-openapi es lo mismo que Springfox?
No. Springfox era la biblioteca anterior, que generaba descripciones Swagger 2, y no ha publicado ninguna versión compatible con Spring Boot 3. springdoc-openapi es un proyecto comunitario distinto, pensado para OpenAPI 3; su versión 2.x está orientada a Spring Boot 3 y usa las anotaciones io.swagger.v3. Migrar consiste en cambiar la dependencia y renombrar anotaciones, como @ApiOperation por @Operation y @ApiModelProperty por @Schema.
¿Dónde se sirve Swagger UI en Spring Boot 3 con springdoc?
Por defecto en /swagger-ui/index.html; /swagger-ui.html redirige ahí. La descripción en bruto está en /v3/api-docs en JSON y en /v3/api-docs.yaml en YAML. Ambas rutas se configuran con springdoc.swagger-ui.path y springdoc.api-docs.path, y las dos deben estar permitidas en tu configuración de Spring Security si el resto de la API está protegido.
¿La descripción debe generarse desde el código o escribirse primero?
El enfoque code-first, que es el de springdoc, mantiene la descripción honesta porque se deriva de lo que realmente se ejecuta, y encaja con equipos que controlan ambos extremos. El contract-first, en el que el fichero OpenAPI se escribe a mano y las interfaces se generan a partir de él, ofrece antes a los consumidores un contrato estable y funciona mejor entre equipos. Muchos proyectos hacen code-first y versionan el fichero exportado para revisar sus cambios.
¿Puedo desactivar springdoc en producción?
Sí: springdoc.api-docs.enabled=false y springdoc.swagger-ui.enabled=false eliminan ambos endpoints, y puedes asociarlos a un perfil. Muchos equipos mantienen la descripción disponible tras autenticación, porque es útil para los consumidores, pero ocultan Swagger UI, ya que una consola interactiva apuntando a producción es una forma cómoda de cometer errores.