Why Protobuf Schemas Can Evolve Safely
Protobuf messages are wire-compatible across schema versions because each field is identified by its number, not its name or position, and every field is technically optional in the wire format (even proto3's implicit-presence fields are simply omitted when at their default). This means an old binary reading a message from a new schema simply skips fields it doesn't recognize (stored as 'unknown fields'), and a new binary reading a message from an old schema treats any field the old sender never set as its default value.
Cricket analogy: A scorecard from a match in the 1990s still makes sense to a modern reader even without T20-era columns like strike rate, because the shared columns (runs, overs, wickets) are read the same way, just as new binaries safely read old messages using shared field numbers.
Safe vs Breaking Changes
Safe changes include adding a new field with a new, previously-unused number; adding a new value to an enum (thanks to open enum semantics); and adding a new message type. Breaking changes include reusing a field number for a different field or type, changing a field's number, changing an incompatible wire type (e.g., int32 to string is unsafe, but int32 to int64 is safe because they share the varint wire type), and removing a field without reserving its number, which invites a future author to accidentally reuse it.
Cricket analogy: Assigning a new player a jersey number that a legendary retired player already wore risks confusing old broadcast footage and stats systems, the same conflict that occurs when a protobuf field number is reused for a different field.
The reserved Keyword
The reserved keyword lets you explicitly retire a field number and/or name so the protoc compiler raises an error if anyone tries to reuse it, which is the safest way to remove a field from an actively-used schema. You can reserve numbers (reserved 4, 9 to 11;), names (reserved "old_field";), or both, and it's good practice to reserve both together whenever you delete a field so that neither the number nor the name can be accidentally reintroduced with different semantics.
Cricket analogy: A cricket club retiring a legendary player's jersey number so no future player can wear it is exactly what reserved does for a protobuf field number, permanently taking it out of circulation.
syntax = "proto3";
message UserProfile {
reserved 4, 9 to 11;
reserved "legacy_avatar_url", "deprecated_bio";
int64 user_id = 1;
string display_name = 2;
string email = 3;
// field 4 (legacy_avatar_url) removed - never reuse
string avatar_url_v2 = 5;
int64 created_at_epoch_ms = 6; // was int32 created_at; safe widen
}Widening a numeric type within the same wire-type family is safe: int32/uint32/int64/uint64/bool all share the varint wire type, and sint32/sint64 share zigzag varint. Crossing families, such as int32 to sint32 or int32 to string, silently corrupts data because the receiver misinterprets the bytes.
Adding a field to a oneof, or moving an existing field into a oneof, is a breaking change for binary wire compatibility in some edge cases and always a breaking change for generated code APIs, since oneof accessors differ from plain field accessors. Treat oneof membership as fixed once shipped.
- Protobuf compatibility is built around field numbers, not names, so renaming a field is safe but renumbering it is not.
- Adding new fields and new enum values is backward- and forward-compatible; open enum semantics preserve unknown values.
- Reusing a retired field number or changing its wire type (e.g., int32 to string) silently corrupts data.
- The reserved keyword blocks a removed field's number and/or name from accidental future reuse.
- Widening within the same wire-type family (int32 to int64) is safe; crossing families is not.
- Moving a field into or out of a oneof breaks generated code compatibility even if binary encoding overlaps.
Practice what you learned
1. What actually identifies a field on the protobuf wire format?
2. Which type change is considered wire-safe?
3. What does the reserved keyword protect against?
4. What happens when an old binary receives a protobuf message containing a field number it doesn't recognize?
5. Is moving an existing field into a oneof block considered a safe change?
Was this page helpful?
You May Also Like
Protobuf Data Types
A tour of Protocol Buffers' scalar, composite, and byte-oriented field types, and how each is encoded on the wire.
Nested Messages and Enums
How to model hierarchical data and closed sets of values in Protocol Buffers using nested message types and enums.
Well-Known Types in Protobuf
The standard library of reusable protobuf message types, like Timestamp, Duration, Any, and wrapper types, and when to use them.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics