Skip to content

When a parser finds a field number its schema doesn't define, it keeps the data. The wire format already says how many bytes the value takes up, so the parser reads them, stores them as an unknown field, and keeps going. When the message is serialized again, the unknown fields are written out with it. The data is the same, but the bytes may differ. C++, for example, stores an unknown varint as a decoded integer and re-encodes it.

Much of Protobuf's compatibility story depends on this. An old client can accept a message from a new server, touch the fields it knows about, and pass the message along without stripping fields added after it was compiled. The same goes for proxies and queues sitting between services that upgrade on different schedules.

Unknown fields only survive on the decoded message. They are kept when that message is serialized again, or copied with a message-aware operation like proto.Clone in Go or CopyFrom in Python, and many services do neither. If a service unpacks a request into a domain object and builds a fresh response (or even a fresh copy of the same type, field by field), the new message has no unknown fields at all. Converting to JSON drops them too, because the JSON mapping has no way to name a field the reader doesn't know. So unknown fields make it through services that forward messages as they are, and get lost at any service that rebuilds them.

Be careful with this at the edge of your system. A payload from outside can carry unknown fields your validation never inspected, and preservation passes them on to internal services that do understand them. Many teams that expose Protobuf APIs publicly drop unknown fields at the boundary as a precaution. Most runtimes give you a discard option when parsing.

USER.PROTO (WRITER, V2)
message User {
  string id = 1;
  string email = 2;
}
sending
{ "id": "u_1", "email": "a@b.c" }
serialize
ON THE WIRE
0a03755f31
field 1 — id
12056140622e63
field 2 — email
parsed by a reader compiled against v1
USER.PROTO (READER, V1)
message User {
  string id = 1;
}
parsed into a field
id = "u_1"
kept as an unknown field
12056140622e63
then the reader passes the message along
FORWARDS THE DECODED MESSAGE
send(user)
0a03755f3112056140622e63
unknown field serialized with it
REBUILDS THE MESSAGE
send(toProto(toDomain(user)))
0a03755f3112 05 61 40 62 2e 63
field 2 dropped: a new instance has no unknown fields

Unknown fields live on the decoded message. Forwarding it, or copying it with a message-aware clone or merge, keeps them. Rebuilding it field by field, or converting it to JSON, drops them.

Further Reading

Next

Schema Evolution

Which Protobuf schema changes are safe and which break deployed clients: wire and JSON compatibility levels, plus the deprecate-and-reserve lifecycle for removing fields.