Protobuf breaking changes: o que sobrevive no fio
6 min de leitura
Uma API REST muda quando a forma do JSON muda, e a maior parte dessa forma é visível na resposta
que dá para ler no navegador. Uma API gRPC muda quando o arquivo .proto muda, e o formato binário
no fio do Protocol Buffers tem suas próprias regras sobre o que um cliente aguenta, que não têm
nada a ver com o que os nomes dos campos dizem. Duas mudanças que parecem igualmente pequenas no
diff, renumerar um campo e adicionar um campo novo, caem em lados opostos da linha que
breaking changes traça em geral: uma é invisível para todo
cliente existente, a outra quebra todos eles de uma vez. Diferenciar breaking changes de protobuf
das mudanças seguras significa ler as próprias regras do formato no fio, não adivinhar pela
aparência da mudança em um diff de .proto.
Por que o número do campo importa mais que o nome dele no Protobuf?
Porque o formato no fio codifica campos por número, não por nome. O código gerado em cada
linguagem lê e escreve esses números; o nome do campo email no seu arquivo .proto é uma
conveniência humana que nunca toca nos bytes binários enviados pela rede. Renomear um campo, de
email para email_address, é seguro no fio binário enquanto o número permanecer o mesmo, o
que surpreende engenheiras acostumadas com REST, onde uma chave JSON renomeada é exatamente o tipo
de mudança que quebra um cliente. A exceção é justamente o caso REST: os
formatos ProtoJSON e texto serializam o nome, então
uma renomeação quebra o transcoding JSON (um grpc-gateway, por exemplo), arquivos em formato texto e
field masks. Renumerar o mesmo campo, mantendo o nome mas trocando 1 por
7, é o oposto: invisível em um code review que só mostra nomes, e quebra toda mensagem que o
cliente enviar ou receber a partir daquele momento.
| Mudança | Segura no fio | Por quê |
|---|---|---|
| Renomear campo, manter o número | Binário sim, JSON e texto não | A codificação binária usa o número; ProtoJSON e o formato texto usam o nome |
| Mudar o número do campo | Não | Toda mensagem existente agora é lida como um campo diferente |
| Adicionar campo novo com número novo | Sim | Clientes antigos ignoram campos que não conhecem |
| Remover um campo, reusar seu número antigo para outra coisa | Não | Dados antigos são decodificados no campo novo errado |
Mudar o tipo de um campo de forma incompatível (ex: int32 para string) | Não | A codificação no fio difere por tipo |
Por que remover um campo é diferente de fazer o mesmo em uma resposta REST JSON?
Porque o número se torna radioativo. O próprio guia do Protobuf recomenda marcar o
número de um campo apagado como reserved, em vez de permitir que seja reusado, porque é
justamente no reúso que o dano real acontece: um cliente ainda rodando código gerado com meses de
idade envia uma mensagem usando o número de campo antigo para um valor antigo, e o servidor, que
agora espera que esse número signifique outra coisa, interpreta os dados errado silenciosamente em
vez de rejeitá-los diretamente. REST não tem uma armadilha equivalente, porque uma chave JSON
apagada simplesmente para de aparecer; não existe forma de uma requisição de um cliente antigo ser
reinterpretada silenciosamente como outra coisa. Um arquivo .proto com reserved 4, 9, 12; no
topo de uma mensagem é uma cicatriz permanente, e esse é o ponto: ele impede que aquele número
acabe indo para um campo novo nas mãos de alguém que não conhece a história dele.
message Invoice {
reserved 4; // era `legacy_customer_id`, removido em 2026-06-01
reserved "legacy_customer_id"; // o nome também, para JSON/texto
string customer_id = 5;
string status = 6;
}
Adicionar um campo sequer precisa de uma entrada no changelog?
Geralmente não uma entrada de breaking change, mas frequentemente uma entrada comum, porque “seguro no fio” e “visível para uma leitora que se importa” são duas afirmações diferentes. Adicionar um campo a uma mensagem de resposta é estruturalmente de graça, clientes antigos decodificam a mensagem e ignoram automaticamente o campo novo. Mas quem está construindo uma integração nova contra esse serviço não tem como saber que o campo existe a menos que alguém diga, porque nem um build bem-sucedido nem um teste que passou tornam um novo campo opcional visível. Changelog de API cobre em geral o que uma entrada aditiva deve às leitoras; a razão específica do gRPC para escrevê-la mesmo assim é que não existe equivalente a olhar uma resposta REST em um debugger e notar uma chave nova aparecendo.
Como isso é diferente do que as chamadoras de GraphQL enfrentam?
As regras para adições coincidem, mas a exposição é diferente. Depreciação de esquema GraphQL cobre um modelo onde o cliente só recebe os campos que pede explicitamente, o que torna mudanças aditivas essencialmente sem risco e apenas remoções o perigo real. Clientes gRPC, em contraste, recebem tudo que o servidor envia e decodificam tudo contra sua própria cópia compilada do esquema; a exposição do cliente é limitada não pelo que ele pediu, mas apenas pelo que seu código gerado consegue ler. Essa diferença importa na hora de escrever o changelog: uma entrada GraphQL pode razoavelmente assumir que os clientes estão protegidos de campos que não pediram, uma entrada gRPC não pode assumir isso de forma alguma.
O versionamento de um serviço gRPC funciona igual ao /v1/, /v2/ do REST?
A intenção é a mesma, o mecanismo é diferente. O que são v1 e v2 em uma API REST
cobre versionamento como caminhos de URL paralelos servindo contratos diferentes; serviços gRPC
geralmente são versionados através do nome do pacote dentro do próprio arquivo .proto,
payments.v1.InvoiceService vira payments.v2.InvoiceService, o que muda o nome de serviço
totalmente qualificado que o cliente disca em vez do segmento de URL que ele pede. As duas
abordagens resolvem o mesmo problema, deixar o contrato antigo continuar funcionando enquanto o
novo existe, mas um time com background REST muitas vezes procura o número de versão no lugar
errado e não percebe que quem faz esse trabalho é a declaração do pacote.
O que uma entrada de changelog do gRPC deveria de fato nomear?
A mensagem, o número do campo, e se a mudança é aditiva ou uma remoção que exige migração, nessa
ordem de importância para a leitora que decide se age ou não. “Adicionado shipping_address
(campo 8) em Order” diz à integradora tudo que ela precisa para atualizar o código gerado e
começar a usá-lo. “Reservado o campo 4 em Invoice, legacy_customer_id sumiu” diz a ela para
verificar se algo na base de código dela ainda lê aquele campo, o que uma nota estilo REST
“campo removido da resposta” não comunica com a mesma urgência, porque uma remoção REST só
retorna menos dados, enquanto o reúso de campo do Protobuf os corrompe ativamente.
FAQ
O tipo de um campo pode alguma vez ser mudado sem quebrar o formato no fio?
Só dentro de grupos de compatibilidade específicos que o Protobuf documenta, como estender
int32 para int64 em alguns casos. Trate qualquer mudança de tipo como breaking a menos que
você tenha verificado contra a própria tabela de compatibilidade do Protobuf; assumir
compatibilidade por analogia com o sistema de tipos de uma linguagem é como isso dá errado.
A depreciação de campo no Protobuf funciona como a diretiva @deprecated do GraphQL?
De forma parecida: o Protobuf suporta a opção de campo [deprecated = true], que ferramentas
podem exibir. Nenhuma das duas é imposta: um servidor GraphQL continua respondendo a uma query por
um campo depreciado, e um cliente protobuf continua codificando um. Ambas são indicativas e exigem o
mesmo suporte de changelog.
Renumerar é seguro se você controla cada cliente? Em um sistema totalmente fechado, em princípio sim, mas isso remove toda a propriedade de segurança pela qual os números de campo existem, e “controlamos cada cliente” é uma afirmação que deixa de ser verdadeira no momento em que um build é cacheado, um deploy atrasa, ou um cliente que ninguém lembrava é adicionado. Reserve o número em vez de reusá-lo, mesmo internamente.
Serviços gRPC precisam de uma página de changelog como uma API REST pública?
Só se times externos os consomem sem ler diffs de .proto diretamente, o mesmo teste de “quem
está do outro lado” que changelog de API interna aplica
em geral. Um serviço gRPC consumido só por outros serviços do mesmo time muitas vezes consegue
passar sem um changelog formal em favor do histórico de commits, porque quem quer que o leia já
tem o esquema aberto.
As afirmações técnicas deste artigo não foram revisadas de forma independente. Se algo estiver errado, avise a gente e vamos corrigir.