Why can a reader that substitutes implicit defaults not tell a writer's explicit zero from an unset field?
answer
- the default is filled in during decode
- absent and default collapse to one value
- encoders may elide a default-valued field
- zero, empty string and false are real values
- make the default the inert value
basics
~20 sBecause the default is materialised during decoding: an absent field and a field carrying the type's default produce the identical in-memory value. Presence information is destroyed before any application code runs, so no later check can recover it.
solid answer
~50 sWith **implicit defaults**, a reader that finds no bytes for a field fills in the type's default — zero for a number, the empty string for text, `false` for a boolean. A writer that deliberately set the field to that same value produces an identical result. Some encodings go further and let the writer **elide** a field whose value equals the default, so the two cases are indistinguishable on the wire as well as in memory. The consequence is that a field whose default value is also a meaningful business value — a discount of `0`, a quantity of `0`, an `isActive` of `false` — can never be read reliably. Any hop that copies the decoded value into a new message then writes the default out explicitly, so an omission on one side of the pipeline becomes an assertion on the other.
go deeper
Remember that a reader fills in a default for a field it does not find, so a zero on the wire and no field at all look the same once the message is decoded.
Explain where presence is lost — elision by the encoder, substitution by the decoder — and name the field types whose defaults collide with real business values.
Trace the damage through an intermediary: a value the producer never set is materialised, copied and then written out explicitly, turning silence into an assertion downstream.
Decide the standing rule for the contract: prefer field semantics whose default is inert over per-field presence markers, which double the contract and can disagree with themselves.
## The rule that creates the ambiguity An **implicit default** is a rule in the wire contract that says: if the bytes for a field are not present, the reader behaves as if the field carried a particular fixed value. That value is normally the type's zero — `0` for numbers, the empty string for text, `false` for booleans, an empty list for repeated fields, and the first declared member for an enumerated type. The rule is there for a good reason. It means a reader written against a newer schema version can decode an older message without special-casing every field added since, and it keeps messages small, because a field at its default carries no information and need not be written. The cost is that **the decoded value carries no record of which fields were actually on the wire**. ## Where the presence information is destroyed 1. **The writer decides.** It either sets the field to a value that happens to be the default, or does not set it at all. 2. **The encoder may elide.** Several encoding families omit a field whose value equals the default, so the two writer intentions converge to the same bytes. 3. **The reader substitutes.** On decode, the missing field becomes the default in the in-memory value. By the time application code runs, step 3 has already happened. No amount of validation downstream can distinguish the two cases, because there is nothing left to distinguish. ## The values that collide | Field | Implicit default | Real value that collides | Symptom in production | |---|---|---|---| | Discount percentage | `0` | A deliberate zero discount | Cannot tell "no discount applied" from "discount not calculated yet" | | Retry count | `0` | A message never retried | A missing counter reads as a fresh message | | `isSuspended` | `false` | An account explicitly cleared | An unset flag reads as an explicit clearance | | Currency code | empty string | Never a real value | Safe: the default is inert | | Priority (enumerated) | first member | Whatever the first member happens to mean | A new writer's silence reads as that member | The last two rows carry the lesson. The empty string is a safe default for a currency code because no valid currency code is empty — the default is **inert**. The enumerated field is dangerous precisely because its default is a real, meaningful member that somebody's code will act on. ## Why an intermediary makes it worse A hop that decodes a message, changes one field and re-emits it does not forward the writer's silence — it forwards its own **materialised** value. A field the producer never set arrives at the hop as the default, is copied into the outgoing value, and is then written out as an explicit default. What began as "the producer had nothing to say about this field" ends as "an upstream service asserted this value". Downstream logic that was written to treat an absent field as "unknown, go and look it up" now sees an assertion and skips the lookup. ## Designing the ambiguity away You cannot recover presence after the fact, so the work is in the field's design: - **Make the default inert.** Choose an encoding of the domain in which the type's default is a value that means "do nothing" — an empty string for a code, an empty list for a set of overrides, an explicitly declared `unspecified` member as the first enumerated value. - **Never let a defaulted field carry a decision.** If acting on the default and acting on a genuine zero must differ, the difference belongs in a second field that says which operation was requested, not in the value itself. - **Shift the unit so zero is impossible.** A field recording a percentage in basis points where `0` is legal is ambiguous; one recording an override that is only ever written when an override exists is not. - **Do not reach for a wider numeric type.** A larger range does not separate zero from unset; the collision is about the default rule, not about the representable values. ## What an interviewer is listening for A weak answer treats this as a nullability preference. A strong answer names the moment the information is destroyed — default substitution during decode, before any application code sees the message — and follows it through a pipeline, where an omission on one hop becomes an explicit assertion on the next. The strongest answers also say what they would *not* do: adding a parallel "was this set" boolean to every field is a design that grows without bound, so it is better to pick field semantics whose default means nothing at all.
- Why does a wider numeric type not solve the zero-versus-unset collision?The collision is created by the default rule, not by the range of values. Whatever the width, the reader still substitutes that type's default for an absent field, and a writer that sets the field to that same value produces the identical decoded result. Widening only changes which values are representable.
- What is wrong with adding a parallel boolean saying whether each field was set?It works for one field and does not scale: the contract now carries two fields per value, both of which can disagree, and every writer must remember to keep them consistent. It is better to choose field semantics whose default value means nothing, so presence stops mattering.
- How does an enumerated field's default differ from a numeric one's?A numeric default is at least recognisable as a suspicious zero. An enumerated field defaults to a declared member that reads as a deliberate choice, so silence is indistinguishable from an explicit selection. Reserving the first member as an explicit unspecified value restores the inert default.
saying these in an interview costs you the question
- Says a check after decoding can recover whether the field was present
- Thinks only optional fields are affected, never ordinary scalars
- Believes a wider numeric type separates zero from unset
- Treats an empty string and a missing string as obviously different everywhere
- Assumes an enumerated field's first member is a neutral value