Générez votre code Java à partir d’une spec OpenAPI

Sommaire
- L’architecture orientée API
- OpenAPI : définition et rôle
- Contenu minimal d’une spec OpenAPI
- Génération côté serveur (Spring Boot)
- Évolution du contrat OpenAPI
- Génération côté client : appeler des services externes
- Conclusion & ressources
L’architecture orientée API
Une architecture orientée API (API‑first / API‑driven) considère les API comme un contrat explicite, un point d’entrée et une surface d’intégration entre composants et équipes. L’objectif est d’exposer des capacités (données, opérations) via des interfaces stables plutôt que de dépendre d’implémentations internes.
Cette approche fonctionne dans les deux sens :
– Côté serveur : votre fichier OpenAPI génère les interfaces de contrôleurs que vous implémentez
– Côté client : votre fichier OpenAPI génère des clients typés pour appeler des services externes
Dans une démarche API-first, on discute et valide d’abord la forme des échanges (endpoints, schémas, codes de réponse, règles d’erreur) avant d’écrire la logique métier correspondante.
Cette approche permet :
- d’aligner rapidement plusieurs équipes (backend, front, QA, intégrateurs)
- de lancer en parallèle mocks, clients générés et jeux de tests
- de réduire les réécritures tardives dues à des ambiguïtés initiales
- d’utiliser le compilateur comme filet (un changement de contrat casse la génération et signale où adapter le code)
- de consommer des API externes avec des clients typés et sécurisés.
OpenAPI : définition et rôle
OpenAPI est un standard déclaratif pour décrire une API HTTP : chemins, paramètres, schémas de données, codes de réponse, sécurité.
Le fichier (openapi.yaml ou .json) devient alors la source de vérité exploitable par :
- la génération de code côté serveur (interfaces, modèles)
- la génération de code côté client (clients HTTP typés)
- les outils de lint (cohérence, nomenclature)
- les mocks (simulation avant backend réel)
- les diffs contractuels (détection des breaking changes)
- la documentation interactive (Swagger UI / ReDoc).
En pratique, cela permet de produire automatiquement :
– Côté serveur : des interfaces de contrôleurs, des classes de modèles et des structures d’erreurs typées
– Côté client : des clients HTTP typés pour consommer des API externes
L’objectif central : réduire les divergences entre documentation, contrat et implémentation, que vous soyez fournisseur ou consommateur d’API.
Contenu minimal d’une spec OpenAPI
Il vous faudra inclure au minimum :
- info (title, version, description claire)
- servers (environnements)
- paths (endpoints + opérations CRUD explicites)
- components.schemas (modèles factorisés)
- components.responses (erreurs réutilisables)
- components.securitySchemes (si authentification)
- Un modèle d’erreur stable
- Des champs optionnels pour anticiper l’évolution (limite les breaking changes = renforcement ultérieur possible)
Exemple illustratif
openapi: 3.1.0
info:
title: Demo API
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/items:
get:
summary: Liste paginée
operationId: listItems
parameters:
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/Size"
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/ItemPage" }
"400": { $ref: "#/components/responses/BadRequest" }
components:
parameters:
Page:
name: page
in: query
schema: { type: integer, minimum: 0, default: 0 }
Size:
name: size
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
responses:
BadRequest:
description: Requête invalide
content:
application/problem+json:
schema: { $ref: "#/components/schemas/Problem" }
schemas:
Item:
type: object
required: [id, name]
properties:
id: { type: string }
name: { type: string, minLength: 2 }
description: { type: string, nullable: true }
ItemPage:
type: object
required: [content, page, size, totalElements]
properties:
content:
type: array
items: { $ref: "#/components/schemas/Item" }
page: { type: integer }
size: { type: integer }
totalElements: { type: integer }
Problem:
type: object
required: [type, title, status, detail]
properties:
type: { type: string }
title: { type: string }
status: { type: integer }
detail: { type: string }
instance: { type: string }
Cet extrait montre une API minimale structurée pour être immédiatement exploitable par un générateur.
Chaque section remplit un rôle précis :
- Bloc openapi et info : version du format et métadonnées (nom, version de la spec). La version d’API (1.0.0) suit une logique SemVer utilisée pour informer les consommateurs.
- servers : le point d’entrée
- paths : définition du chemin / des items avec une seule opération HTTP GET. L’attribut operationId (listItems) fournit un identifiant stable qui sera réutilisé comme nom de méthode dans le code généré (ou dans les clients)
- Paramètres paginés : plutôt que de copier les définitions page et size dans chaque opération, l’exemple les référence via $ref pointant vers components.parameters. Cela garantit une seule source de vérité si les contraintes (min/max) évoluent
- Réponses : la réponse 200 inclut un schéma ItemPage. Le choix d’une structure paginée explicite (content, page, size, totalElements) rend la sémantique claire pour les consommateurs et évite les conventions implicites. Un code 400 standardisé est factorisé via components.responses.BadRequest
- Schémas : Item illustre un objet simple avec contraintes (minLength: 2) et un champ optionnel (description nullable). ItemPage est un wrapper paginé réutilisable : forcer les champs content, page, size, totalElements dans required évite qu’une implémentation partielle passe inaperçue. Le schéma Problem prépare la gestion d’erreurs homogène (format proche d’un modèle Problem Details)
- Champs required : leur usage impose au générateur d’ajouter des annotations de validation (si configuré) ou d’informer les clients que ces propriétés seront toujours présentes
Cet exemple couvre les éléments structurants : paramètres réutilisables, pagination, modèle d’erreur, factorisation de schémas et identifiant d’opération. Il n’intègre volontairement ni sécurité ni corps de requête d’écriture pour rester focalisé sur la mécanique de référence et la lisibilité. Dans le contexte réel, nous pourrons bien sûr ajouter le POST de création, l’authentification Bearer, les filtres supplémentaires, et cela sans changer la structure initiale.
Quelques points à vérifier avant la génération
Avant d’utiliser ce fichier avec OpenAPI Generator, n’oubliez pas de :
- Valider syntaxiquement le YAML et de vous assurer que tous les schémas référencés existent (ex : swagger-cli validate)
- Le linter avec des règles internes (présence de summary, interdiction de majuscules dans les paths, etc.)
- Vérifier que les codes de statut documentés correspondent à des comportements réels attendus
Génération côté serveur (Spring Boot)
La génération côté serveur vise à produire un « squelette » contractuel compilable (interfaces, modèles, éventuellement contrôleurs fins) qui reflète fidèlement la spécification OpenAPI.
L’implémentation métier restera du code maintenu manuellement.
L’outil le plus utilisé est OpenAPI Generator (via le plugin Maven ou invocation CLI).
Les modes de génération Spring
OpenAPI Generator propose plusieurs stratégies côté Spring :
- interfaceOnly=true : génère uniquement des interfaces d’API avec les annotations Spring (@RequestMapping, @Operation, paramètres, etc.). Vous écrivez le contrôleur réel qui implémente ces interfaces (contrôle total, simple à tester)
- delegatePattern=true : génère un contrôleur « mince » qui délègue à un bean *ApiDelegate. Il ne faudra qu’écrire la classe delegate (moins de boilerplate, mais un niveau d’indirection supplémentaire)
- Génération complète (sans interfaceOnly ni delegatePattern) : produit un contrôleur complet
Cette dernière option est généralement déconseillée car elle mélange logique et artefacts régénérés (risques d’écrasement ou de divergence).
Voici un exemple minimal de configuration Maven (interfaceOnly) :
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.5.0</version>
<executions>
<execution>
<id>generate-spring-interfaces</id>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/openapi/openapi.yaml</inputSpec>
<generatorName>spring</generatorName>
<output>${project.build.directory}/generated-sources/openapi</output>
<apiPackage>com.example.api</apiPackage>
<modelPackage>com.example.api.model</modelPackage>
<configOptions>
<interfaceOnly>true</interfaceOnly>
<useTags>true</useTags>
<performBeanValidation>true</performBeanValidation>
<hideGenerationTimestamp>true</hideGenerationTimestamp>
</configOptions>
<generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests>
</configuration>
</execution>
</executions>
</plugin>
Les paramètres ci-dessous définissent à la fois la source du contrat OpenAPI et la manière dont le code Spring est généré, structuré et intégré au projet :
<inputSpec>
Chemin (absolu ou relatif au projet) vers le fichier OpenAPI.<generatorName>spring</generatorName>
Sélectionne le générateur Spring conçu par OpenAPI.
À savoir que d’autres générateurs existent selon les contextes (jaxrs-spec…).<output>
Dossier racine de l’arborescence générée. Maven ajoutera automatiquement.../src/main/javasous ce répertoire au classpath.
Placer ce répertoire soustarget/permet d’éviter de versionner le code généré<apiPackage> / <modelPackage> / <invokerPackage>: séparation logiqueapiPackage: contient les interfaces ou contrôleurs générés.modelPackage: regroupe les DTO / POJO issus des schémas.invokerPackage: contient les classes utilitaires (exceptions, Jackson config, ApiClient côté client). En génération Spring, ce package est parfois très réduit, voire vide.
<configOptions>: bloc clé influençant la forme du code.
Quelques options :<interfaceOnly>true</interfaceOnly>
Génère uniquement des interfaces (pas de classes@RestController). Cette option est idéale pour garder un contrôle total sur la couche web et éviter tout code réécrit à chaque génération. À noter que cette option est incompatible avec<delegatePattern>, il faut choisir l’un ou l’autre.<useTags>true</useTags>
Regroupe les opérations dans plusieurs interfaces basées sur lestagsdéfinis dans la spec. Sans tag, l’ensemble des éléments se retrouvent agrégés dans une seule interface Api. L’utilisation de tags cohérents doit donc être considérée comme une règle de style à part entière.<performBeanValidation>true</performBeanValidation>
Ajoute les annotations Bean Validation (Jakarta) dérivées des contraintes du schéma (required,minLength,maximum, etc.). Vérifiez que la dépendance de validation est présente (souvent apportée via spring-boot-starter-validation).<hideGenerationTimestamp>true</hideGenerationTimestamp>
Supprime le timestamp des commentaires générés afin d’éviter du bruit inutile dans les diffs Git.<useSpringBoot3>true</useSpringBoot3>
Force l’utilisation de templates compatibles Jakarta (utile si l’auto-détection ne suffit pas).<useResponseEntity>true</useResponseEntity>
Enveloppe les retours dansResponseEntity<>afin de contrôler les headers et les status dynamiquement. En modeinterfaceOnly, cela impacte directement les signatures à implémenter.<delegatePattern>true</delegatePattern>(alternative)
Active le pattern de délégation. Dans ce cas, interfaceOnly ne doit pas être utilisé.<interfaceSuffix>/<apiSuffix>
Permet d’imposer une convention de nommage explicite (par exemple UserApi vs UserController).
Dans cet exemple, nous avons fait le choix de l’option interfaceOnly, mais d’autres options existent, n’hésitez pas à lire la documentation d’OpenAPI pour en savoir plus.
Après avoir effectué ces configurations, la création des classes générées s’effectue en lançant mvn clean compile. Par défaut, les classes générées sont produites dans le répertoire target/generated-sources/openapi, que Maven ajoute automatiquement aux sources du projet grâce au plugin. Il est déconseillé de déplacer les fichiers ou de les copier dans src/main/java. Conserver une séparation claire entre les fichiers « générés » et « manuels » permet de limiter le bruit inutile sur votre dépôt, d’autant plus que votre fichier yaml est déjà versionné.
Cette organisation implique une règle simple : les fichiers générés ne doivent jamais être modifiés manuellement, puisqu’ils sont systématiquement écrasés à chaque nouvelle génération. Toute personnalisation du résultat doit donc passer par les options de configuration du plugin.
Enfin, lorsque l’option performBeanValidation=true est activée, les contraintes définies dans les schémas OpenAPI (comme required, minLength ou maximum) sont automatiquement traduites en annotations Jakarta Bean Validation (@NotNull, @Size, @Min, etc.). Pour que ces validations soient effectivement appliquées à l’exécution (notamment via Spring MVC), il est nécessaire de disposer de la dépendance jakarta.validation, généralement déjà présente dans un projet Spring Boot standard.
Les classes générées pour l’exemple donné
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>${openapi.generator.version}</version>
<executions>
<execution>
<id>generate-api</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/api/openapi.yaml</inputSpec>
<generatorName>spring</generatorName>
<output>${project.build.directory}/generated-sources/openapi</output>
<apiPackage>fr.neosoft.blog.controller</apiPackage>
<modelPackage>fr.neosoft.blog.model</modelPackage>
<invokerPackage>fr.neosoft.blog.invoker</invokerPackage>
<configOptions>
<interfaceOnly>true</interfaceOnly>
<useSpringBoot3>true</useSpringBoot3>
<useSpringBuiltInValidation>true</useSpringBuiltInValidation>
<useSpringController>true</useSpringController>
<skipDefaultInterface>true</skipDefaultInterface>
</configOptions>
<addCompileSourceRoot>true</addCompileSourceRoot>
</configuration>
</execution>
</executions>
</plugin>
L’exemple fourni plus haut ainsi que ce pom en configuration ci-dessus permettent de créer 5 classes :
– 3 classes models Item, ItemPage et Problem
– une classe interface controller DefaultApi
– une classe utilitaire ApiUtil
Quelques points d’attention
Il est important de veiller à ne pas mélanger le code généré et la logique métier, ainsi qu’à ne pas introduire de dépendance circulaire (ex : des services appelant directement des classes d’invoker générées qui elles-mêmes dépendent de code métier).
Détail de notre implémentation dans le cas du interfaceOnly
La structure de votre projet correspondra donc à :
src/
main/
resources/openapi.yaml (votre fichier OpenAPI)
java/... (votre code métier, où vous concevrez controller et service)
target/
generated-sources/openapi (par défaut)
@RestController
public class ItemApiController implements ItemsApi {
private final ItemService itemService;
public ItemApiController(ItemService itemService) {
this.itemService = itemService;
}
@Override
public ResponseEntity<ItemPage> listItems(Integer page, Integer size) {
return ResponseEntity.ok(itemService.list(page, size));
}
}
Mon
Notre contrôleur va donc étendre le contrôleur ItemsApi automatiquement généré.
Si delegatePattern=true, le contrôleur généré appellera automatiquement un bean ItemsApiDelegate à implémenter.
Évolution du contrat OpenAPI
L’évolution du contrat OpenAPI est une étape naturelle dans le cycle de vie d’une API. L’approche contractuelle garantit que les modifications soient détectées et traitées de manière systématique.
Lorsque le fichier openapi.yaml évolue :
- Mettez à jour le fichier OpenAPI avec les nouvelles définitions
- Exécutez mvn clean compile pour régénérer les interfaces et modèles
- Votre IDE signalera immédiatement les incohérences entre les interfaces générées et vos implémentations
Exemple concret d’évolution
Supposons l’ajout d’un nouvel endpoint pour récupérer un item par son ID :
/items/{id}:
get:
summary: Récupérer un item par son ID
operationId: getItemById
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/Item" }
Après régénération, l’interface ItemsApi contiendra une nouvelle méthode :
public interface ItemsApi {
ResponseEntity<ItemPage> listItems(Integer page, Integer size);
// Nouvelle méthode générée
ResponseEntity<Item> getItemById(String id);
}
Le contrôleur existant montrera alors une erreur de compilation car il n’implémente pas cette nouvelle méthode (s’il utilise l’option true) :
@RestController
public class ItemApiController implements ItemsApi {
private final ItemService itemService;
// Erreur : doit implémenter getItemById(String)
@Override
public ResponseEntity<ItemPage> listItems(Integer page, Integer size) {
return ResponseEntity.ok(itemService.list(page, size));
}
}
Les breaking changes sont donc détectés immédiatement à la compilation, l’IDE montre exactement ce qui doit être implémenté. Le fichier OpenAPI reste la source de vérité, et il nous est impossible d’oublier d’implémenter un endpoint documenté.
Le compilateur devient alors un garde-fou contractuel, garantissant que votre implémentation reste toujours alignée avec votre spécification OpenAPI.
Génération côté client : appeler des services externes
Jusqu’à présent, nous avons vu comment OpenAPI permet de générer des contrôleurs côté serveur. Mais la spécification OpenAPI peut également être utilisée pour générer des clients qui consomment des API externes. Cette approche est particulièrement utile lorsque votre application doit interagir avec des services tiers ou d’autres microservices internes.
La génération de clients offre les mêmes avantages que côté serveur : cohérence contractuelle, détection précoce des breaking changes, et réduction du code boilerplate.
La configuration de Maven étant assez proche, seules certaines options diffèrent pour s’adapter à la génération d’un client :
<execution>
<id>generate-api-to-use</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/api/api-to-use.yaml</inputSpec>
<generatorName>java</generatorName>
<output>${project.build.directory}/generated-sources/openapi</output>
<apiPackage>fr.neosoft.ext.api</apiPackage>
<modelPackage>fr.neosoft.ext.model</modelPackage>
<invokerPackage>fr.neosoft.ext.invoker</invokerPackage>
<configOptions>
<library>feign</library>
<useJakartaEe>true</useJakartaEe>
<openApiNullable>false</openApiNullable>
</configOptions>
<addCompileSourceRoot>true</addCompileSourceRoot>
</configuration>
</execution>
Illustration d’un contrat OpenAPI côté client
L’exemple ci-dessous illustre un contrat OpenAPI simple, utilisé comme point de départ pour la génération d’un client chargé de consommer une API de gestion des utilisateurs :
openapi: 3.0.3
info:
title: User Management API
description: A simple API to manage users.
version: 1.0.0
servers:
- url: https://api.example.com/v1
description: Production server
- url: http://localhost:8080/v1
description: Local development server
paths:
/users:
get:
summary: List all users
operationId: getAllUsers
responses:
'200':
description: A list of users
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
post:
summary: Create a new user
operationId: createUser
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserInput'
responses:
'201':
description: User created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/User'
/users/{id}:
get:
summary: Get a user by ID
operationId: getUserById
parameters:
- name: id
in: path
required: true
schema:
type: integer
format: int64
responses:
'200':
description: A single user
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: User not found
components:
schemas:
User:
type: object
required:
- id
- name
- email
properties:
id:
type: integer
format: int64
example: 123
name:
type: string
example: "Alice Johnson"
email:
type: string
format: email
example: "alice@example.com"
UserInput:
type: object
required:
- name
- email
properties:
name:
type: string
example: "Bob Smith"
email:
type: string
format: email
example: "bob@example.com"
Ce contrat OpenAPI décrit une API de gestion des utilisateurs exposant deux principaux points d’accès.
Le premier permet de lister l’ensemble des utilisateurs ou d’en créer un nouveau, en renvoyant des réponses en format JSON avec des objets appartenant aux schémas définis.
Le second point d’accès permet de récupérer un utilisateur spécifique à partir de son ID, en renvoyant une réponse adaptée selon si l’utilisateur est trouvé ou non.
Utilisation du client généré
Le client génère plusieurs classes dont l’interface DefaultApi et des classes models ApiResponse, User et UserInput spécifiques. Pour l’utiliser dans un service, il suffit de l’injecter et de l’utiliser :
@Service
public class UserApiClientService {
private final DefaultApi defaultApi;
public UserApiClientService(DefaultApi defaultApi) {
this.defaultApi = defaultApi;
}
public List<User> getAllUsers() {
return defaultApi.getAllUsers();
}
public User createUser(String name, String email) {
UserInput userInput = new UserInput();
userInput.setName(name);
userInput.setEmail(email);
return defaultApi.createUser(userInput);
}
public User getUserById(Long id) {
return defaultApi.getUserById(id);
}
}
Les avantages de l’approche client généré
Tous les appels sont typés, les modèles correspondent exactement au contrat. Quand l’API externe évolue : régénérez le client et corrigez les erreurs de compilation. Le code généré reflète la documentation OpenAPI.
Conclusion
OpenAPI sert de contrat unique qui aligne les équipes et alimente la génération automatisée d’interfaces et de modèles. En limitant la génération à ces artefacts structurels, vous préservez la clarté du projet et conservez la logique métier dans du code maintenu manuellement. L’objectif final est de réduire la friction et les erreurs humaines sur la surface d’échange inter-systèmes en rendant toute dérive détectable dès les premières étapes du développement.
Pour découpler le code métier, n’hésitez pas à utiliser le pattern Adapter.
Enfin, si OpenAPI est principalement associé aux services REST, les concepts sont transposables aux services SOAP via WSDL, en utilisant des plugins comme jaxws-maven-plugin ou cxf-codegen-plugin.
Pour aller plus loin
- Site officiel OpenAPI – La fondation et la spécification officielle
- Swagger Editor – Éditeur en ligne pour créer et valider vos spécifications
- OpenAPI Generator – Le générateur principal avec support de 50+ langages
- OpenAPI Generator Spring – Documentation spécifique au générateur Spring
- Pattern Adapter (Wikipedia) – Explication du pattern Adapter pour découpler le code métier
- API Design Guide – Google – Guide de conception d’API par Google
- REST API Tutorial – Ressources complètes sur les API REST
