skip to content

How is Protobuf oneof represented, and what are the compatibility rules when evolving a oneof or adding/removing fields in Protobuf and JSON Schema?

level: seniorimportance: should knowfreq 30%

answer

  1. oneof = at most one set; setting one clears others
  2. Wire-wise a oneof member is a plain optional field
  3. Add-to-oneof safe; move-in/out risky
  4. reserved numbers/names make removals safe
  5. JSON: oneOf/anyOf/allOf + $ref, Draft-07

basics

~20 s

A Protobuf oneof groups fields where at most one is set; setting one clears the others. Adding a new field to a oneof is generally backward/forward compatible, but moving an existing field into or out of a oneof, or removing a field, can break compatibility. JSON Schema models oneof via oneOf/anyOf with $ref.

solid answer

~50 s

Protobuf oneof is a union: several fields share storage and at most one is set; the binary encoding is just whichever field's tag is present, and the generated API exposes a case/which-one accessor. Because oneof members are plain optional fields on the wire, adding a new field to an existing oneof is typically safe (old readers ignore the unknown tag; old data simply has the case unset). Risky changes: moving an existing field in or out of a oneof changes generated-code semantics and can be a breaking change; removing a field breaks readers that expect it. Confluent's compatibility checker treats Protobuf changes (add/remove fields, reserved numbers) under the subject's compatibility level (BACKWARD default). For JSON Schema (Draft-07), a oneof maps to the oneOf keyword (exactly one of), or anyOf, often combined with $ref; adding optional properties is backward compatible, while adding required properties or tightening oneOf is breaking. Always rely on reserved field numbers/names in Protobuf to make removals safe.

code

text · 13 lines
text
message Payment {
  oneof method {            // at most one set
    Card card = 1;
    Bank bank = 2;
    // adding a new member here is generally safe:
    Wallet wallet = 3;
  }
  reserved 4;              // guard a removed field number
  reserved "legacy_field";
}

// JSON Schema (Draft-07) discriminated union:
// { "oneOf": [ {"$ref":"card.json"}, {"$ref":"bank.json"} ] }

go deeper

for a junior

Know oneof means only one of the grouped fields can be set at a time.

for a middle

Explain that oneof members are normal optional fields on the wire and adding members is usually safe.

for a senior

Reason about which oneof/field evolutions break compatibility and how reserved guards removals; map to JSON Schema oneOf/anyOf.

for a principal

Define org-wide schema-evolution policy: compatibility levels per subject, CI compatibility checks, reserved-number discipline, and union-evolution conventions.

## Protobuf `oneof` A **oneof** declares a set of fields where **at most one** may be set at a time. Assigning one member automatically **clears** any other member. On the **wire**, a oneof is not special: each member is encoded exactly like a normal optional field (tag = field number + wire type, then value). The only thing present in the bytes is whichever single member was set — or nothing. The *generated code* adds a discriminator: a `WhichXxx()` / `getXxxCase()` accessor returning which member (or `*_NOT_SET`) is active. ### Evolving a oneof - **Adding a new field to an existing oneof**: generally **safe**. Old readers encounter an unknown tag and ignore it (forward compat); reading old data, the new case is simply unset (backward compat). - **Moving an existing standalone field into a oneof** (or out of one): **risky / breaking**. Field numbers may be unchanged on the wire, but the *semantics* change — setting it now clears siblings — and generated APIs differ. The compatibility checker and downstream code can break. - **Merging two oneofs or splitting one**: breaking; treat as a new schema. - **Removing a oneof member / any field**: breaks readers expecting it. Use **`reserved`** field numbers and names to prevent the number being reused, which keeps future evolutions safe. ## Field-level compatibility (Protobuf generally) Protobuf is forgiving because unknown fields are preserved/ignored and there are no required fields in proto3. Confluent's **compatibility checker** runs these rules under the subject's level (default **BACKWARD**): you may add fields and reserve removed ones; you must not change a field's type or reuse a retired number for a different type. ## JSON Schema (Draft-07) analogue JSON Schema has no `oneof` field construct, but the **`oneOf`** keyword means *validates against exactly one* of the listed subschemas; **`anyOf`** means *at least one*; **`allOf`** means *all*. A discriminated union is typically `oneOf` over several `$ref`-ed subschemas. Compatibility: - **Adding an optional property**: backward compatible (old data still validates; new readers tolerate absence). - **Adding a `required` property**: **breaking** for old producers. - **Tightening** (`additionalProperties:false`, narrowing `oneOf`): breaking. - **Loosening** (allowing more): typically backward compatible. Confluent enforces these via the JSON Schema compatibility checker, again under the subject's compatibility level. ## Practical guidance - Prefer **adding** over **changing**; never reuse retired Protobuf field numbers — `reserved 5; reserved "old_name";`. - Test changes with the registry's compatibility endpoint (`POST /compatibility/subjects/<s>/versions/<v>`) in CI before deploy. - For JSON Schema unions, keep `oneOf` branches additive and avoid making properties required after the fact.

  • Is adding a brand-new field to an existing Protobuf oneof backward compatible?
    Generally yes. A oneof member is a normal optional field on the wire, so old readers ignore the new tag and old data leaves the new case unset. Moving an existing field into the oneof, however, is the risky change.
  • What is the difference between oneOf and anyOf in JSON Schema?
    oneOf requires the instance to validate against exactly one of the subschemas; anyOf requires it to validate against at least one. oneOf is the closer analogue of a Protobuf oneof / discriminated union.

saying these in an interview costs you the question

  • Claiming oneof has a special wire encoding — each member is encoded as an ordinary optional field; only generated code adds the discriminator.
  • Saying removing a Protobuf field is always safe — it breaks readers; you must reserve the number/name.
  • Confusing oneOf (exactly one) with anyOf (at least one) in JSON Schema.
  • Assuming adding a required property in JSON Schema is backward compatible — it is breaking.

context