cswork / libraryOpen practice workspace

Articles

Protobuf Compatibility Interview Questions: Field Numbers, Unknown Fields, and JSON Boundaries

Practice Protobuf compatibility interview questions with field numbers, unknown fields, JSON round trips, explicit presence, and a concrete rollout test matrix.

A new service writes a Protobuf field. An old service reads the message and forwards it. The final consumer receives the new field correctly—until someone adds a JSON logging-and-replay step in the middle. The same schema change now loses data.

Protobuf compatibility interview questions test a specific principle: compatibility belongs to a complete path through versions, encodings, and application behavior. A schema diff can identify risks, but it cannot prove that every relay preserves meaning.

For RPC transport, deadlines, and streaming context, start with the broader gRPC interview guide. Here, we focus on schema evolution and executable round trips.

Evidence boundary: official Protobuf documentation supports the wire-format rules. The order schema, migration plan, and test matrix are original examples. The experiments use a named local runtime; they are not candidate reports, employer requirements, or proof about every supported language implementation.

Why Are Field Numbers More Important than Names?

Official fact: the binary format encodes a field tag using its field number and wire type. The field's source-code name is not carried as the binary identifier. A reader uses its schema to interpret the tag and payload. Protobuf encoding guide

Consider an old order message:

syntax = "proto3";
message Order {
  string order_id = 1;
  string region = 2;
}

If a developer removes region and reuses number 2 for coupon_code, old stored bytes can still decode into a string. The parser may succeed while the application interprets eu as a coupon. Successful parsing is therefore weaker than semantic compatibility.

Official guidance: do not reuse tag numbers; reserve deleted numbers, and reserve deleted names to prevent later accidental reuse. The compiler can then reject conflicting declarations. Protobuf best practices

A safer deletion leaves a visible tombstone in the schema:

message Order {
  reserved 2;
  reserved "region";
  string order_id = 1;
}

Reservation is a development guard, not a runtime migration. Historical messages and old binaries remain in circulation; JSON clients still need a separate compatibility check. In an interview, pair the schema edit with an explanation of stored-message retention and the last old reader that still matters.

Is Adding a Field Always Safe?

Official fact: adding a field is binary wire-safe, and old binary readers can retain fields they do not understand. New readers must still handle older messages in which the field is absent. These statements concern the binary contract, not every application invariant. Proto3 language guide

Suppose version 2 adds region = 2 and uses it to select a data-residency policy. Version 1 messages contain no region. The migration is not complete until version 2 defines what absence means: reject the operation, derive a trusted value, or route it through a documented legacy policy.

Do not quietly treat the default empty string as a valid region. A wire-safe addition can create an application-level failure if new code requires information old writers never supplied.

Design recommendation: split “can decode” from “can act correctly.” Test an old producer against the new consumer before enabling the new behavior. Also test a new producer against the old consumer, including any message it stores or forwards. A rolling deployment is a period of deliberate version skew, not a single upgrade event.

Do Unknown Fields Survive an Old Binary Relay?

Official fact: proto3 preserves unknown fields in binary parse-and-serialize operations. Converting to JSON or reconstructing a message field by field can lose them. Message-oriented copying such as CopyFrom() avoids a common form of accidental loss. Unknown-field retention

Local experiment: with Python 3.12.14 and protobuf 7.36.2, we created separate descriptor pools for an old Order containing order_id, and a new Order also containing region. Both had an ordinary scalar attempts field for a later presence test. Separate pools let both schema versions retain the same message name without a descriptor conflict.

The new writer serialized order_id="O17", region="eu". The old reader parsed those bytes and serialized the same message object. The new reader recovered region="eu". The old reader did not need to expose the field through a generated accessor to preserve its binary representation.

Then we changed only the relay: parse with the old schema, convert that old message to ProtoJSON, and parse the JSON with the new schema. The recovered region was empty. The field had disappeared at the representation boundary.

Finally, we sent JSON emitted by the new schema directly to the old schema's parser. Strict parsing rejected the unknown field. Enabling ignore_unknown_fields allowed parsing but discarded the region. Ignoring an unfamiliar field is an availability choice, not a promise to preserve it for a later consumer.

These tests establish specific runtime behavior for these schemas. They do not justify comparing serialized bytes for universal equality: serialization details can differ without changing the logical message. Assert the fields, presence, and invariants you actually need.

What Changes at a ProtoJSON Boundary?

Official fact: ProtoJSON uses field names, normally in lower camel case or an explicit json_name, and enum names in its ordinary representation. It does not provide binary-style unknown-field propagation. Its compatibility rules therefore differ from binary wire compatibility. ProtoJSON format

A source field rename that keeps the same number and type can leave the binary representation intact while changing the emitted JSON key. Existing generated accessors also change. A reviewer must consider the binary consumer, JSON consumer, and compiled application separately.

