Protobuf Reserved Fields & API Versioning
The Point
When removing a response field from a protobuf message, you cannot simply delete it – you must mark it as reserved. Otherwise, if a future field reuses the same field number, old clients will misinterpret the new field’s value as the old field.
Explanation
Why is reserved needed?
Protobuf identifies data on the wire by field number, not field name. So when you:
- Originally have
string old_field = 3;in a response message - Integrate a new external service, the backend logic changes, this field is no longer populated, so you delete it
- Later add
int32 new_field = 3;(reusing number 3)
At this point, clients still using the old proto definition will receive the response and try to read new_field’s value as old_field – the type mismatch causes a crash.
Correct approach: mark it as reserved
1message MyResponse {
2 reserved 3, 5; // reserve these field numbers from reuse
3 reserved "old_field", "another"; // also reserve the field names (prevents accidental reuse in code)
4
5 string active_field = 1;
6 int32 other_field = 2;
7}With reserved in place, any attempt to reuse those numbers or names will cause a protoc compilation error.
What is the relationship between v2alpha and v2beta?
These are API version stability labels, following Google AIP (API Improvement Proposals) naming conventions, commonly seen in gRPC / proto package naming:
| Version | Meaning |
|---|---|
v2alpha / v2alpha1 | Experimental; may change drastically or be removed at any time; no backward compatibility guarantee |
v2beta / v2beta1 | Feature is mostly finalized, but details may still change; usually has compatibility commitments but incomplete |
v2 | Stable release; full backward compatibility guarantee |
The typical progression is: v2alpha1 -> v2alpha2 -> v2beta1 -> v2beta2 -> v2
How it looks in proto files:
1// Experimental version
2package mycompany.myservice.v2alpha1;
3
4// Feature-complete but still being polished
5package mycompany.myservice.v2beta1;
6
7// Stable release
8package mycompany.myservice.v2;Different versioned packages are completely independent namespaces that can coexist, allowing old and new clients to each use their corresponding version.
Knowledge Sugar
Two ways to write reserved, and they can be separate or combined:
1reserved 1 to 3; // contiguous range
2reserved 1, 2, 3; // individual listing
3reserved "foo", "bar"; // reserve names (recommended to do both, prevents typo reuse)Difference from deprecated:
deprecated = trueis a human-readable warning telling developers “this field is not recommended anymore,” but it still worksreservedis enforced at compile time – it completely blocks any reuse
Practical mindset when designing new proto contracts:
- A field number, once released, is forever – before deleting a field, decide whether it needs
reserved - Alpha/Beta versions give you room to experiment; once you reach a stable
v2, removing or changing fields requires a formal deprecation process
Further reading:
- Google AIP-180: definition of breaking vs non-breaking changes
oneoffield numbers in protobuf also needreserved- Buf (buf.build): a protobuf linter that can automatically detect breaking changes