ADR 0076 – Champs requis du contrat API et corps vide
Accepté (2026-10-16, mode autonome ; dette technique ouverte demandée par le fil coordinateur)
ADR 0076 – Champs requis du contrat API et corps vide
Statut : Accepté (2026-10-16, mode autonome ; dette technique ouverte demandée par le fil coordinateur)
Deux dettes du contrat entre le backend et le front : le client généré ne sait pas quels champs sont toujours là, et un corps de nœud vide n’a pas de forme unique.
Contexte
- Le client Angular est généré depuis le contrat OpenAPI de springdoc. Springdoc ne lit pas la nullabilité des records Java : aucun champ de réponse n’était déclaré requis, donc tous les champs du client étaient optionnels (
id?: string,rows?: ViewRow[]). Le front se défendait partout (?? [],?.,!), sans que le compilateur puisse dire où c’était utile. - Seuls les champs de requête validés (
@NotNull,@NotBlank) étaient requis. - Un corps vide pouvait être stocké sous deux formes :
NULL(le front envoieundefinedpour un éditeur vide) ou""(un appel d’API direct). Passer de l’une à l’autre écrivait unnode.updatedsans changement visible.
Décision
Champs requis : requis sauf @Nullable
Dans le contrat, un composant de record de réponse est requis, donc non optionnel dans le client, sauf s’il est annoté @Nullable (org.jspecify.annotations.Nullable, déjà dans les dépendances de Spring).
- Sont aussi non requis :
@Schema(requiredMode = NOT_REQUIRED)ou@Schema(nullable = true), et un champ écarté du JSON quand il est nul (@JsonInclude(NON_NULL),NON_EMPTY,NON_ABSENT). @Nullableveut dire « peut être absent ou nul ». Le client le typechamp?: T, comme avant : le code du front qui testait déjà l’absence reste juste.- La règle est posée par un
OpenApiCustomizer(RequiredFieldsCustomizer) qui lit les annotations des records. Elle vaut pour les schémas atteints depuis une réponse (transitivement). Les corps de requête seuls gardent la règle de la validation : un champ absent y est permis, et les champs que la validation impose restent requis. Les noms viennent de@JsonPropertyquand il y en a (extends,final). - Une liste, une table ou un champ normalisé par le constructeur compact d’un record (
x == null ? List.of() : …) n’est donc jamais@Nullable. Les définitions YAML suivent leur méta-schéma : les clés qu’il requiert sont requises, les autres sont@Nullablesauf si le constructeur leur donne une valeur par défaut. - Un nouveau champ de réponse est requis par défaut. Si une valeur peut manquer, l’annoter
@Nullableest un acte à faire exprès ; l’oublier se voit au premier test du front qui lit ce champ comme requis, et au contrat régénéré (git diff frontend/openapi.json).
Conséquences côté front :
- Les types du client sont stricts : les
?? [],?? {}et?.devenus inutiles sont signalés par Angular (NG8102, NG8107) ; ceux des gabarits ont été retirés. - Les fixtures des tests construisent des objets partiels par un assistant
fixture<T>()(src/app/testing/fixture.ts), qui est un cast assumé : un test ne remplit que ce qu’il lit. - La palette lit une ligne de vue comme un résumé de nœud : elle prend un type étroit (
PaletteNode) au lieu d’unNodeSummaryincomplet.
Corps vide
Un corps vide ("") et un corps absent sont la même chose : NULL en base. NodeService.canonicalBody l’applique à la création et à la modification, avant la comparaison qui décide d’un événement. Passer de l’un à l’autre n’écrit donc rien. Les blancs seuls (" ") restent un corps : ce n’est pas du vide, c’est de la mise en forme. NodeView.body est @Nullable : le client lit null pour un nœud sans corps.
Conséquences
- Les données existantes ne sont pas reprises. Un corps
""déjà enregistré se lit tel quel et passe àNULLà sa prochaine modification, sans événement parasite le jour où l’on y revient avec un corps vide. - Les champs typés
T | null(nullable au sens JSON) ne sont pas distingués de « absent » dans le client. Le contrat ne dit pas lesquels sont renvoyés ennullet lesquels sont omis ; si le front en a besoin, ce sera une décision à part. - Les
Boolean/Integerd’un enregistrement de définition ne sont requis que si le constructeur compact les renseigne. - Un test unitaire (
RequiredFieldsCustomizerTest) fixe la règle, etOpenApiContractITécrit le contrat qui sert à régénérer le client (skillapi-contract).