Skip to content

Scalar Types & Defaults

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 typeUse it forNotes
int32 / int64General integersVariable-length; inefficient for negatives
uint32 / uint64Values that are never negativeVariable-length
sint32 / sint64Integers that are often negativeZigZag encoding — far smaller for negatives
fixed32 / fixed64Large values that are usually bigAlways 4/8 bytes; beats varint past ~2^28
float / doubleFractional numbers4 / 8 bytes
boolTrue / false1 byte
stringUTF-8 textMust be valid UTF-8
bytesArbitrary binaryAny 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.

In proto3, every field has a default, and a field that was never set is indistinguishable from a field explicitly set to that default:

TypeDefault
numbers0
boolfalse
string"" (empty)
bytesempty
enumthe first value (must be 0)
messagenot set (null-like)

And here is the trap that surprises everyone.

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"]
Without presence, absent and zero look identical

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.

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.

Which type is best for an integer field that is frequently negative?
In proto3, what is the default value of an unset string field?
What is the "unset vs zero" problem?
When should you mark a scalar field `optional`?