skip to content

In a partial-update request, why is clearing a list field harder than clearing a scalar field like a nickname?

level: seniorimportance: nice to knowfreq 26%

answer

  1. the empty list is already cleared
  2. cleared value coincides with the default
  3. default-omitting encoders drop it
  4. wrapping moves the question, not the answer
  5. presence must be stated, not encoded

basics

~20 s

Because an empty list is already the cleared value, so null adds nothing a scalar's null adds, and encoders that omit empty collections make set-to-empty look identical to never-mentioned. The fix is to state presence separately.

solid answer

~50 s

For a scalar, the three wire states map neatly: absent is *leave alone*, null is *clear*, a value is *set*. For a list the mapping breaks down, because an empty list `[]` is already the cleared value — it is a present value meaning *no elements*, not a gap. So null buys nothing that `[]` does not already say. Worse, many encoders omit a collection that has no elements, on the grounds that an empty collection is the field's default, which turns *set this to empty* into *never mentioned* somewhere on the path. A presence wrapper does not rescue this the way it rescues a scalar; what is missing is a statement that the field was mentioned at all. That is what an explicit list of updated field paths provides, which is why collection fields are the case that pushes teams towards it.

go deeper

for a junior

Know that an empty list is a value meaning no elements, not a missing field, and that the two are different things for a request that only carries what it is changing.

for a middle

Explain why null adds no meaning for a collection when the empty collection is already the cleared value, and what a default-omitting encoder does to an empty list.

for a senior

Describe the silent failure: a successful request that leaves the collection untouched because a hop dropped the empty value, reproducing in one environment and not another.

for a principal

Decide the contract-level policy — one spelling per instruction, an explicit path list wherever collections are updatable, and an explicit verb where replace and append both have to exist.

## Why the scalar recipe does not transfer For a scalar field, three wire states carry three instructions cleanly: - absent → leave the stored value alone; - present and null → clear it; - present with a value → set it. Apply that to a list field and the middle row loses its job. An empty list is not a gap; it is a present value whose content is *no elements*. Clearing a list and setting it to empty are the same outcome, so `null` is a second spelling of something `[]` already says. Contracts that keep both then have to define what the extra spelling means, and teams usually land on one of three readings — a synonym for empty, a rejected value, or a distinct *unset* state — with no agreement across systems about which. ## The failure that actually bites The sharper problem is on the encode side. Encoders in several families omit a field whose value equals the type's default, and for a collection the default is *no elements*. So a client that deliberately sends an empty list may find that nothing at all is written for the field, and the receiving side sees an absence. In a partial update, absence means *leave alone*. The client asked to remove every tag on a profile, and the server kept them all. This is a silent failure with three unhelpful properties: - It does not fail at the boundary; the request is well-formed and the response is a success. - It is invisible in a text-encoded body and appears only when a binary hop is in the path. - It depends on which services the request traversed, so it reproduces in one environment and not another. ## Why a presence wrapper is the wrong reach For a scalar, wrapping the value in a small object works because the wrapper is present whatever it contains, restoring the separation between *was this mentioned* and *what does it say*. The instinct is to do the same for a list, but the list's problem is not that it cannot express the cleared value — it can, as `[]`. Its problem is that the cleared value is also the default, and a default-omitting encoder drops defaults. Wrapping moves the question up one level without answering it. What does answer it is a statement of presence that does not live in the encoded value at all: an explicit list of the field paths this request intends to set. The server applies exactly those paths, taking each value from the body, so an empty list named in the path list is applied as an empty list no matter what the encoder did with the bytes. | Field kind | Does null add meaning? | Survives a default-omitting encoder? | |---|---|---| | Scalar, bare | Yes — null is the only cleared spelling | No, if the new value equals the default | | Scalar, wrapped | Wrapper presence carries it instead | Yes — the wrapper is not the default | | List or map | No — the empty collection already clears | Only with an explicit path list | ## What to do about it 1. **Decide, per contract, whether null is legal for a collection field at all,** and reject it rather than silently treating it as empty. One spelling for one instruction. 2. **Use an explicit path list wherever collections are updatable,** so a cleared collection is expressed as presence rather than as content. 3. **Beware of the merge question hiding underneath.** For lists, *replace* and *append* are different operations, and a sparse body cannot express which one the sender meant. If both are needed, that is a second signal the contract has to carry, not something a reader may infer from the list being empty. 4. **Test the whole path, not the endpoint.** Send an empty collection through every hop the real traffic uses and confirm it arrives as an empty collection rather than as an absence. ## Why this is a differentiator Most candidates handle the scalar case well and stop there. Noticing that the collection case is different — because its cleared value and its default coincide — takes having shipped one. It is also a good question precisely because the failure is quiet: the request succeeds, the response looks right, and the tags are still there.

  • If a contract allows both null and an empty list for a collection field, what should the server do?
    Pick one spelling and reject the other at the boundary. Treating them as synonyms is defensible but leaves two ways to say one thing, which every client library will then disagree about. What is not defensible is giving null a third meaning such as unset, because a reader cannot distinguish that from an absence once any hop re-encodes the body.
  • Does the same problem affect map fields and nested records?
    Yes, for the same reason. An empty map is the cleared value and usually also the default, so a default-omitting encoder can drop it. A nested record is worse, because clearing the whole sub-record and setting every one of its fields to their defaults can encode identically, and the two mean different things to the stored model.
  • Why can a sparse body not express whether a list should be replaced or appended to?
    Because the body carries a value, not a verb. A list of three tags in the body says what the sender has, not whether it should become the whole set or join the existing one. If both operations are needed, the contract has to carry the verb explicitly; inferring it from whether the list is empty overloads one field with two instructions.

saying these in an interview costs you the question

  • Assumes a null list and an empty list must mean different things
  • Thinks a presence wrapper solves clearing for collections
  • Believes an empty list is always written to the wire
  • Infers replace versus append from whether the list is empty
  • Tests the endpoint only, never the full encoding path