API contracts

JSON Merge Patch: null is a delete instruction

JSON Merge Patch leaves omitted object members unchanged and uses null to request member removal. An adapter that inserts null for missing fields can turn an empty update into a deletion.

Contract
Omitted and null are different
Fixture
Eight merge cases plus one adapter counterexample
Array behavior
Replace the whole array
Boundary
Local transformations; no HTTP stack
Paper cards in three slots represent an unchanged value, an empty position and a replacement.
Conceptual illustration of field presence and replacement. The article distinguishes omission from explicit removal.

Reproduce the disappearing nickname

Start with a profile containing nickname: "A". The client sends an empty update because it changed nothing. A request adapter reads the missing nickname using a lookup that returns null, then builds a new object containing nickname: null. The resulting patch removes the nickname.

The request did not ask for that deletion. The adapter invented it by collapsing two different states: absent and present with a null value. This is a useful failure to test even when the patch library itself is correct.

The accompanying Python fixture runs eight transformation cases and a separate adapter counterexample. It checks the transformed document and verifies that the original input remains unchanged. No HTTP framework, generated SDK or database participates in the fixture.

Choose the update language before the DTO

JSON Merge Patch uses the media type application/merge-patch+json. In an object patch, omitted members remain untouched, supplied values update members and null requests member removal. Nested objects are processed recursively. An array replaces the corresponding array as a whole. IETF RFC 7396.

Those rules make a nullable product field a contract decision. Removing a member and storing an explicit null are different document states. If callers must select between them, the patch format must preserve that choice all the way through decoding and validation.

JSON Patch supplies explicit operations such as remove and replace. A replace operation can carry a null value, which gives an API a way to express stored null separately from removal. Choose the format from the operations your clients need rather than from the easiest request class to generate. IETF RFC 6902.

ABSENT / No update instruction / Retain the member; NULL / Removal instruction / Delete the member; VALUE / Update instruction / Validate the result
Figure 1. Proposed architecture. Rules shown for members of an object Merge Patch. View full-size figure.

Read the expected outputs, including the awkward ones

The fixture starts from a profile with a nickname, two notification settings and a two-item tag array. The assertions below are specific to that input.

Executed merge cases and the endpoint policy each result requires
Input caseObserved resultApplication question
Empty patch objectProfile unchangedDid decoding preserve omission?
Nickname set to nullNickname member removedIs removal allowed?
Nickname set to empty stringEmpty string retainedIs blank a valid nickname?
Email preference set falseSMS preference retainedDid validation keep the nested distinction?
New one-item tags arrayOld tags replacedDid the editor intend full replacement?
Empty tags arrayTags clearedIs an empty collection permitted?
Whole patch is nullWhole result is nullDoes this endpoint permit a non-object document?
Absent member set to nullProfile unchangedIs the request acceptable under your schema?

A transformation being valid does not make it valid for your endpoint. The whole-document-null case should fail a profile schema that requires an object. That rejection belongs to endpoint policy. Silently turning the input into an empty object would hide a client error and change its meaning.

For an editor that only changes one tag, replacing the full array can overwrite a concurrent edit to another tag. Use a contract that fits the editing operation, or require a current resource version. The separate ETag optimistic concurrency guide addresses lost updates after the request's meaning is settled.

Keep field presence through every boundary

Inspect the raw parsed request before constructing a domain command. Track whether a member exists independently from the value it carries. A language may offer a dedicated optional wrapper, a presence bit or a tagged union. The representation matters less than retaining the distinction.

Illustrative procedure
ABSENT              -> leave the field unchanged
PRESENT with null   -> apply the contract's removal operation
PRESENT with value  -> validate and apply that value

Do not populate an update object from every property on a form model. A disabled input, an untouched input and an intentionally cleared input can otherwise become indistinguishable. Test the actual serialized body emitted by the client, not only the object visible in its debugger.

The fixture's broken adapter constructs {"nickname": request.get("nickname")} from an empty request. Its corrected path preserves the empty request. Download the executable example and complete outputs, then run python3 experiment.py. Compare the adapter_counterexample record before trying the same check through your SDK.

EMPTY REQUEST / {} / Existing nickname: A; BROKEN ADAPTER / Adds nickname: null / Nickname removed; PRESERVED REQUEST / Keeps empty object / Nickname remains A
Figure 2. Executed local model. Executed counterexample beside eight transformation cases. View full-size figure.

Validate the resulting resource as well as the patch

Suppose an account requires at least one notification channel. Each field may individually accept a Boolean, yet a patch that leaves both false can violate the account rule. Construct a candidate result from the current resource, validate the resulting state and only then persist the accepted update.

Apply authorization to the fields and operation being requested. A general patch transformer must not let a profile endpoint change a tenant ID, role or billing status because those properties happen to exist in storage. Use an explicit writable-field policy. The permission-matrix example provides a way to make those decisions testable.

HTTP PATCH requires the server to apply the set of changes atomically rather than expose a partially modified resource. The database transaction and the external effects of your endpoint still need an implementation design. IETF RFC 5789.

Move the test to the client-server boundary

Keep the local fixture as a reference, then send the same cases through the supported client. Capture request bytes, decoded field presence and the persisted resource. This catches a serializer that drops null values as well as one that adds them.

  • Send an empty update and verify that no optional field disappears.
  • Clear a field intentionally and inspect the serialized operation.
  • Submit a forbidden field alongside a valid change and verify that no partial write appears.
  • Retry a stale version after another editor changes an array and check the documented conflict response.

Add these cases when generating a new SDK version or changing a request-validation library. The failure often occurs before the patch algorithm runs, so a green unit test for that algorithm cannot close the integration check.

Sources

Documentation checked .

  1. IETF RFC 7396: JSON Merge Patch
  2. IETF RFC 6902: JSON Patch
  3. IETF RFC 5789: HTTP PATCH

Continue the conversation

Comments (1)

  1. Dreamtsoft Editorial

    The adapter counterexample is useful at the serializer boundary too. Capture the outgoing request bytes for an untouched form field and an intentionally cleared field. A correct server-side merge function cannot recover a distinction the client has already erased.

Leave a comment

Your name and comment stay in this page and are cleared after the spam check.

10–2,000 characters. Keep the discussion relevant to this article.

Spam protection verification
Spam protection loads when you begin the form.

JavaScript is required to use this form and its spam protection.