Mudanças de API

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çaSegura no fioPor quê
Renomear campo, manter o númeroBinário sim, JSON e texto nãoA codificação binária usa o número; ProtoJSON e o formato texto usam o nome
Mudar o número do campoNãoToda mensagem existente agora é lida como um campo diferente
Adicionar campo novo com número novoSimClientes antigos ignoram campos que não conhecem
Remover um campo, reusar seu número antigo para outra coisaNãoDados antigos são decodificados no campo novo errado
Mudar o tipo de um campo de forma incompatível (ex: int32 para string)NãoA 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.

Relacionado na changeloop: Documentação para desenvolvedores, Comparativo de ferramentas de changelog

changeloop
O time por trás de um changelog que fecha o loop. Os usuários pedem algo, sua equipe entrega, quem pediu fica sabendo.