Deprecazione di GraphQL senza numero di versione
6 min di lettura
Un’API REST può pubblicare /v2/ accanto a /v1/ e lasciare che chi chiama si muova al proprio
ritmo. GraphQL ha uno schema a un endpoint, e ogni client, l’app mobile con la build dell’anno
scorso e la dashboard interna distribuita stamattina, interroga lo stesso grafo. Non c’è URL da
biforcare. Deprecare un campo significa marcarlo come deprecato sul posto, in uno schema da cui
tutti già dipendono, il che rende la disciplina diversa da REST anche se il problema di fondo,
dire a chi chiama che qualcosa sta per sparire, è lo stesso che deprecazione di API
copre in generale.
Come marca GraphQL un campo come deprecato, se non c’è versione da incrementare?
Con la direttiva @deprecated, applicata direttamente al campo:
type Product {
price: Float @deprecated(reason: "Use priceV2 for multi-currency support.")
priceV2: Money
}
Il campo resta interrogabile. Non sparisce, non restituisce un 404, non cambia comportamento; porta solo una nota leggibile da macchina che la maggior parte degli strumenti GraphQL, GraphiQL, Apollo Studio, i linter di schema, mostrerà a chiunque navighi lo schema o scriva una query contro di esso. Questo è l’intero meccanismo. Non c’è un endpoint di deprecazione separato, nessun header, nessun documento aggiuntivo richiesto dalla specifica, il che è sia l’attrattiva sia la trappola: la direttiva è facile da aggiungere e facile da ignorare, perché niente costringe un client a guardarla.
Qualcuno vede davvero il motivo della deprecazione?
Solo chi usa lo schema direttamente, tramite introspezione o un editor consapevole dello schema, e
questo è un pubblico più piccolo dei lettori abituali di un changelog di API. Un’app mobile
costruita contro una query sei mesi fa ha già quella query incorporata nel suo binario; continuerà
a chiedere price e continuerà a ricevere una risposta, deprecato o no, finché qualcuno non
ricostruisce l’app con il nuovo campo e rilascia un aggiornamento. La direttiva dice a chi scrive
codice nuovo di non usare il campo vecchio. Non fa nulla per il client già distribuito e in
esecuzione.
| Meccanismo | Chi raggiunge |
|---|---|
Direttiva @deprecated | Sviluppatrici che navigano lo schema o scrivono nuove query |
| Fallimenti CI del linter di schema | Il team proprietario del codice client, se ne gestisce uno |
| Una voce di changelog | Chiunque la legga, incluso un team client senza linter |
| Niente (il campo funziona e basta) | Un client già costruito che usa il campo vecchio |
Un campo deprecato dovrebbe comunque avere una voce di changelog?
Sì, e fa più lavoro della sola direttiva, perché un changelog raggiunge persone che la direttiva non può: un team partner che consuma il grafo senza navigarne lo schema, un client costruito contro una copia cachata dello schema di mesi fa, chiunque se ne accorgerebbe solo leggendo prosa. Changelog di API copre in generale cosa deve una voce a chi chiama; una voce GraphQL deve una cosa che REST raramente deve esplicitare, perché chi chiama REST la deduce dal numero di versione: se il campo vecchio funziona ancora oggi, funziona ancora con un avviso, o ha effettivamente smesso di restituire dati. La direttiva da sola non risponde a nulla di questo per una lettrice che non ha mai aperto lo schema.
Quando è davvero sicuro rimuovere un campo dallo schema?
Solo quando i log delle query mostrano che nessuno lo chiede più, il che è una domanda di
utilizzo, non di calendario. Un campo può portare @deprecated per un anno ed essere ancora
portante per un client mai ricostruito; rimuoverlo su un calendario fisso, come spesso fa un
Sunset REST, rompe quel client senza alcun avviso su cui possa agire, perché GraphQL non gli dà
nulla su cui agire oltre alla direttiva che non ha mai letto. Registrate l’utilizzo a livello di
campo prima di impegnarvi su una data di rimozione, e trattate qualsiasi conteggio di query
diverso da zero come una pausa, non un conto alla rovescia.
Aggiungere un campo comporta lo stesso rischio che in un’API REST?
Meno, per un nuovo campo, perché un client GraphQL riceve solo i campi che chiede esplicitamente.
Aggiungere priceV2 accanto a price non può rompere una query esistente nel modo in cui
aggiungere un campo a una risposta JSON REST può rompere un deserializzatore rigido, dato che
niente costringe il client a richiedere il campo nuovo. Aggiungere un valore a un enum esistente è
l’eccezione che vale la pena nominare nello stesso respiro: un client che fa uno switch esaustivo
su ogni valore dell’enum, cosa che i linguaggi fortemente tipizzati incoraggiano, si rompe nel
momento in cui arriva un valore nuovo, indipendentemente dal fatto che una query lo chiedesse. La
sicurezza vale solo per i campi e i membri di union su cui un client sceglie di entrare; non vale
per un insieme chiuso che il codice di un client enumera a mano.
Cosa serve a una voce di changelog GraphQL che non serve a una REST?
La forma della query, non solo il nome del campo, perché “il campo price è deprecato” manca del
pezzo di cui chi chiama ha davvero bisogno: quali tipi e quali query lo toccano. Una voce utile
nomina il tipo, il campo, il campo sostitutivo e, se potete generarlo, le query reali in
produzione che ancora richiedono la forma vecchia. Quest’ultimo pezzo, legare l’avviso di
deprecazione all’utilizzo reale, è ciò che chi chiama REST ottiene gratis dai log del server su un
URL e chi chiama GraphQL no, perché ogni query colpisce lo stesso endpoint indipendentemente da
cosa chiede.
Qualcos’altro oltre a un campo può portare la direttiva @deprecated?
I valori enum, usando la stessa direttiva sulla definizione del valore stesso invece che su quella del campo:
enum ShippingMethod {
STANDARD
EXPRESS
OVERNIGHT @deprecated(reason: "Use EXPRESS with priority: true instead.")
}
La specifica definisce @deprecated per esattamente due posizioni, la definizione di un campo o
un valore enum, e nient’altro nella release stabile; la deprecazione a livello di argomento e di
campo di input esiste solo nel linguaggio delle bozze successive, non in quello che la maggior
parte dei server implementa oggi. Un valore enum marcato in questo modo resta un valore legale che
un server può ancora restituire o accettare, la stessa promessa di non-rottura che fa un campo
deprecato, ed è quello che lo rende sicuro da pubblicare prima di rimuovere davvero il valore.
FAQ
GraphQL supporta qualcosa come un header Sunset per un intero endpoint?
No, perché di solito c’è un solo endpoint. Il tempismo della deprecazione vive a livello di campo,
nel testo del motivo della direttiva @deprecated e in qualunque changelog o guida alla
migrazione un team pubblichi accanto ad esso, non in un header di risposta che un client possa
leggere programmaticamente.
Un campo deprecato può essere rimosso e riaggiunto in seguito con un tipo diverso?
Solo come nuovo nome di campo. Reintrodurre lo stesso nome di campo con un tipo cambiato è
esattamente il breaking change che il ciclo di deprecazione esiste per evitare; date al
sostituto un proprio nome, come fa priceV2, e lasciate che il vecchio si estingua completamente
prima che il nome sia libero per il riutilizzo.
Il testo del motivo @deprecated dovrebbe linkare alla voce di changelog?
Sì, quando gli strumenti dello schema lo supportano. Il campo motivo accetta una stringa semplice,
e un URL dentro quella stringa è il percorso più breve da una sviluppatrice che fissa l’output
dell’introspezione alla spiegazione più completa che una voce di changelog può dare.
Un cambiamento di schema GraphQL è mai retrocompatibile in un modo in cui REST non lo è? I cambiamenti additivi di campo, sì, per il motivo sopra: i client ottengono solo ciò che chiedono. I nuovi valori enum sono l’eccezione, perché un client che enumera un insieme chiuso può rompersi su un valore che non si aspettava. Le rimozioni e i cambiamenti di tipo sono esattamente rompenti quanto i loro equivalenti REST.
Le affermazioni tecniche di questo articolo non sono state verificate in modo indipendente. Se qualcosa non è corretto, faccelo sapere e lo correggeremo.