Scalar Types & Defaults
The scalar types
Section titled “The scalar types”Protobuf ships a small set of scalar types. The interesting choices are among the integers, where the encoding — not just the range — should drive your pick.
| proto type | Use it for | Notes |
|---|---|---|
int32 / int64 | General integers | Variable-length; inefficient for negatives |
uint32 / uint64 | Values that are never negative | Variable-length |
sint32 / sint64 | Integers that are often negative | ZigZag encoding — far smaller for negatives |
fixed32 / fixed64 | Large values that are usually big | Always 4/8 bytes; beats varint past ~2^28 |
float / double | Fractional numbers | 4 / 8 bytes |
bool | True / false | 1 byte |
string | UTF-8 text | Must be valid UTF-8 |
bytes | Arbitrary binary | Any byte sequence |
The subtlety: int32/int64 use varint encoding, which is compact for small positive numbers but wastes bytes on negatives. If a field is regularly negative, use sint32/sint64. If it’s a random large number (like a hash), fixed64 avoids varint’s overhead.
proto3 default values
Section titled “proto3 default values”In proto3, every field has a default, and a field that was never set is indistinguishable from a field explicitly set to that default:
| Type | Default |
|---|---|
| numbers | 0 |
bool | false |
string | "" (empty) |
bytes | empty |
enum | the first value (must be 0) |
| message | not set (null-like) |
And here is the trap that surprises everyone.
The “unset vs zero” problem
Section titled “The “unset vs zero” problem”A plain proto3 scalar cannot tell you “was this field sent?” — it only tells you the value, and an absent field reads as the default.
flowchart TB a["client omits 'age'"] --> z["server reads age = 0"] b["client sends age = 0"] --> z z --> q["server cannot tell which one happened"]
Imagine an UpdateUser request with int32 age = 5. If the client wants to clear the age it sends 0 — but a client that simply didn’t touch age also sends 0. The server can’t distinguish “set to zero” from “not provided.” For partial updates, that ambiguity is a real bug.
optional brings back presence
Section titled “optional brings back presence”proto3 later reintroduced field presence via the optional keyword. An optional scalar tracks whether it was explicitly set, giving generated code a has_age() / hasAge check:
message UpdateUserRequest { int64 id = 1; optional int32 age = 5; // now distinguishable: set vs unset}Reach for optional whenever “not provided” must be different from “the zero value” — classic cases are partial updates (PATCH-style) and settings where 0/false/"" are legitimate values a user might choose.