Protobuf breaking changes: wat overleeft op de wire
6 min lezen
Een REST-API verandert wanneer een JSON-vorm verandert, en het grootste deel van die vorm is
zichtbaar in het antwoord dat je in een browser kunt lezen. Een gRPC-API verandert wanneer een
.proto-bestand verandert, en het binaire wire-formaat van Protocol Buffers heeft eigen regels
over wat een client kan verdragen die niets te maken hebben met wat de veldnamen zeggen. Twee
wijzigingen die er in een diff even klein uitzien, een veld hernummeren tegenover er een
toevoegen, vallen aan tegenovergestelde kanten van een lijn die breaking changes
in het algemeen trekt: de ene is onzichtbaar voor elke bestaande client, de andere breekt ze
allemaal tegelijk. Protobuf breaking changes onderscheiden van veilige wijzigingen betekent de
eigen regels van het wire-formaat lezen, niet gokken op basis van hoe de wijziging eruitziet in
een .proto-diff.
Waarom telt veldnummering meer dan veldnaam in Protobuf?
Omdat het wire-formaat velden codeert op nummer, niet op naam. De gegenereerde code in elke taal
leest en schrijft die nummers; de veldnaam email in je .proto-bestand is een gemak voor mensen
dat nooit de binaire bytes raakt die over het netwerk worden verzonden. Een veld hernoemen, email
naar email_address, is veilig op de binaire wire zolang het nummer hetzelfde blijft, wat
ontwikkelaars verrast die gewend zijn aan REST, waar een hernoemde JSON-key precies het soort
verandering is dat een client breekt. De uitzondering is juist dat REST-geval: de
ProtoJSON- en tekstformaten serialiseren de naam,
dus een hernoeming breekt JSON-transcoding (bijvoorbeeld een grpc-gateway), bestanden in tekstformaat
en field masks. Datzelfde veld hernummeren, de naam behouden maar 1
veranderen in 7, is precies andersom: onzichtbaar in een codereview die alleen namen toont, en
het corrumpeert elk bericht dat een client vanaf dat punt verstuurt of ontvangt.
| Verandering | Veilig op de wire | Waarom |
|---|---|---|
| Veld hernoemen, nummer behouden | Binair ja, JSON en tekst nee | Binaire codering gebruikt het nummer; ProtoJSON en tekstformaat gebruiken de naam |
| Nummer van een veld veranderen | Nee | Elk bestaand bericht wordt nu als het verkeerde veld gelezen |
| Nieuw veld toevoegen met nieuw nummer | Ja | Oude clients negeren velden die ze niet herkennen |
| Veld verwijderen, oud nummer hergebruiken voor iets anders | Nee | Oude data wordt gedecodeerd in het verkeerde nieuwe veld |
Type van een veld incompatibel veranderen (bijv. int32 naar string) | Nee | De wire-codering verschilt per type |
Wat maakt het verwijderen van een veld anders dan in een REST JSON-antwoord?
Het nummer wordt radioactief. Protobufs eigen richtlijnen
raden aan het nummer van een verwijderd veld als reserved te markeren in plaats van het te laten hergebruiken, omdat hergebruik is waar
de echte schade gebeurt: een client die nog code van vorige maand draait stuurt een bericht met
het oude nummer van het veld voor de oude betekenis, en de server, die nu verwacht dat dat nummer
iets anders betekent, interpreteert de data stilletjes verkeerd in plaats van ze ronduit af te
wijzen. REST heeft geen equivalente valkuil, omdat een verwijderde JSON-key simpelweg stopt met
verschijnen; er is geen manier waarop het verzoek van een oude client stilzwijgend als iets anders
wordt geherinterpreteerd. Een .proto-bestand met reserved 4, 9, 12; bovenaan een bericht is een
permanent litteken, en dat is het punt: het voorkomt dat het nummer wordt toegewezen aan een
nieuw veld door iemand die de geschiedenis ervan niet kende.
message Invoice {
reserved 4; // was `legacy_customer_id`, verwijderd op 2026-06-01
reserved "legacy_customer_id"; // ook de naam, voor JSON/tekst
string customer_id = 5;
string status = 6;
}
Vereist het toevoegen van een veld ooit een changelog-item?
Meestal geen breaking-change-item, maar vaak wel een gewoon item, omdat “veilig op de wire” en “onzichtbaar voor een lezer die erom geeft” twee verschillende claims zijn. Een veld toevoegen aan een responsbericht kost structureel niets, oude clients decoderen het bericht en negeren het nieuwe veld automatisch. Maar iemand die een nieuwe integratie bouwt tegen die service heeft geen manier om te weten dat het veld bestaat tenzij iemand het haar vertelt, omdat niets aan een geslaagde build of een geslaagde test een nieuw optioneel veld zichtbaar maakt. Changelog van API behandelt in het algemeen wat een additief item aan lezers verschuldigd is; de gRPC-specifieke reden om er toch een te schrijven is dat er geen equivalent is van het doorbladeren van een REST-antwoord in een debugger om te merken dat er een nieuwe key is verschenen.
Hoe verschilt dit van wat GraphQL-aanroepers meemaken?
De regels voor toevoegingen komen overeen, maar de blootstelling verschilt. GraphQL-schemadeprecatie behandelt een model waarbij een client alleen de velden ontvangt die hij expliciet opvraagt, wat additieve wijzigingen in wezen risicovrij maakt en verwijderingen het enige echte gevaar. gRPC- clients daarentegen ontvangen wat de server ook stuurt en decoderen alles tegen hun eigen gecompileerde kopie van het schema; de blootstelling van een client is niet begrensd door wat hij opvroeg, alleen door wat zijn gegenereerde code kan lezen. Dat verschil is belangrijk voor het schrijven van changelogs: een GraphQL-item kan redelijkerwijs aannemen dat clients afgeschermd zijn van velden die ze niet opvroegen, en een gRPC-item kan dat helemaal niet aannemen.
Werkt het versioneren van een gRPC-service hetzelfde als REST’s /v1/, /v2/?
Het mechanisme is anders, zelfs als de bedoeling hetzelfde is. Wat zijn v1 en v2 in een
REST-API behandelt versionering als parallelle
URL-paden die verschillende contracten bedienen; gRPC-services versioneren doorgaans via de
packagenaam in het .proto-bestand zelf, payments.v1.InvoiceService wordt
payments.v2.InvoiceService, wat de volledig gekwalificeerde servicenaam verandert die een client
belt in plaats van een URL-segment dat hij opvraagt. Beide benaderingen lossen hetzelfde probleem
op, een oud contract laten blijven werken terwijl er een nieuw bestaat, maar een team met een
REST-achtergrond zoekt vaak op de verkeerde plek naar een versienummer en mist dat de
package-declaratie dat werk doet.
Wat zou een gRPC-changelog-item eigenlijk moeten noemen?
Het bericht, het veldnummer, en of het additief is of een verwijdering die migratie vereist, in
die volgorde van belangrijkheid voor een lezer die beslist of ze moet handelen. “shipping_address
(veld 8) toegevoegd aan Order” vertelt een integrator alles wat nodig is om gegenereerde code bij
te werken en het te gaan gebruiken. “Veld 4 gereserveerd op Invoice, legacy_customer_id is weg”
vertelt haar te controleren of er nog iets in haar codebase dat veld leest, wat een REST-achtige
notitie “een veld verwijderd uit het antwoord” niet met dezelfde urgentie communiceert, omdat
REST-verwijderingen gewoon minder data teruggeven terwijl hergebruik van Protobuf-velden ze actief
corrumpeert.
FAQ
Kan het type van een veld ooit worden veranderd zonder het wire-formaat te breken?
Alleen binnen specifieke compatibele groepen die Protobuf documenteert, zoals int32 verruimen
naar int64 in sommige gevallen. Behandel elke typewijziging als breaking tenzij je hem hebt
gecontroleerd tegen Protobufs eigen compatibiliteitstabel; compatibiliteit aannemen door analogie
met het typesysteem van een taal is hoe dit fout gaat.
Werkt het deprecaten van een veld in Protobuf zoals GraphQL’s @deprecated-directive?
Op vergelijkbare wijze: Protobuf ondersteunt een [deprecated = true]-veldoptie die tooling kan
tonen. Geen van beide wordt afgedwongen: een GraphQL-server beantwoordt nog steeds een query naar
een gedeprecieerd veld, en een protobuf-client codeert er nog steeds een. Beide zijn adviserend en
hebben dezelfde changelog-ondersteuning nodig.
Is hernummeren ooit veilig als je elke client controleert? In een volledig gesloten systeem, in principe, maar het elimineert de hele veiligheidseigenschap waarvoor veldnummers bestaan, en “we controleren elke client” is een bewering die ophoudt waar te zijn zodra een build wordt gecachet, een deploy wordt vertraagd, of er een client wordt toegevoegd die niemand zich herinnerde. Reserveer het nummer in plaats van het te hergebruiken, ook intern.
Hebben gRPC-services een changelogpagina nodig zoals een publieke REST-API?
Alleen als externe teams ze consumeren zonder .proto-diffs direct te lezen, dezelfde “wie zit er
aan de andere kant”-test die interne API-changelogs in het
algemeen toepast. Een gRPC-service die alleen wordt geconsumeerd door andere services van hetzelfde
team kan vaak een formele changelog overslaan ten gunste van de commitgeschiedenis, omdat iedereen
die het leest het schema al open heeft.
De technische beweringen in dit artikel zijn niet onafhankelijk gecontroleerd. Klopt er iets niet, laat het ons weten, dan corrigeren we het.