— The stack

FastAPI PATCH semantics: omitted is not the same as null

See an executed Pydantic 2.12.5 PATCH fixture showing how exclude_unset preserves clear intent and exclude_none turns explicit null into no change.

4 min readUpdated 2026-11-05By AptixLabs studio
Payload presence: Omitted, null or value; Intent resolution: Preserve, clear or replace; Domain validation: Field and merged rules; Persistence: Only authorized assignmentsAptixLabs · 2026-11-05

Original explanatory figure of a synthetic or proposed design; not a deployed product screenshot. View full-size diagram

For a partial update, omitted usually means preserve the stored value, explicit null can mean clear it, and a concrete value means replace it. Define those meanings per field before using FastAPI or Pydantic serialization flags. exclude_unset preserves the distinction between absent and explicitly supplied fields; adding exclude_none removes explicit nulls and can silently change a clear operation into no change.

Decide the field contract before the model

A profile may allow nickname to be cleared but require timezone to remain a valid non-null value. Optional input typing alone does not decide those domain rules. Document omission, null, empty string and replacement independently. Also specify whether arrays replace the whole collection or merge, and whether nested objects are recursively patched. HTTP PATCH does not choose those application semantics automatically.

FastAPI's update guide demonstrates exclude_unset for partial updates. Use the Pydantic API appropriate to the installed major version and check the resulting dictionary before mapping it to SQL. An existing POST endpoint with merge-like behavior is not automatically a PATCH resource; name the actual route and compatibility contract accurately when applying this pattern.

FastAPI: partial updates and exclude_unset

Executed omitted/null fixture

Our isolated fixture ran with Pydantic 2.12.5 on Python 3.12.13. The stored synthetic profile starts with nickname Mira, timezone UTC and bio Original. The payload supplies nickname as null, omits timezone and replaces bio. With exclude_unset alone, nickname remains present as None. With exclude_unset plus exclude_none, nickname disappears from the update dictionary.

class Patch(BaseModel):
    nickname: str | None = None
    timezone: str | None = None
    bio: str | None = None
payload = {'nickname': None, 'bio': 'Updated'}
p = Patch(**payload)
clear_intent = p.model_dump(exclude_unset=True)
# {'nickname': None, 'bio': 'Updated'}
drop_null = p.model_dump(exclude_unset=True, exclude_none=True)
# {'bio': 'Updated'}

The fixture merges both dictionaries into separate copies of the original profile. The clear contract produces nickname null, timezone UTC and bio Updated. The null-dropping contract leaves nickname Mira. Those are actual local model-and-dictionary results, not an executed database update. They demonstrate the semantic difference without implying that either policy is correct for every field.

Actual local Pydantic fixture outcome
Payload fieldexclude_unsetResult under clear contract
nickname: nullIncluded as NoneNickname cleared
timezone omittedAbsentUTC preserved
bio: UpdatedIncludedBio replaced
nickname with exclude_noneDroppedMira preserved instead

Validate the resolved update before persistence

After extracting supplied fields, apply an allowlist and field-specific rules. Reject null for a field that cannot be cleared, validate a replacement timezone and normalize a permitted empty string consistently. Then validate cross-field constraints on the prospective result where the domain requires it. Updating a start date while preserving an old end date can create an invalid pair even when the individual new field is well typed.

Be careful with model-copy update shortcuts: do not assume merging arbitrary data revalidates every business invariant. Build a validated final representation or invoke the domain service's explicit update validation. Map supplied fields to parameterized assignments and keep immutable identity and ownership columns outside the patch surface. The authorization context must come from verified identity, not a tenant field the caller added.

Preserve old-client meaning deliberately

Some older clients send null for every unknown optional field. If a new server interprets those values as clear, an apparently harmless release can erase data. Inspect actual supported-client payloads, define a versioned or capability-aware transition where needed and add fixtures for omitted fields from old releases. A feature flag cannot teach an installed client a new null contract.

Also include concurrent updates in the acceptance suite. A minimal patch can still overwrite a changed field when the UI submits stale data. Use a documented version or conflict rule for workflows that require it. The saved Pydantic artifact proves field-presence handling for one version; endpoint authorization, transaction behavior and old-client parsing remain separate checks to run before release.

Mobile app API backward compatibility

Building UltraFit360: the app, coach portal and shared backend

Bring one API or data workflow and a sanitized failure example for a bounded implementation assessment.

AptixLabs saas development

How do omitted fields differ from null in FastAPI PATCH requests?

Under an explicit clear contract, omission preserves a value, null clears it and a supplied value replaces it. exclude_unset retains explicit null; exclude_none drops it.

Should exclude_none always be avoided?

No. It is appropriate when null intentionally means ignore; the key is matching the documented field contract.

Does a nullable field have to be clearable?

No. Input typing and business update permissions are separate; reject unsupported clear operations explicitly.

Need help with b2b saas development for connected customer workflows?

Tell us about your product, existing stack and the problem you need to solve. We can discuss integration scope, implementation tradeoffs and a practical first milestone.

Start a conversation