For example, renaming region to shipping_region can change the output key to shippingRegion. Pinning an appropriate json_name can preserve a chosen JSON spelling, but you still need to test actual parser behavior and source-code consumers. A field-number-only review will miss these interfaces.

ProtoJSON serializes 64-bit integers as decimal strings by default to avoid loss through double-precision numbers. A generic JSON layer that converts that string into a JavaScript number can still introduce precision loss. The safe contract must survive every transformation, including analytics exports, gateways, and replay tooling.

Inference for migration planning: draw the JSON boundary explicitly even if most traffic is binary gRPC. A small operational replay service can be the only component that destroys a field and still cause a serious recovery bug months after a rollout.

Why Does Presence Matter When the Value Is Zero?

Official fact: an ordinary proto3 scalar with implicit presence does not distinguish an absent field from a field set to its default value through the normal API. Explicit presence, such as optional int32 attempts, allows that distinction. The field-presence guide also explains how round trips through implicit-presence code can lose an explicitly present default. Application note: Field presence

Imagine a patch operation where absent means “leave the attempt limit unchanged” and zero means “disable retries.” If both collapse to zero, the server cannot infer the client's intention. An update contract needs explicit presence or a separate field mask and well-defined patch semantics.

Our local fixture wrote an optional attempts=0 with presence set. It passed through an older schema where the same number was an implicit scalar. After reserialization and decoding with the optional schema, HasField("attempts") was false. The numeric value remained zero; the intent-bearing presence bit did not survive.

A value-only assertion passes this counterexample and misses the lost presence. Add assertions for presence, clearing, and merge behavior. Ask whether the API is replacing an object, merging it, or applying a partial update before deciding which representation is appropriate.

Do not solve this by treating every zero as missing. Zero can be a legitimate value. The contract must represent the distinction rather than asking downstream code to guess.

Audit Five Schema Changes

Use a compact decision matrix, then explain the condition behind each answer. “Safe” without a named path is incomplete.

EditWhat the review must establish
Add an optional fieldBinary readers tolerate it; old writers' absence has valid semantics; JSON rollout is controlled.
Reuse a deleted numberReject it: historical bytes can acquire a different meaning.
Rename a fieldKeep its number and type: binary identity remains; generated APIs and JSON names still need migration.
Widen an integerOld readers and intermediate systems must not encounter values they cannot represent.
Move existing fields into oneofReview documented compatibility rules and test old messages containing multiple former fields.

The proto3 guide distinguishes wire-safe changes from conditionally compatible changes. Do not assume that sharing a wire type means two declared types preserve meaning. For example, signed integer encodings and interpretation deserve explicit tests rather than a blanket “all integers are compatible” rule. Updating a message type

For a widening migration, keep new writers within the old range until all relevant consumers are ready. That includes retained events and rollback targets. For a oneof migration, use a fixture containing both previously independent fields and inspect which value survives. The failure is often silent data selection rather than a parse exception.

How Do You Roll Out and Roll Back Safely?

Engineering recommendation: build a path matrix with old and new writers, old and new readers, a binary relay, a JSON relay, and stored-message replay. Cover the combinations your deployment, replay, and rollback procedures actually permit.

For each path, assert successful parsing where required, field values, unknown-field preservation, explicit presence, enum handling, and the business decision produced by the decoded message. Include defaults, maximum values, unfamiliar enum values, and malformed input where validation matters.

Deploy readers that understand the addition before enabling writers that rely on it. During the mixed-version period, keep the new behavior gated and observe failures by schema version and route. A clean aggregate error rate can hide one old mobile client or replay worker that handles very little traffic.

Rollback deserves its own test. Once new values have been persisted, rolling application code back does not roll the data back. Keep a compatible reader, retain a translation path, or constrain writes until the rollback window closes. State which choice you made and why.

Keep fixture bytes and expected semantic outcomes in version control. Regenerate them intentionally when the contract changes. A snapshot of generated code alone does not test whether an old operational tool can still read a retained message.

Practice the Complete Message Path

Some explicitly use a protobuf-like interview format; those are exercises, not the full official Protobuf specification.

Practice questionWhat to rehearse
Validate JSON against a protobuf-like schemaSeparate schema interpretation from JSON validation.
Parse a protobuf-like schema and answer queriesTrack field identities and nested type references.
Design Parsing Boundaries for Interdependent Message TypesDefine parsing ownership across message types.
Implement map serialization and deserializationPreserve values across serialization boundaries.
Build message schema parser with size/type queriesExplain format rules before applying size assumptions.

A focused rehearsal is to explain one message through three paths: new-to-old binary, new-to-old-to-new binary, and new-to-old-to-JSON-to-new. Then add an explicitly present zero. Predict both the value and its presence after each path, and identify the transformation that changes either.

Continue with Backend Engineer interview practice. For every proposed change, name the oldest reader, the longest-lived stored message, and every encoding boundary before you call the rollout safe.

Sources and Further Reading