Breaking changes de Protobuf: qué sobrevive en el wire
7 min de lectura
Una API REST cambia cuando cambia una forma JSON, y la mayor parte de esa forma es visible en la
respuesta que puedes leer en un navegador. Una API gRPC cambia cuando cambia un archivo .proto,
y el formato binario de wire de Protocol Buffers tiene sus propias reglas sobre qué puede tolerar
un cliente que no tienen nada que ver con lo que dicen los nombres de los campos. Dos ediciones
que se ven igual de pequeñas en un diff, renumerar un campo frente a añadir uno, caen en lados
opuestos de una línea que breaking changes traza en general: una es
invisible para cada cliente existente, la otra los rompe a todos a la vez. Distinguir los breaking
changes de Protobuf de los cambios seguros significa leer las propias reglas del formato de wire,
no adivinar a partir de cómo se ve el cambio en un diff de .proto.
¿Por qué importa más la numeración de campos que el nombre de campo en Protobuf?
Porque el formato de wire codifica los campos por número, no por nombre. El código generado en
cada lenguaje lee y escribe esos números; el nombre de campo email en tu archivo .proto es una
comodidad para humanos que nunca toca los bytes binarios enviados por la red. Renombrar un campo,
email a email_address, es seguro en el wire binario mientras el número se mantenga
igual, lo cual sorprende a ingenieras acostumbradas a REST, donde una clave JSON renombrada es
exactamente el tipo de cambio que rompe a un cliente. La excepción es ese mismo caso de REST: los
formatos ProtoJSON y de texto serializan el nombre,
así que un renombrado rompe el transcoding JSON (un grpc-gateway, por ejemplo), los archivos en
formato de texto y las field masks. Renumerar ese mismo campo, manteniendo el
nombre pero cambiando 1 a 7, es exactamente lo contrario: invisible en una revisión de código
que solo muestra nombres, y corrompe cada mensaje que un cliente envía o recibe a partir de ese
punto.
| Cambio | Seguro en el wire | Por qué |
|---|---|---|
| Renombrar un campo, mantener su número | Binario sí, JSON y texto no | La codificación binaria usa el número; ProtoJSON y el formato de texto usan el nombre |
| Cambiar el número de un campo | No | Cada mensaje existente ahora se lee como el campo equivocado |
| Añadir un campo nuevo con número nuevo | Sí | Los clientes antiguos ignoran campos que no reconocen |
| Eliminar un campo, reutilizar su número antiguo para otra cosa | No | Los datos antiguos se decodifican en el campo nuevo equivocado |
Cambiar el tipo de un campo de forma incompatible (p. ej. int32 a string) | No | La codificación de wire difiere por tipo |
¿Qué hace diferente eliminar un campo respecto a hacerlo en una respuesta JSON REST?
El número se vuelve radiactivo. La propia guía de Protobuf
recomienda marcar el número de un campo eliminado como reserved en vez de dejar que se reutilice, porque la reutilización es
donde ocurre el daño real: un cliente que todavía corre código generado del mes pasado envía un
mensaje usando el número antiguo del campo para el significado antiguo, y el servidor, que ahora
espera que ese número signifique otra cosa, malinterpreta los datos en silencio en vez de
rechazarlos de plano. REST no tiene una trampa equivalente, porque una clave JSON eliminada
simplemente deja de aparecer; no hay forma de que la petición de un cliente antiguo se
reinterprete calladamente como otra cosa. Un archivo .proto con reserved 4, 9, 12; al
principio de un mensaje es una cicatriz permanente, y ese es el punto: evita que el número se le
dé a un campo nuevo por parte de alguien que no conocía su historia.
message Invoice {
reserved 4; // era `legacy_customer_id`, eliminado el 2026-06-01
reserved "legacy_customer_id"; // también el nombre, para JSON/texto
string customer_id = 5;
string status = 6;
}
¿Añadir un campo llega a necesitar una entrada de changelog?
Normalmente no una entrada de breaking change, pero a menudo sí una normal, porque “seguro en el wire” e “invisible para una lectora a quien le importa” son afirmaciones distintas. Añadir un campo a un mensaje de respuesta no cuesta nada estructuralmente, los clientes antiguos decodifican el mensaje e ignoran el campo nuevo automáticamente. Pero quien construye una integración nueva contra ese servicio no tiene forma de saber que el campo existe a menos que alguien se lo diga, porque nada en un build exitoso o un test que pasa hace visible un campo opcional nuevo. Changelog de API cubre en general qué debe una entrada aditiva a quien la lee; la razón específica de gRPC para escribir una de todos modos es que no hay equivalente a navegar una respuesta REST en un depurador para notar que apareció una clave nueva.
¿En qué se diferencia esto de lo que enfrentan quienes llaman a GraphQL?
Las reglas para las adiciones coinciden, pero la exposición es distinta. Deprecación de esquema en GraphQL cubre un modelo donde un cliente solo recibe los campos que pide explícitamente, lo que hace que los cambios aditivos sean esencialmente libres de riesgo y las eliminaciones el único peligro real. Los clientes gRPC, en cambio, reciben lo que sea que el servidor envíe y decodifican todo contra su propia copia compilada del esquema; la exposición de un cliente no está limitada por lo que pidió, solo por lo que su código generado sabe leer. Esa diferencia importa para escribir changelogs: una entrada de GraphQL puede razonablemente asumir que los clientes están protegidos de campos que no pidieron, y una entrada de gRPC no puede asumir eso en absoluto.
¿Versionar un servicio gRPC funciona igual que /v1/, /v2/ de REST?
El mecanismo es distinto aunque la intención sea la misma. Qué son v1 y v2 en una API
REST cubre el versionado como rutas de URL paralelas que
sirven contratos distintos; los servicios gRPC típicamente versionan a través del nombre del
paquete en el propio archivo .proto, payments.v1.InvoiceService se convierte en
payments.v2.InvoiceService, lo cual cambia el nombre de servicio totalmente cualificado que un
cliente marca en vez de un segmento de URL que pide. Ambos enfoques resuelven el mismo problema,
dejar que un contrato antiguo siga funcionando mientras existe uno nuevo, pero un equipo que viene
de REST a menudo busca un número de versión en el lugar equivocado y se pierde que la declaración
del paquete está haciendo ese trabajo.
¿Qué debería nombrar realmente una entrada de changelog de gRPC?
El mensaje, el número de campo y si es aditivo o una eliminación que requiere migración, en ese
orden de importancia para una lectora que decide si actuar. “Añadido shipping_address (campo 8)
a Order” le dice a quien integra todo lo necesario para actualizar código generado y empezar a
usarlo. “Reservado el campo 4 en Invoice, legacy_customer_id ya no existe” le dice que revise
si algo en su base de código todavía lee ese campo, algo que una nota al estilo REST “se eliminó un
campo de la respuesta” no comunica con la misma urgencia, porque las eliminaciones REST solo
devuelven menos datos mientras la reutilización de campos de Protobuf los corrompe activamente.
FAQ
¿Se puede cambiar el tipo de un campo alguna vez sin romper el formato de wire?
Solo dentro de grupos compatibles específicos que documenta Protobuf, como ampliar int32 a
int64 en algunos casos. Trata cualquier cambio de tipo como rompedor a menos que lo hayas
comprobado contra la propia tabla de compatibilidad de Protobuf; asumir compatibilidad por
analogía con el sistema de tipos de un lenguaje es cómo esto sale mal.
¿Deprecar un campo en Protobuf funciona como la directiva @deprecated de GraphQL?
De forma parecida: Protobuf soporta una opción de campo [deprecated = true] que las herramientas
pueden mostrar. Ninguna de las dos se aplica a la fuerza: un servidor GraphQL sigue respondiendo a
una consulta sobre un campo deprecado, y un cliente protobuf sigue codificándolo. Ambas son
informativas y necesitan el mismo respaldo de changelog.
¿Renumerar es alguna vez seguro si controlas cada cliente? En un sistema totalmente cerrado, en principio, pero elimina toda la propiedad de seguridad para la que existen los números de campo, y “controlamos cada cliente” es una afirmación que deja de ser cierta en el momento en que un build queda en caché, un despliegue se retrasa, o se añade un cliente que nadie recordaba. Reserva el número en vez de reutilizarlo, incluso internamente.
¿Los servicios gRPC necesitan una página de changelog como una API REST pública?
Solo si equipos externos los consumen sin leer diffs de .proto directamente, la misma prueba de
“quién está del otro lado” que changelogs de API interna
aplica en general. Un servicio gRPC que solo consumen otros servicios del mismo equipo suele poder
saltarse un changelog formal en favor del historial de commits, porque quien lo lee ya tiene el
esquema abierto.
Las afirmaciones técnicas de este artículo no se han revisado de forma independiente. Si algo está mal, avísanos y lo corregiremos.