Modifiche alle API

I breaking change di Protobuf: cosa sopravvive sul wire

6 min di lettura

Un’API REST cambia quando cambia una forma JSON, e la maggior parte di quella forma è visibile nella risposta che potete leggere in un browser. Un’API gRPC cambia quando cambia un file .proto, e il formato binario di wire di Protocol Buffers ha regole proprie su cosa può tollerare un client che non hanno nulla a che fare con quello che dicono i nomi dei campi. Due modifiche che sembrano ugualmente piccole in un diff, rinumerare un campo contro aggiungerne uno, cadono su lati opposti di una linea che breaking changes traccia in generale: una è invisibile per ogni client esistente, l’altra li rompe tutti insieme. Distinguere i breaking change di Protobuf da quelli sicuri significa leggere le regole proprie del formato di wire, non indovinare da come si legge il cambiamento in un diff .proto.

Perché la numerazione dei campi conta più del nome del campo in Protobuf?

Perché il formato di wire codifica i campi per numero, non per nome. Il codice generato in ogni linguaggio legge e scrive quei numeri; il nome del campo email nel vostro file .proto è una comodità per gli umani che non tocca mai i byte binari inviati sulla rete. Rinominare un campo, email in email_address, è sicuro sul wire binario finché il numero resta lo stesso, il che sorprende chi viene da REST, dove una chiave JSON rinominata è esattamente il tipo di cambiamento che rompe un client. L’eccezione è proprio il caso REST: i formati ProtoJSON e testo serializzano il nome, quindi una ridenominazione rompe il transcoding JSON (un grpc-gateway, per esempio), i file in formato testo e le field mask. Rinumerare quello stesso campo, tenendo il nome ma cambiando 1 in 7, è esattamente l’opposto: invisibile in una code review che mostra solo nomi, e corrompe ogni messaggio che un client invia o riceve da quel punto in poi.

CambiamentoSicuro sul wirePerché
Rinominare un campo, mantenere il numeroBinario sì, JSON e testo noLa codifica binaria usa il numero; ProtoJSON e il formato testo usano il nome
Cambiare il numero di un campoNoOgni messaggio esistente viene ora letto come il campo sbagliato
Aggiungere un campo nuovo con numero nuovoSìI client vecchi ignorano campi che non riconoscono
Rimuovere un campo, riusare il suo vecchio numero per altroNoI dati vecchi vengono decodificati nel campo nuovo sbagliato
Cambiare il tipo di un campo in modo incompatibile (es. int32 a string)NoLa codifica di wire differisce per tipo

Cosa rende diverso rimuovere un campo rispetto a farlo in una risposta JSON REST?

Il numero diventa radioattivo. Le indicazioni ufficiali di Protobuf raccomandano di marcare come reserved il numero di un campo rimosso invece di lasciarlo riusare, perché il riuso è dove avviene il danno vero: un client che esegue ancora codice generato del mese scorso invia un messaggio usando il vecchio numero del campo per il vecchio significato, e il server, che ora si aspetta che quel numero significhi qualcos’altro, interpreta male i dati in silenzio invece di rifiutarli apertamente. REST non ha una trappola equivalente, perché una chiave JSON rimossa smette semplicemente di apparire; non c’è modo che la richiesta di un client vecchio venga silenziosamente reinterpretata come qualcos’altro. Un file .proto con reserved 4, 9, 12; in cima a un messaggio è una cicatrice permanente, ed è quello il punto: impedisce che il numero venga assegnato a un campo nuovo da qualcuno che non ne conosceva la storia.

message Invoice {
  reserved 4; // era `legacy_customer_id`, rimosso il 2026-06-01
  reserved "legacy_customer_id"; // anche il nome, per JSON/testo
  string customer_id = 5;
  string status = 6;
}

Aggiungere un campo arriva mai a richiedere una voce di changelog?

Di solito non una voce di breaking change, ma spesso sì una normale, perché “sicuro sul wire” e “invisibile per chi legge e ci tiene” sono affermazioni diverse. Aggiungere un campo a un messaggio di risposta non costa nulla strutturalmente, i client vecchi decodificano il messaggio e ignorano il campo nuovo automaticamente. Ma chi costruisce una nuova integrazione contro quel servizio non ha modo di sapere che il campo esiste a meno che qualcuno non glielo dica, perché nulla in una build riuscita o in un test superato rende visibile un nuovo campo opzionale. Changelog di API copre in generale cosa deve una voce additiva a chi legge; il motivo specifico di gRPC per scriverne una comunque è che non c’è un equivalente a navigare una risposta REST in un debugger per notare che è comparsa una chiave nuova.

