Spring Boot et Spring Cloud

Documenter une API avec springdoc-openapi : OpenAPI 3 et Swagger UI sous Spring Boot

Intermédiaire8 min de lectureMis à jour

Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise

Vous saurez publier une description OpenAPI 3 et Swagger UI pour un service Spring Boot 3 avec springdoc-openapi, la compléter par des annotations d'opération, de schéma et de sécurité là où l'inférence ne suffit pas, la découper en groupes, l'exporter au moment du build pour générer des clients, et l'empêcher de mentir sur votre API.

Ce que vous allez apprendre

  • Expliquer ce que décrit la spécification OpenAPI et comment springdoc-openapi la déduit de Spring MVC à l'exécution
  • Ajouter springdoc-openapi à un projet Spring Boot 3 et configurer les chemins d'api-docs et de Swagger UI
  • Documenter opérations, réponses, schémas et paramètres avec les annotations swagger-core
  • Décrire l'authentification avec des security schemes et documenter les réponses d'erreur de façon cohérente
  • Exporter la description au moment du build et en générer des clients typés

Une API sans description est une API que ses clients apprennent par tâtonnements. La spécification OpenAPI vous fournit un contrat lisible par une machine, que des outils transforment en documentation interactive, en bibliothèques clientes typées, en tests de contrat et en configuration de gateway. springdoc-openapi produit ce contrat à partir d'une application Spring Boot, à l'exécution, en inspectant les contrôleurs : la description suit le code au lieu de s'en éloigner peu à peu. Cette page montre comment l'installer sous Spring Boot 3, où s'arrête l'inférence et où commencent les annotations, comment décrire la sécurité et les erreurs, et comment faire du fichier généré un élément de votre build plutôt qu'une curiosité d'exécution.

Ce que décrit OpenAPI

Un document OpenAPI (JSON ou YAML) recense les chemins d'une API, les opérations sur chaque chemin, leurs paramètres, corps de requête et réponses, les schémas de ces corps, les security schemes acceptés par l'API, ainsi que des métadonnées comme le titre, la version, le contact et les serveurs. La version 3.0, publiée en 2017, est celle qu'attendent la plupart des outils ; la 3.1 a aligné le schema object sur JSON Schema 2020-12. springdoc 2.x émet du 3.0 par défaut, et du 3.1 lorsque springdoc.api-docs.version=openapi_3_1 est défini.

« Swagger » survit comme nom de l'outillage autour de la spécification : Swagger UI affiche un document sous forme de console navigable, et les annotations swagger-core (io.swagger.v3.oas.annotations) servent à ajouter les informations que les signatures Java ne portent pas. springdoc-openapi fait le pont entre la vision qu'a Spring de vos contrôleurs et ces artefacts ; c'est un projet communautaire, qui ne fait pas partie du portfolio Spring, et c'est devenu le choix par défaut depuis que Springfox a cessé de suivre le rythme.

Mise en place sous Spring Boot 3

Une seule dépendance pour les applications Spring MVC :

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

Prenez springdoc-openapi-starter-webflux-ui pour WebFlux, et les variantes -api (springdoc-openapi-starter-webmvc-api) si vous voulez la description sans Swagger UI. La ligne 2.x est celle de Spring Boot 3 ; la 1.x ciblait Boot 2 et les packages javax.

Démarrez l'application et vous disposez de :

  • /v3/api-docs — la description en JSON, /v3/api-docs.yaml en YAML
  • /swagger-ui/index.html — Swagger UI, qui lit cette description

