API-wijzigingen

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.

VeranderingVeilig op de wireWaarom
Veld hernoemen, nummer behoudenBinair ja, JSON en tekst neeBinaire codering gebruikt het nummer; ProtoJSON en tekstformaat gebruiken de naam
Nummer van een veld veranderenNeeElk bestaand bericht wordt nu als het verkeerde veld gelezen
Nieuw veld toevoegen met nieuw nummerJaOude clients negeren velden die ze niet herkennen
Veld verwijderen, oud nummer hergebruiken voor iets andersNeeOude data wordt gedecodeerd in het verkeerde nieuwe veld
Type van een veld incompatibel veranderen (bijv. int32 naar string)NeeDe 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.

Meer op changeloop: Documentatie voor ontwikkelaars, Changelog-tools vergeleken

changeloop
Het team achter een changelog die de cirkel rondmaakt. Je gebruikers vragen iets, je team levert het, degene die het vroeg hoort ervan.