In cosa differisce questo da ciò con cui deve fare i conti chi chiama GraphQL?

Le regole per le aggiunte coincidono, ma l’esposizione è diversa. Deprecazione di schema GraphQL copre un modello in cui un client riceve solo i campi che richiede esplicitamente, il che rende i cambiamenti additivi essenzialmente privi di rischio e le rimozioni l’unico pericolo reale. I client gRPC, al contrario, ricevono qualunque cosa il server invii e decodificano tutto contro la propria copia compilata dello schema; l’esposizione di un client non è limitata da ciò che ha richiesto, solo da ciò che il suo codice generato sa leggere. Questa differenza conta per scrivere i changelog: una voce GraphQL può ragionevolmente assumere che i client siano protetti dai campi che non hanno richiesto, e una voce gRPC non può assumerlo affatto.

Versionare un servizio gRPC funziona come /v1/, /v2/ di REST?

Il meccanismo è diverso anche quando l’intento è lo stesso. Cosa sono v1 e v2 in un’API REST copre il versionamento come percorsi URL paralleli che servono contratti diversi; i servizi gRPC tipicamente versionano tramite il nome del package nel file .proto stesso, payments.v1.InvoiceService diventa payments.v2.InvoiceService, il che cambia il nome di servizio completamente qualificato che un client chiama invece di un segmento URL che richiede. Entrambi gli approcci risolvono lo stesso problema, lasciare che un contratto vecchio continui a funzionare mentre ne esiste uno nuovo, ma un team che viene da un background REST spesso cerca un numero di versione nel posto sbagliato e si perde che la dichiarazione del package sta facendo quel lavoro.

Cosa dovrebbe nominare davvero una voce di changelog gRPC?

Il messaggio, il numero del campo, e se è additivo o una rimozione che richiede migrazione, in quell’ordine di importanza per chi legge e deve decidere se agire. “Aggiunto shipping_address (campo 8) a Order” dice a chi integra tutto il necessario per aggiornare il codice generato e iniziare a usarlo. “Riservato il campo 4 su Invoice, legacy_customer_id non c’è più” gli dice di controllare se qualcosa nella loro codebase legge ancora quel campo, cosa che una nota in stile REST “rimosso un campo dalla risposta” non comunica con la stessa urgenza, perché le rimozioni REST restituiscono semplicemente meno dati mentre il riuso dei campi Protobuf li corrompe attivamente.

FAQ

Il tipo di un campo può mai essere cambiato senza rompere il formato di wire? Solo entro specifici gruppi compatibili che Protobuf documenta, come allargare int32 a int64 in alcuni casi. Trattate qualsiasi cambio di tipo come rompente a meno che non l’abbiate controllato contro la tabella di compatibilità di Protobuf stessa; assumere compatibilità per analogia con il sistema di tipi di un linguaggio è come questo va storto.

Deprecare un campo in Protobuf funziona come la direttiva @deprecated di GraphQL? In modo simile: Protobuf supporta un’opzione di campo [deprecated = true] che gli strumenti possono mostrare. Nessuna delle due è imposta: un server GraphQL risponde comunque a una query su un campo deprecato, e un client protobuf ne codifica comunque uno. Entrambe sono consultive e hanno bisogno dello stesso supporto di changelog.

Rinumerare è mai sicuro se controllate ogni client? In un sistema completamente chiuso, in linea di principio, ma elimina l’intera proprietà di sicurezza per cui esistono i numeri di campo, e “controlliamo ogni client” è un’affermazione che smette di essere vera nel momento in cui una build viene messa in cache, un deploy viene ritardato, o viene aggiunto un client di cui nessuno si ricordava. Riservate il numero invece di riusarlo, anche internamente.

I servizi gRPC hanno bisogno di una pagina di changelog come un’API REST pubblica? Solo se team esterni li consumano senza leggere direttamente i diff .proto, lo stesso test “chi c’è dall’altra parte” che changelog di API interne applica in generale. Un servizio gRPC consumato solo da altri servizi dello stesso team può spesso saltare un changelog formale a favore della cronologia dei commit, perché chiunque lo legga ha già lo schema aperto.


Le affermazioni tecniche di questo articolo non sono state verificate in modo indipendente. Se qualcosa non è corretto, faccelo sapere e lo correggeremo.

Correlati su changeloop: Documentazione per sviluppatori, Strumenti di changelog a confronto

changeloop
Il team che costruisce un changelog che chiude il cerchio. I tuoi utenti chiedono, il tuo team rilascia, chi ha chiesto lo viene a sapere.