In a partial-update API, a client sends a JSON body where a field is missing versus present with the value null. Why does that distinction matter, and how do you handle it on the server?
answer
- Three intents: untouched / set / clear
- Nullable DTO collapses absent and null
- Merge patch: null = delete (specified)
- Tri-state wrapper or raw JsonNode/containsKey
- Clearable field → DELETE sub-resource
basics
~20 sMissing means "leave it alone"; null usually means "clear it". Most JSON binders collapse both to a null field, so the server cannot tell them apart and silently wipes data. Fix it by parsing into a tri-state (absent / null / value) or a raw node.
solid answer
~50 sA partial update has three possible intents per field: don't touch, set to a value, clear. JSON expresses them as absent, `"x": 5`, and `"x": null` — but a typical DTO with nullable fields maps *both* absent and null to `null`, so the server sees two intents as one. Two failure modes follow: either you treat null as "untouched" and clients can never clear a field, or you treat it as "clear" and every client that omits a field wipes it. Solutions, in rough order of preference: use JSON Merge Patch, where null is defined as delete; parse into a tri-state wrapper (`Optional`-of-nullable, Jackson's `JsonNullable`, Kotlin `JsonElement`) or inspect the raw parsed tree for key presence; expose the clearable field as a sub-resource so `DELETE /orders/42/note` is explicit; or use JSON Patch, where `remove` and `replace: null` are distinct operations. Whatever you pick, document it, and cover it with a test that sends the field absent and the field null.
go deeper
State the difference in intent — missing means leave alone, null usually means clear — and that the server must be able to tell them apart.
Explain how a nullable DTO destroys the distinction and name a concrete tri-state technique in your stack.
Discuss the data-loss incident shape, per-intent validation, nested recursion, unknown-field policy, and the tests you would add.
Standardise the semantic across the platform — one patch format, one null policy, enforced in the shared serialization layer and the API linter rather than per team.
## The three intents Every partial update carries, per field, one of three intents: 1. **Leave it as it is** — the client does not know or care about this field. 2. **Set it to this value.** 3. **Clear it** — remove the value, make it null/empty. JSON has exactly the vocabulary to express all three: key absent, key with value, key with `null`. The bug is not in JSON; it is in the layer between JSON and your domain object. ## Where the information is lost A conventional deserialization target looks like: ``` data class OrderPatch(val status: String?, val note: String?) ``` Both `{}` and `{"note": null}` produce `note = null`. The distinction has already been destroyed before your service code runs, so no amount of careful business logic can recover it. Teams then pick one of two bad defaults: - **"null means untouched"** — safe against accidental wipes, but the API now has no way at all to clear an optional field. This is where the sentinel hacks appear: `"note": ""`, `"note": "__CLEAR__"`, or a parallel `clearFields: ["note"]` array. All of them leak into every client and every SDK. - **"null means clear"** — expressive, but now any client that serializes its whole model (including fields it never populated) silently erases data. This is the incident version: a mobile app upgraded its model, started sending `"couponCode": null`, and stripped coupons from thousands of orders. ## Handling it properly **Option 1 — use a format that defines it.** JSON Merge Patch (RFC 7386) fixes the meaning: absent = untouched, null = delete. You inherit a specified, testable semantic and can point clients at the RFC. The cost is that you can then never set a field *to* null — usually fine, since "null" and "absent" are the same thing in most domains. **Option 2 — keep the tri-state in your types.** Most ecosystems have a wrapper: an `Optional<T?>`-style container, Jackson's `JsonNullable`/`JsonSetter(nulls=...)` plus `@JsonInclude`, a serde `Option<Option<T>>` in Rust, `undefined` vs `null` in TypeScript (which is one of the rare languages where the distinction survives natively). Alternatively, bind the body to a raw tree (`JsonNode`, `Map<String, Any?>`) and ask `containsKey("note")`. Verbose, but explicit — and it is the only approach that also lets you reject unknown fields properly. **Option 3 — make clearing a separate operation.** If a field is genuinely optional and clearable, model it as a sub-resource: `PUT /orders/42/note` sets it, `DELETE /orders/42/note` clears it. No ambiguity, natural idempotency, and per-field authorization becomes trivial. This is often the cleanest answer for the small number of fields that actually need clearing. **Option 4 — JSON Patch (RFC 6902).** `{"op":"remove","path":"/note"}` and `{"op":"replace","path":"/note","value":null}` are different operations, so all three intents are expressible. You pay in verbosity and in harder authorization. ## The parts people forget **Validation differs per intent.** "Set to null" must be checked against whether the field is nullable in the domain; "untouched" must skip that check entirely. A single `@NotNull`-style annotation on the patch DTO breaks partial updates because it rejects the absent case. **Nested objects.** Merge patch recurses: `{"customer": {"city": null}}` clears only `city`. A naive implementation that replaces the whole `customer` object wipes the rest of the address. If you implement merge semantics by hand, implement the recursion. **Unknown fields.** Silently ignoring a typo (`"nnote"`) means a client believes it updated something and it did not. Rejecting unknown fields with `400`/`422` turns that into an immediate, obvious error. Decide, document, and be consistent. **Response shape.** After a partial update, return the full updated representation (`200`) rather than `204` when the semantics are subtle — it lets the client see what actually happened and is the cheapest possible defence against exactly this class of bug. **Tests.** Two cases per clearable field: body with the key absent, body with the key null. They are one line each and they catch the regression that costs data.
- A client complains it cannot clear an optional field. What do you change?First establish which semantic the endpoint has today, because the fix differs: if null currently means "untouched", adopt JSON Merge Patch semantics (null deletes) or add a DELETE sub-resource for that field. Avoid sentinel values like empty string or "__CLEAR__" — they become permanent contract debt and behave differently per field type. Whichever you pick, document it and add tests for the absent and null bodies.
- Should a partial-update endpoint reject fields it does not recognise?Usually yes for internal or versioned APIs: an ignored typo makes the client think a change was applied when it was not, and the failure surfaces much later. Reject with 400 or 422 naming the offending field. The counter-argument is forward compatibility for public APIs where older servers must tolerate newer clients — in that case ignore unknown fields but say so explicitly in the contract.
saying these in an interview costs you the question
- Assuming the JSON binder preserves the difference between an absent key and a null value
- Introducing a sentinel value such as empty string or "__CLEAR__" to mean "clear this field"
- Putting not-null validation on a patch DTO, which makes omitting a field an error
- Replacing a whole nested object when only one nested field was sent
- Not testing both the absent-key and explicit-null request bodies