Dépréciation GraphQL sans numéro de version
6 min de lecture
Une API REST peut publier /v2/ à côté de /v1/ et laisser chaque appelant migrer à son propre
rythme. GraphQL a un schéma sur un endpoint, et chaque client, l’app mobile sur le build de
l’année dernière et le dashboard interne déployé ce matin, interroge le même graphe. Il n’y a pas
d’URL à forker. Déprécier un champ signifie le marquer comme déprécié sur place, dans un schéma
dont tout le monde dépend déjà, ce qui rend la discipline différente de REST même si le problème
sous-jacent, dire aux appelants que quelque chose va disparaître, est le même que couvre
dépréciation d’API en général.
Comment GraphQL marque-t-il un champ comme déprécié, s’il n’y a pas de version à incrémenter ?
Avec la directive @deprecated, appliquée
directement au champ :
type Product {
price: Float @deprecated(reason: "Use priceV2 for multi-currency support.")
priceV2: Money
}
Le champ reste interrogeable. Il ne disparaît pas, ne renvoie pas de 404, ne change pas de comportement ; il porte juste une note lisible par machine que la plupart des outils GraphQL, GraphiQL, Apollo Studio, les linters de schéma, montreront à quiconque parcourt le schéma ou écrit une requête contre lui. C’est tout le mécanisme. Il n’y a pas d’endpoint de dépréciation séparé, pas d’en-tête, pas de document compagnon exigé par la spec, ce qui est à la fois l’attrait et le piège : la directive est facile à ajouter et facile à ignorer, parce que rien ne force un client à la regarder.
Est-ce que quelqu’un voit vraiment la raison de la dépréciation ?
Seulement ceux qui utilisent le schéma directement, par introspection ou un éditeur conscient du
schéma, et c’est un public plus petit que les lecteurs habituels d’un changelog d’API. Une app
mobile construite contre une requête il y a six mois a déjà cuit cette requête dans son binaire ;
elle continuera à demander price et continuera à recevoir une réponse, déprécié ou non, jusqu’à
ce que quelqu’un reconstruise l’app avec le nouveau champ et publie une mise à jour. La directive
dit à une développeuse qui écrit du nouveau code de ne pas utiliser l’ancien champ. Elle ne fait
rien pour le client déjà déployé et en fonctionnement.
| Mécanisme | Qui il atteint |
|---|---|
Directive @deprecated | Développeuses parcourant le schéma ou écrivant de nouvelles requêtes |
| Échecs CI du linter de schéma | L’équipe propriétaire du code client, si elle en fait tourner un |
| Une entrée de changelog | Quiconque la lit, y compris une équipe cliente sans linter |
| Rien (le champ fonctionne juste) | Un client déjà construit utilisant l’ancien champ |
Un champ déprécié devrait-il quand même avoir une entrée de changelog ?
Oui, et elle fait plus de travail que la directive seule, parce qu’un changelog atteint des gens que la directive ne peut pas : une équipe partenaire qui consomme le graphe sans parcourir son schéma, un client construit contre une copie en cache du schéma vieille de plusieurs mois, quiconque ne le remarquerait qu’en lisant de la prose. Changelog d’API couvre en général ce qu’une entrée doit à un appelant ; une entrée GraphQL doit une chose que REST n’a rarement besoin d’expliciter, parce que les appelants REST la déduisent du numéro de version : si l’ancien champ fonctionne encore aujourd’hui, fonctionne encore avec un avertissement, ou a effectivement cessé de renvoyer des données. La directive seule ne répond à rien de tout ça pour une lectrice qui n’a jamais ouvert le schéma.
Quand est-il vraiment sûr de retirer un champ du schéma ?
Seulement une fois que les logs de requêtes montrent que personne ne le demande plus, ce qui est
une question d’usage, pas de calendrier. Un champ peut porter @deprecated pendant un an et
rester structurant pour un client jamais reconstruit ; le retirer selon un calendrier fixe, comme
le fait souvent un Sunset REST, casse ce client sans aucun avertissement sur lequel il puisse
agir, parce que GraphQL ne lui donne rien sur quoi agir au-delà de la directive qu’il n’a jamais
lue. Journalisez l’usage au niveau du champ avant de vous engager sur une date de retrait, et
traitez tout compte de requêtes non nul comme une pause, pas un compte à rebours.
Ajouter un champ porte-t-il le même risque que dans une API REST ?
Moins, pour un nouveau champ, parce qu’un client GraphQL ne reçoit que les champs qu’il demande
explicitement. Ajouter priceV2 à côté de price ne peut pas casser une requête existante de la
façon dont ajouter un champ à une réponse JSON REST peut casser un désérialiseur strict, parce que
rien ne force le client à demander le nouveau champ. Ajouter une valeur à une énumération existante
est l’exception qui vaut la peine d’être nommée dans le même souffle : un client qui teste
exhaustivement chaque valeur d’énumération, ce que les langages fortement typés encouragent, casse
dès qu’une nouvelle valeur arrive, que ce soit demandé par une requête ou non. La sécurité ne vaut
que pour les champs et les membres d’union auxquels un client choisit d’adhérer ; elle ne vaut pas
pour un ensemble fermé qu’un client énumère à la main dans son code.
Qu’est-ce qu’une entrée de changelog GraphQL a besoin que n’a pas une entrée REST ?
La forme de la requête, pas seulement le nom du champ, parce que « le champ price est déprécié »
manque de la pièce dont un appelant a réellement besoin : quels types et quelles requêtes le
touchent. Une entrée utile nomme le type, le champ, le champ de remplacement et, si vous pouvez le
générer, les requêtes réelles en production qui demandent encore l’ancienne forme. Cette dernière
pièce, relier l’avis de dépréciation à l’usage réel, est ce que les appelants REST obtiennent
gratuitement des logs serveur sur une URL et que les appelants GraphQL n’obtiennent pas, parce que
chaque requête frappe le même endpoint quoi qu’elle demande.
Autre chose qu’un champ peut-il porter la directive @deprecated ?
Les valeurs d’énumération, en utilisant la même directive directement sur la définition de la valeur plutôt que sur celle du champ :
enum ShippingMethod {
STANDARD
EXPRESS
OVERNIGHT @deprecated(reason: "Use EXPRESS with priority: true instead.")
}
La spec définit @deprecated pour exactement deux emplacements, une définition de champ ou une
valeur d’énumération, et rien d’autre à ce jour dans la version stable ; la dépréciation au niveau
des arguments et des champs d’entrée n’existe que dans un langage de brouillon plus récent, pas
dans ce que la plupart des serveurs implémentent aujourd’hui. Une valeur d’énumération marquée
ainsi reste une valeur légale qu’un serveur peut encore renvoyer ou accepter, la même promesse de
non-rupture que fait un champ déprécié, ce qui la rend sûre à livrer avant de vraiment retirer la
valeur.
FAQ
GraphQL supporte-t-il quelque chose comme un en-tête Sunset pour tout un endpoint ?
Non, parce qu’il n’y a généralement qu’un seul endpoint. Le calendrier de dépréciation vit au
niveau du champ, dans le texte de raison de la directive @deprecated et dans quel que soit le
changelog ou guide de migration qu’une équipe publie à côté, pas dans un en-tête de réponse qu’un
client peut lire de façon programmatique.
Un champ déprécié peut-il être retiré puis réajouté plus tard avec un type différent ?
Seulement sous un nouveau nom de champ. Réintroduire le même nom de champ avec un type changé est
exactement le changement cassant que le cycle de dépréciation existe pour éviter ; donnez au
remplaçant son propre nom, comme le fait priceV2, et laissez l’ancien s’éteindre complètement
avant que le nom soit libre pour réutilisation.
Le texte de raison @deprecated devrait-il pointer vers l’entrée de changelog ?
Oui, quand l’outillage de schéma le supporte. Le champ raison accepte une simple chaîne de
caractères, et une URL à l’intérieur de cette chaîne est le chemin le plus court entre une
développeuse fixant une sortie d’introspection et l’explication plus complète qu’une entrée de
changelog peut donner.
Un changement de schéma GraphQL est-il jamais rétrocompatible d’une façon dont REST ne l’est pas ? Les changements additifs de champ, oui, pour la raison ci-dessus : les clients n’obtiennent que ce qu’ils demandent. Les nouvelles valeurs d’énumération sont l’exception, parce qu’un client qui énumère un ensemble fermé peut casser sur une valeur qu’il n’attendait pas. Les retraits et les changements de type sont exactement aussi cassants que leurs équivalents REST.
Les affirmations techniques de cet article n'ont pas été vérifiées de façon indépendante. Si quelque chose est faux, dis-le-nous et nous le corrigerons.