A field absent from a JSON payload, a field explicitly set to null, and a field set to an empty string ("") are three distinct signals, not one. Absent means "don't touch this." Null means "explicitly no value." Empty string means "a real value, and it's blank." Conflating any two of them is what causes silent data-loss bugs on update.
Quick answer: Absent means "no opinion, leave it alone." Null means "explicitly cleared, no value." Empty string means "a real, blank value." The distinction matters most on
PATCH, where absent-vs-null is the difference between an unchanged record and a silently wiped one — so define it per field in the contract, not per client's best guess.
The Three States Every API Field Can Be In
Every field in a JSON response or request body can independently be absent (key missing), null (key present, value null), or present with an empty value ("", [], 0). These aren't three flavors of "no data" — a well-designed contract assigns each one a distinct, documented meaning instead of leaving clients to infer it from context.
The confusion is understandable because most server-side languages blur the line by default. A Python dict.get(), a JavaScript obj.field ?? null, and an ORM's default serializer often normalize "missing" and "null" into the same value before the response ever leaves your server. That normalization is convenient for writing code and disastrous for API consumers, who receive a response that has already thrown away information they needed.
| State | JSON shape | What it typically means | Common misuse |
|---|---|---|---|
| Absent | key not in object | "No opinion" / not applicable / not yet computed | Used to mean "empty," hiding a real gap in data |
| Null | "field": null | Explicitly no value / intentionally cleared | Used interchangeably with absent, especially on PATCH |
| Empty string | "field": "" | A real value that happens to be blank | Used as a stand-in for null, breaking required-but-nullable fields |
A concrete example makes the difference visible. Consider a customer resource with a middleName field:
{"middleName": "Ray"}— the customer has a middle name, and it's "Ray."{"middleName": null}— you asked, and the customer confirmed they have no middle name.{"middleName": ""}— a form was submitted with the field left blank; unclear if that means "no middle name" or "not answered."{}(nomiddleNamekey at all) — you never asked, or the value hasn't been computed yet.
Those are four distinct real-world facts collapsed onto one field. GraphQL's type system forces part of this decision at the schema level — every field is nullable unless explicitly marked non-null with ! — which is one reason teams weighing REST, RPC, and GraphQL against each other find GraphQL's null handling either clarifying or, if the team doesn't also define presence semantics for partial queries, just as ambiguous as REST's. The wire format doesn't solve the modeling problem; it just moves where you're forced to confront it.
PATCH Semantics: When Null Means Delete and Missing Means Leave Alone
On a partial update, PATCH, the rule that avoids data loss is simple: omitting a field leaves it unchanged; sending null clears it — but only if your contract says so explicitly, since nothing in HTTP or JSON enforces this by default. Skip that rule, and one client library that defaults unset fields to null will quietly erase data on every request.
This is not a hypothetical edge case. It is one of the most common causes of silent data-loss bugs in production APIs, precisely because it never throws an error — the request succeeds, the response looks fine, and the deleted data doesn't surface until someone notices a field is missing days later.
Postman's annual State of the API report has repeatedly found unclear or inconsistent contracts among the top handful of frustrations developers cite when integrating with a third-party API, named by roughly half of respondents in recent editions. This exact ambiguity — what a value's absence versus its explicit nullness is supposed to mean — is very often what sits underneath that complaint.
A Data-Loss Bug, Step by Step
Say a contact resource has email and phone, both optional. A settings screen lets a user update just their phone number. Here is the request the client actually sends:
PATCH /contacts/482
{
"phone": "+1-555-0142",
"email": null
}
The client's form library initialized email to null because the field wasn't rendered on this screen, and it serializes every bound field on submit — including ones the user never saw. If the server interprets any JSON null as "clear this field," the customer's email address is deleted as a side effect of updating their phone number. Nobody asked for that. Nothing in the request looks wrong. The bug ships clean and surfaces three weeks later as a support ticket about "missing" customer emails.
Two real IETF standards solve this exact problem, and it's worth knowing which one your API implements:
RFC 7396, JSON Merge Patch — treats the request body as a partial document to merge: keys present with a value overwrite, keys present withnulldelete the target field, and keys absent from the body are left untouched. This is the semantics described above, and it is the one most REST APIs implicitly promise when they say "send only the fields you want to change."RFC 6902, JSON Patch — sidesteps the ambiguity entirely by using explicit operations (add,remove,replace,test) instead of overloading a JSON value's meaning. There is no guessing about whatnullmeans, because clearing a field is a distinctremoveoperation, not a value.
If you're building a PATCH endpoint on plain JSON without adopting either RFC formally, you are still implicitly choosing one of their behaviors — you just haven't written it down. Google's API design guidance, AIP-134 (Standard methods: Update), takes this seriously enough to require an explicit field mask (update_mask) alongside the body, specifically because relying on null-vs-absent proved too easy for client libraries to get wrong at scale.
Stripe's API takes the RFC 7396 approach on many update endpoints — passing null for an optional attribute like metadata explicitly unsets it, and that behavior is called out by name in their docs precisely because it isn't the intuitive default.
Rule of thumb: if a field can be cleared by the client,
nullmust mean clear and omission must mean unchanged — write that sentence into your contract for every writable field, not just the ones where it seems obvious.
Required, Optional, and Nullable Are Three Different Switches
required, optional, and nullable are three independent controls, not one sliding scale — conflating them produces fields that are technically documented but practically ambiguous. Required governs whether a key must appear at all; nullable governs whether its value may be null. A field can be required and nullable at once: always present, sometimes legitimately empty.
This is where most JSON Schema and OpenAPI definitions go quietly wrong: teams reach for required to mean "must have a real value," when it only checks presence of the key, saying nothing about whether null satisfies it. type: ["string", "null"] is the actual mechanism for allowing null; required is a completely separate list.
| Combination | Key must appear | Value may be null | Real-world example |
|---|---|---|---|
| Required, non-nullable | Yes | No | id, createdAt — always exists, never blank |
| Required, nullable | Yes | Yes | deletedAt — always present, null until deleted |
| Optional, non-nullable | No | N/A if present | discountCode — omitted if not applicable, never sent as null |
| Optional, nullable | No | Yes | middleName — may be omitted, or explicitly cleared with null |
Deciding which quadrant a field belongs in is a resource-modeling decision as much as a schema-syntax one — it depends on what the field actually represents in the domain, which is exactly the question modeling API resources as nouns forces you to answer before you ever open a schema editor. A field like deletedAt earns "required, nullable" because soft-delete is a first-class state of the resource, not an afterthought bolted onto the response.
It also helps to think about the field from the consuming client's side, not just the server's. A billing integration reading nextInvoiceDate isn't just parsing a timestamp — it's trying to get a job done, whether that's "warn the finance team before a charge" or "reconcile a subscription's lifecycle state."
Applying a Jobs-to-be-Done lens surfaces which ambiguity actually breaks that workflow: if nextInvoiceDate being absent and being null both mean "no upcoming charge," you don't need to distinguish them. If one means "not yet scheduled" and the other means "subscription canceled," collapsing them breaks the client's logic.
A short checklist for every field going into a response or request schema:
- Is the key ever legitimately absent? If yes, mark it
optionaland confirm your serializer actually omits it rather than defaulting tonull. - Is the value ever legitimately unknown or cleared, even when the key is present? If yes, mark it
nullablein addition to whateverrequiredstatus it has. - Could an empty string,
0, or[]be mistaken for null by a client? If yes, document explicitly what that empty value means versusnull. - Does this field's ambiguity actually change client behavior, or is the distinction cosmetic? Spend the design effort where it changes behavior first.
Where Default Values Belong (and Where They Don't)
A default value should live in exactly one place: either the server always fills it in before the field reaches the client, or the client is told what to assume when it's absent. The worst contract has both — docs saying "defaults to false" while the serializer sometimes omits the field and sometimes sends false explicitly.
Three defensible patterns, and one that isn't:
- Server always populates it. The field is never actually absent or null in practice — the default is computed and serialized every time. Simplest for clients, costs you nothing but server-side discipline.
- Field is optional, and absence has a documented default. The client is told "if
retryLimitis absent, treat it as3." This only works if every client actually reads the docs, which is optimistic but sometimes the right tradeoff for rarely-used fields. - Field is required, non-nullable, and always has an explicit value. No default needed because the field is never allowed to be missing in the first place — push the decision upstream to whoever creates the resource.
- What doesn't work: documenting a default for a field that's sometimes absent and sometimes explicitly
null, without saying which one triggers the default. Pick one signal to mean "use the default" and make the other signal mean something else, or disallow it.
Defaults are also a versioning trap. Adding a new optional field with a server-side default is usually additive and safe; quietly changing what an existing absent field defaults to is a breaking change wearing a patch-release version number, because every client that relied on the old default silently gets new behavior. This is squarely inside the job of specifying an API contract precisely — defaults are part of the contract, not an implementation detail you're free to adjust later.
Writing the Contract So Clients Stop Guessing
The fix for null-vs-missing ambiguity isn't a clever runtime convention — it's writing the decision into the schema itself, field by field, before any client has to infer it from a support ticket. OpenAPI 3.1 and JSON Schema give you the vocabulary: required for presence, type arrays including "null" for nullability, and a description stating what each state means.
A schema snippet that actually removes the ambiguity looks like this:
deletedAt:
type: ["string", "null"]
format: date-time
description: >
Null when the resource is active. Set to the deletion
timestamp once soft-deleted. This field is always present.
That description line is doing the real work — the type declaration alone tells a codegen tool what's structurally legal, not what each legal value means. Skipping the prose because "the schema already says it's nullable" is how ambiguity survives a technically-complete OpenAPI spec.
This decision doesn't only surface once, at initial design — it resurfaces at every stage of a client developer's integration: the first read, the first write, and again the first time they build a sync job that has to distinguish "nothing changed" from "this was cleared." Treating that sequence like any other customer journey worth mapping — first contact, first real task, first edge case, first failure — makes it obvious that documentation written for the first-read moment usually doesn't answer the first-write question, and needs its own explicit pass.
Making the Decision Impossible to Skip
As you define a response, it has you set each field's presence (required or optional) and type explicitly, including whether null is a legal value, before generating the endpoint's contract. The point isn't that the tool decides for you — it's that it won't let the question quietly stay undecided the way a hand-written schema comment often does.
Key Takeaways
- Absent, null, and empty string are three distinct signals — "no opinion," "explicitly no value," and "a real but blank value" — and a well-designed contract assigns each one a documented meaning per field rather than treating them as synonyms.
- On
PATCH, omission must mean "leave unchanged" andnullmust mean "clear this field," or a client library that defaults unset variables tonullwill silently delete data the user never touched. RFC 7396(JSON Merge Patch) andRFC 6902(JSON Patch) are the two established standards for this exact problem — merge-patch overloadsnullas delete, JSON Patch uses explicit operations to avoid overloading it at all.Required,optional, andnullableare independent switches, not one spectrum — a field can be required and nullable simultaneously (always present, sometimes explicitly empty).- Defaults belong in exactly one place, either always server-computed or explicitly documented for a specific absence signal — never both, and never silently changed once clients depend on the old behavior.
- The schema's type declaration isn't the whole contract — a
descriptionstating what absence and null each mean is what actually stops client teams from guessing differently from each other.
Frequently Asked Questions
Should a required field ever be nullable?
Yes — required and nullable answer different questions, so a field can be both. Required means the key must always appear in the payload; nullable means the value inside that key is allowed to be null. A field like deletedAt is a common example: always present, but null until the resource is actually deleted.
What should a PATCH endpoint do with a field the client never included in the request body?
A well-designed PATCH endpoint leaves an omitted field completely unchanged, updating only the fields that were actually present in the request. This is the behavior described by RFC 7396 (JSON Merge Patch) and is what most API consumers assume "send only what changed" means — if your endpoint does anything else, document that deviation loudly, because it violates the default expectation.
Is PUT or PATCH the right choice for partial updates?
PATCH is designed for partial updates and should be the default choice whenever a client might send only some of a resource's fields. PUT conventionally means "replace this resource entirely with what I'm sending," so using PUT for a partial payload forces an implicit and often undocumented decision about what happens to the fields the client left out.
How do I represent a nullable field in OpenAPI or JSON Schema?
Use a type array that includes "null" alongside the field's real type, for example type: ["string", "null"], rather than relying on the required keyword, which only governs whether the key is present and says nothing about whether its value can be null. Pair the type declaration with a description explaining what null means for that specific field.
Why do null-vs-missing bugs make it to production so often if the fix is "just document it"?
Because most serialization frameworks and ORMs normalize absent and null values by default long before a developer consciously decides which one they meant, so the ambiguity is baked in at the code level, not just the docs level. It typically surfaces only when a specific client's default-initialization behavior collides with a specific server's interpretation — a combination that's easy to miss in testing and expensive to catch until a customer notices their data went missing.