GraphQL-deprecatie zonder versienummer
5 min lezen
Een REST-API kan /v2/ naast /v1/ uitrollen en aanroepers op hun eigen tempo laten migreren.
GraphQL heeft één schema op één endpoint, en elke client, de mobiele app op de build van vorig
jaar en het interne dashboard dat vanochtend is uitgerold, bevraagt dezelfde graph. Er is geen URL
om te forken. Een veld deprecaten betekent het ter plekke als gedeprecieerd markeren, in een
schema waar iedereen al van afhangt, wat de discipline anders maakt dan REST, ook al is het
onderliggende probleem, aanroepers vertellen dat iets gaat verdwijnen, hetzelfde als wat
API-deprecatie in het algemeen behandelt.
Hoe markeert GraphQL een veld als gedeprecieerd, als er geen versie is om te verhogen?
Met de @deprecated-directive, toegepast direct op het veld:
type Product {
price: Float @deprecated(reason: "Use priceV2 for multi-currency support.")
priceV2: Money
}
Het veld blijft bevraagbaar. Het verdwijnt niet, geeft geen 404, verandert niet van gedrag; het draagt alleen een machineleesbare notitie die de meeste GraphQL-tools, GraphiQL, Apollo Studio, schema-linters, tonen aan iedereen die het schema doorbladert of er een query tegen schrijft. Dat is het hele mechanisme. Er is geen apart deprecatie-endpoint, geen header, geen bijbehorend document dat de spec vereist, wat zowel de aantrekkingskracht als de valkuil is: de directive is makkelijk toe te voegen en makkelijk te negeren, omdat niets een client dwingt ernaar te kijken.
Ziet iemand de deprecatiereden eigenlijk wel?
Alleen wie het schema rechtstreeks gebruikt, via introspectie of een schemabewuste editor, en dat
is een kleiner publiek dan de gebruikelijke lezers van een API-changelog. Een mobiele app die zes
maanden geleden tegen een query is gebouwd, heeft die query al ingebakken in zijn binary; hij
blijft price opvragen en blijft een antwoord krijgen, gedeprecieerd of niet, totdat iemand de
app opnieuw bouwt met het nieuwe veld en een update uitrolt. De directive zegt tegen een
developer die nieuwe code schrijft dat ze het oude veld niet moet gebruiken. Het doet niets voor
de client die al is uitgerold en draait.
| Mechanisme | Wie het bereikt |
|---|---|
@deprecated-directive | Developers die het schema doorbladeren of nieuwe queries schrijven |
| CI-fouten van schema-linter | Het team dat de clientcodebase bezit, als ze er een draaien |
| Een changelog-item | Wie het ook leest, inclusief een clientteam zonder linter |
| Niets (het veld werkt gewoon) | Een al gebouwde client die het oude veld gebruikt |
Zou een gedeprecieerd veld toch een changelog-item moeten krijgen?
Ja, en dat doet meer werk dan de directive alleen, omdat een changelog mensen bereikt die de directive niet bereikt: een partnerteam dat de graph consumeert zonder het schema te doorbladeren, een client gebouwd tegen een maanden oude gecachte kopie van het schema, iedereen die het alleen zou opmerken door proza te lezen. API-changelog behandelt in het algemeen wat een item een aanroeper schuldig is; een GraphQL-item is één ding schuldig dat REST zelden hoeft te expliciteren, omdat REST-aanroepers dat afleiden uit het versienummer: of het oude veld vandaag nog werkt, nog werkt met een waarschuwing, of daadwerkelijk gestopt is met data teruggeven. De directive alleen beantwoordt daar niets van voor een lezer die het schema nooit heeft geopend.
Wanneer is het echt veilig om een veld uit het schema te verwijderen?
Alleen zodra querylogs laten zien dat niemand er meer om vraagt, wat een gebruiksvraag is, geen
kalendervraag. Een veld kan een jaar @deprecated dragen en toch dragend zijn voor een client die
nooit opnieuw is gebouwd; het op een vaste kalender verwijderen, zoals een REST-Sunset-header
vaak doet, breekt die client zonder enige waarschuwing waarop hij kan reageren, omdat GraphQL hem
niets geeft om op te reageren behalve de directive die hij nooit las. Log het gebruik op
veldniveau voordat jullie je vastleggen op een verwijderdatum, en behandel elk niet-nul
querytelling als een pauzeknop, geen aftelling.
Draagt het toevoegen van een veld hetzelfde risico als in een REST-API?
Minder, voor een nieuw veld, omdat een GraphQL-client alleen de velden ontvangt waar hij expliciet
om vraagt. priceV2 naast price toevoegen kan een bestaande query niet breken op de manier
waarop een veld toevoegen aan een REST-JSON-respons een strikte deserializer kan breken, omdat
niets de client dwingt het nieuwe veld op te vragen. Een waarde toevoegen aan een bestaande enum is
de uitzondering die het waard is in dezelfde adem te noemen: een client die exhaustief schakelt op
elke enum-waarde, wat sterk getypeerde talen aanmoedigen, breekt zodra er een nieuwe waarde
verschijnt, ongeacht of enige query erom vroeg. De veiligheid geldt alleen voor velden en
union-leden waar een client zelf voor kiest; ze geldt niet voor een gesloten verzameling die de
code van een client met de hand opsomt.
Wat heeft een GraphQL-changelog-item nodig dat een REST-item niet heeft?
De vorm van de query, niet alleen de veldnaam, omdat “het veld price is gedeprecieerd” precies
het stuk mist dat een aanroeper echt nodig heeft: welke types en welke queries het raken. Een
nuttig item noemt het type, het veld, het vervangende veld en, als jullie het kunnen genereren, de
daadwerkelijke queries in productie die nog steeds de oude vorm opvragen. Dat laatste stuk, de
deprecatiemelding koppelen aan echt gebruik, is wat REST-aanroepers gratis krijgen uit
serverlogs op een URL en GraphQL-aanroepers niet, omdat elke query hetzelfde endpoint raakt,
ongeacht wat hij opvraagt.
Kan iets anders dan een veld de @deprecated-directive dragen?
Enum-waarden, met dezelfde directive op de definitie van de waarde zelf in plaats van op het veld:
enum ShippingMethod {
STANDARD
EXPRESS
OVERNIGHT @deprecated(reason: "Use EXPRESS with priority: true instead.")
}
De spec definieert @deprecated voor precies twee plekken, een velddefinitie of een enum-waarde,
en verder niets in de stabiele release; deprecatie op argument- en input-veldniveau bestaat alleen
in latere concepttaal, niet in wat de meeste servers vandaag implementeren. Een enum-waarde die zo
is gemarkeerd, blijft een geldige waarde die een server nog kan teruggeven of accepteren, dezelfde
niet-brekende belofte die een gedeprecieerd veld doet, wat het veilig maakt om uit te brengen
voordat de waarde echt wordt verwijderd.
FAQ
Ondersteunt GraphQL iets als een Sunset-header voor een heel endpoint?
Nee, omdat er meestal maar één endpoint is. De deprecatietiming leeft op veldniveau, in de
redentekst van de @deprecated-directive en in welke changelog of migratiegids een team er ook
naast publiceert, niet in een responseheader die een client programmatisch kan lezen.
Kan een gedeprecieerd veld worden verwijderd en later opnieuw worden toegevoegd met een ander type?
Alleen als nieuwe veldnaam. Dezelfde veldnaam opnieuw introduceren met een veranderd type is
precies de breaking change die de deprecatiecyclus probeert te voorkomen; geef de vervanging zijn
eigen naam, zoals priceV2 doet, en laat de oude volledig uitsterven voordat de naam weer vrij is
voor hergebruik.
Zou de @deprecated-redentekst moeten linken naar het changelog-item?
Ja, wanneer de schematooling dat ondersteunt. Het redenveld accepteert een gewone string, en een
URL binnen die string is de kortste weg van een developer die naar introspectie-output staart
naar de vollere uitleg die een changelog-item kan geven.
Is een GraphQL-schemawijziging ooit backward compatible op een manier waarop REST dat niet is? Additieve veldwijzigingen, ja, om de reden hierboven: clients krijgen alleen wat ze opvragen. Nieuwe enum-waarden zijn de uitzondering, omdat een client die een gesloten verzameling opsomt kan breken op een waarde die hij niet verwachtte. Verwijderingen en typewijzigingen zijn precies zo breaking als hun REST-equivalenten.
De technische beweringen in dit artikel zijn niet onafhankelijk gecontroleerd. Klopt er iets niet, laat het ons weten, dan corrigeren we het.