La configuration courante se trouve sous 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 et packages-to-scan restreignent ce qui apparaît ; sans eux, chaque handler mapping du contexte est documenté, y compris le /error de Boot. Avec Spring Security, autorisez explicitement /v3/api-docs/** et /swagger-ui/** (ou vos chemins personnalisés), sinon l'interface se charge mais la requête de description est redirigée vers une page de connexion.

Ce qui est déduit et ce qui ne l'est pas

springdoc parcourt le RequestMappingHandlerMapping et déduit l'essentiel du document sans la moindre annotation :

Déduit deApparaît comme
Chemins et méthodes HTTP de @RequestMappingChemins et opérations
@PathVariable, @RequestParam, @RequestHeaderParamètres, avec required tiré de l'annotation
Type du @RequestBodySchéma du corps de requête
Type de retour ou ResponseEntity<T>Schéma de réponse pour le statut par défaut
@ResponseStatusCode de statut de la réponse
Contraintes Jakarta Bean Validation (@NotNull, @Size, @Min, @Pattern, @Email…)required, minLength, minimum, pattern, format du schéma
Annotations Jackson (@JsonProperty, @JsonIgnore)Noms de propriétés et omissions
Records, enums, java.time, Optional, Page<T>Schémas ; Pageable devient les paramètres de requête page/size/sort
Méthodes de @RestControllerAdvice portant @ResponseStatusRéponses supplémentaires sur chaque opération

Ce qu'il ne peut pas savoir : les résumés et descriptions lisibles par un humain, les statuts d'erreur que renvoie une opération précise, les exemples, la dépréciation, les paramètres mutuellement exclusifs et la manière de s'authentifier. C'est le rôle des annotations.

Enrichir avec des annotations

Les annotations sont fournies avec le starter. Le sous-ensemble utile est réduit :

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

Le détail au niveau des schémas se place sur les 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) {}
}

Notez la répartition des rôles : les contraintes de validation produisent déjà required, minimum et maximum, donc @Schema ajoute descriptions et exemples au lieu de répéter les règles. @Hidden retire une opération, un paramètre ou une propriété ; @Parameter(hidden = true) masque un paramètre isolé, comme un Principal injecté ; deprecated = true sur @Operation signale un endpoint voué à disparaître.

Comme le document est généré à partir des beans réellement chargés, les erreurs apparaissent immédiatement dans Swagger UI. Lancez l'application en local après avoir ajouté des annotations et lisez le résultat comme le ferait un consommateur : une @ApiResponse qui annonce un 404 sur une opération dont le handler ne lève jamais d'exception est pire que pas d'annotation du tout.

Métadonnées au niveau de l'application

Titre, version, contact et serveurs proviennent d'un bean OpenAPI ou des propriétés :

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

Prendre la version dans BuildProperties (généré par le plugin Maven ou Gradle de Boot) garantit que la version de la description suit chaque release sans que personne ait à penser à modifier une chaîne.

Security schemes

Une API protégée par OAuth 2.0 ou un bearer token doit le dire, sinon le « Try it out » de Swagger UI échoue en 401 et les clients générés n'ont aucun moyen de transmettre des identifiants. Déclarez le scheme une fois et référencez-le :

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

Pour les flux OAuth 2.0, type = SecuritySchemeType.OAUTH2 avec flows = @OAuthFlows(authorizationCode = @OAuthFlow(authorizationUrl = ..., tokenUrl = ..., scopes = ...)) permet à Swagger UI de dérouler le flux authorization code auprès de votre fournisseur d'identité ; springdoc.swagger-ui.oauth.client-id fournit le client. Les opérations publiques s'en exemptent avec @SecurityRequirements (au pluriel, vide) sur la méthode. L'intégration Spring Security de springdoc, incluse dans le starter et active dès que Spring Security est sur le classpath, masque les paramètres @AuthenticationPrincipal pour qu'ils n'apparaissent pas comme paramètres de requête.

Documenter les erreurs une seule fois

Si votre API renvoie des problem details RFC 9457 (voir le guide sur la gestion des erreurs), vous ne voulez pas répéter les réponses 400 et 500 sur chaque opération. Un bean OperationCustomizer les ajoute globalement :

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

Il existe des customizers à plusieurs niveaux (OpenApiCustomizer pour le document entier, OperationCustomizer, ParameterCustomizer, PropertyCustomizer) : ce sont les bons outils pour tout ce qui est transverse, les annotations étant réservées aux spécificités d'une opération.

Groupes et API volumineuses

Un document unique pour un service qui expose une API publique, une API d'administration interne et une API partenaires est difficile à lire et expose des endpoints internes à des consommateurs externes. GroupedOpenApi le découpe ; chaque groupe est servi à /v3/api-docs/{group} et sélectionnable dans 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();
}

On peut obtenir la même chose avec les propriétés springdoc.group-configs[0].group. Dans un écosystème de microservices, une gateway peut agréger le /v3/api-docs de chaque service dans un seul Swagger UI via springdoc.swagger-ui.urls : les consommateurs disposent d'un point d'entrée unique sans que les services se connaissent.

Faire de la description un artefact de build

Une description qui n'existe que sur un serveur en cours d'exécution est difficile à comparer, à relire ou à fournir aux générateurs de code. Le springdoc-openapi-maven-plugin démarre l'application pendant la phase integration-test, télécharge /v3/api-docs et écrit openapi.json (ou du YAML) dans le répertoire de build ; le plugin Gradle fait de même avec une tâche generateOpenApiDocs. Commitez le résultat : une pull request qui modifie l'API montre alors le changement de contrat à côté du code.

Ce fichier alimente OpenAPI Generator (openapi-generator-maven-plugin) pour produire des clients typés en TypeScript, Java, Kotlin, Python et d'autres langages, et peut être publié sur un portail d'API ou une gateway. C'est aussi l'entrée des tests de contrat et des outils de détection de breaking changes qui comparent deux versions d'une description.

Erreurs fréquentes

Documenter le chemin d'erreur avec @ResponseStatus sur les classes d'exception. springdoc lit @ResponseStatus sur les méthodes @ExceptionHandler des classes d'advice et ajoute ces réponses à chaque opération, qu'elle puisse les produire ou non. Utilisez plutôt des annotations @ApiResponse ciblées ou un customizer.

Laisser paths-to-match par défaut. Actuator, /error et toute servlet oubliée apparaissent dans le document public.

Exposer Swagger UI sans authentification sur une API de production. L'interface elle-même est inoffensive ; le bouton « Try it out » branché sur des identifiants de production ne l'est pas. Protégez-la ou désactivez-la dans cet environnement.

Faire confiance aux schémas déduits pour les types polymorphes. Une List<Payment> où Payment est une interface produit un schéma vide. Utilisez @Schema(oneOf = {...}) avec un discriminator, ou les @JsonTypeInfo et @JsonSubTypes de Jackson, que springdoc comprend.

Figer une vieille version de springdoc. Chaque version mineure de Spring Boot exige en général une version de springdoc correspondante ; le site du projet publie la table de compatibilité. Un décalage se manifeste par un document vide ou un échec au démarrage dans SpringDocConfiguration.

Décrire une API qui n'a jamais existé. Des annotations qui promettent des champs, des statuts ou des paramètres que le code n'implémente pas sont le moyen le plus rapide de perdre la confiance des consommateurs dans la documentation. Confrontez Swagger UI aux réponses réelles après chaque modification.

Points clés à retenir

  • OpenAPI est le contrat d'API standard lisible par une machine ; springdoc-openapi le génère à l'exécution à partir des contrôleurs Spring MVC ou WebFlux et sert Swagger UI par-dessus.
  • Spring Boot 3 exige springdoc 2.x (springdoc-openapi-starter-webmvc-ui) ; Springfox n'est pas une option.
  • L'inférence couvre chemins, paramètres, corps, contraintes de validation et annotations Jackson ; @Operation, @ApiResponse, @Schema et @Parameter ajoutent le contexte humain que les outils ne peuvent pas déduire.
  • Déclarez les security schemes une fois avec @SecurityScheme et appliquez-les globalement pour que l'interface et les clients générés puissent s'authentifier.
  • Utilisez les customizers pour le contenu transverse, comme les réponses d'erreur en problem details, et GroupedOpenApi pour publier des documents distincts selon les publics.
  • Exportez la description au moment du build, commitez-la et générez-en les clients, pour que le contrat soit relu comme du code.

Sources et lectures complémentaires

Questions fréquentes

springdoc-openapi, est-ce la même chose que Springfox ?
Non. Springfox est l'ancienne bibliothèque qui générait des descriptions Swagger 2, et elle n'a jamais eu de version compatible avec Spring Boot 3. springdoc-openapi est un projet communautaire distinct, conçu pour OpenAPI 3 ; sa version 2.x cible Spring Boot 3 et utilise les annotations io.swagger.v3. Migrer revient à changer de dépendance et à renommer des annotations, par exemple @ApiOperation en @Operation et @ApiModelProperty en @Schema.
Où Swagger UI est-il servi sous Spring Boot 3 avec springdoc ?
Par défaut à /swagger-ui/index.html ; /swagger-ui.html y redirige. La description brute se trouve à /v3/api-docs en JSON et à /v3/api-docs.yaml en YAML. Les deux chemins se configurent avec springdoc.swagger-ui.path et springdoc.api-docs.path, et doivent tous deux être autorisés dans votre configuration Spring Security si le reste de l'API est protégé.
Faut-il générer la description à partir du code ou l'écrire d'abord ?
L'approche code-first, celle de springdoc, garde la description honnête puisqu'elle découle de ce qui s'exécute réellement ; elle convient aux équipes qui maîtrisent les deux côtés. L'approche contract-first, où le fichier OpenAPI est écrit à la main et les interfaces générées à partir de lui, offre plus tôt un contrat stable aux consommateurs et fonctionne mieux entre équipes. Beaucoup de projets font du code-first et versionnent le fichier exporté pour relire ses changements.
Peut-on désactiver springdoc en production ?
Oui : springdoc.api-docs.enabled=false et springdoc.swagger-ui.enabled=false suppriment les deux endpoints, et vous pouvez les lier à un profil. Beaucoup d'équipes gardent la description accessible derrière une authentification, car elle rend service aux consommateurs, mais masquent Swagger UI : une console interactive pointée sur la production est un moyen commode de faire des erreurs.