Quick answer: JSON Merge Patch API tests should cover eight boundaries: preserving omitted fields, deleting explicit null members, replacing arrays, restricting root values, merging nested objects, authorizing property changes, validating the candidate resource, and rejecting stale writes. Test the complete endpoint as well as its merge function; correct merge semantics cannot repair a client that sends the wrong intent.
A profile editor changes one name. The request succeeds, but the phone number disappears. The merge function might be correct: the client or server adapter could have converted an omitted phone field into an explicit deletion instruction. Start your investigation at the request boundary, not at the success status.
This is a release-test guide for an endpoint that has already chosen JSON Merge Patch. For format selection, use the JSON Patch versus Merge Patch comparison. Wikipedia's JSON Patch background introduces the alternative operation-based format. The technical references below govern the actual behavior; the older standards are foundational specifications, not recent trend statistics.
1. Does serialization preserve fields the user did not edit?
Test an edit through the real client serializer and server request adapter, then verify that omitted object members remain unchanged. Preserve field presence separately from field value. Defaulting every absent property to null changes a partial update into a deletion request, even when the merge function itself implements the standard correctly.
Create a fixture with a display name, phone number, and notification preference. Edit only the name through the same form and request builder used in production. Capture the wire body, inspect the parsed request, and read the resource again after persistence. The phone number must survive every stage.
Include explicit false, zero, and empty-string edits. A truthiness condition such as if (value) silently ignores these legitimate values. Conversely, an adapter that constructs every possible field with a null default can erase data the user never touched. Neither defect needs malformed JSON to occur.
Run this regression after changing generated clients or request models. A type that represents an optional field as a nullable value does not necessarily preserve whether that field was sent. The null, missing, and empty-string guide explains the distinctions; this test checks that your whole request path retains them.
2. Does explicit null remove a member rather than store it?
Send an explicit null for an existing optional member and assert that the member is absent afterward, not present with a null value. Also test deletion of an already absent member and preservation of an existing null when omitted. These cases distinguish deletion instructions from unchanged data and catch accidental normalization.
Use property-presence assertions, not only value comparisons. Reading a missing member in JavaScript produces undefined, but a database adapter or response serializer might insert a null again. Assert the stored representation and the response contract independently. If your domain treats both forms alike, document that mapping rather than mistaking it for patch behavior.
Oracle's JSON_MERGEPATCH documentation confirms that null members remove fields and that the function cannot assign null to an object field through that instruction. This remains relevant even when your application uses a different database.
If customers must deliberately store null, reconsider the update representation. An operation-based JSON Patch can express a value assignment separately from removal. Do not introduce a hidden exception where one merge-patch field stores null and another deletes: clients would need undocumented knowledge to construct safe requests. Required-field deletion belongs in the resource-validation test below.
3. Does an array patch replace the complete collection?
Patch a collection with a smaller array and verify that omitted elements disappear rather than survive through an accidental deep merge. Test an empty array, reordered elements, and nested objects inside the array. JSON Merge Patch replaces the array as a value; it does not merge items by index or identity.
A settings API with two notification channels makes a useful fixture. Replace the channels with a one-element array and verify the exact result. Then send an empty array and check that it empties the collection rather than being interpreted as an omitted update. Assertions should cover order when order is meaningful.
Use JSON Compare on sanitized before-and-after documents to review the removed elements. Leave its Ignore array order option off when position matters. The tool reports structural differences; it does not execute merge patches, validate permissions, or prove that a server handled the request correctly.
Be especially careful when borrowing Kubernetes examples. The Kubernetes patch documentation distinguishes JSON merge patch from strategic merge patch, whose list behavior can depend on schema metadata. A Kubernetes-specific list merge is not a portable expectation for an ordinary merge-patch endpoint. For individual item updates, consider a dedicated subresource or a different documented patch format.
4. Does the endpoint reject unsupported root values and media types?
Test null, arrays, strings, and numbers at the patch root against the endpoint's documented resource contract. A non-object merge patch replaces the entire target, so rejecting those values requires an explicit API restriction. Also verify media-type handling; accepting valid JSON syntax does not establish which partial-update semantics the server applies.
For a profile-object endpoint, it is reasonable to permit only object patches. That is an application rule, not a claim that the underlying format only accepts objects. Send a root null and confirm rejection leaves the stored profile unchanged. A successful response containing null would be a very different operation.
Exercise application/merge-patch+json, an unsupported patch type, and whatever behavior you document for application/json. Some middleware parses only its default JSON type, so the request can reach the handler without the expected parsed body. Check the body and status rather than assuming every suffix is enabled.
RFC 5789 defines HTTP PATCH and the Accept-Patch response header for advertising supported formats. Its guidance is about the HTTP interface, while the JSON media-type guide explains the relevant JSON variants. Keep malformed-body errors separate from unsupported-format errors so a client can fix the correct boundary.
5. Do nested updates preserve siblings and handle type changes?
Patch one nested object member and assert that its untouched siblings survive. Then test changing a scalar into an object, deleting a nested member, and applying an empty object to a non-object value. These transitions expose differences between standards-based merge patch, shallow assignment, and generic recursive merge utilities with their own rules.
The following local demonstration checks the data transformation, not an HTTP server. It accepts parsed JSON values, creates fresh results, and uses null-prototype objects to avoid inherited setters during assignment. Production code still needs size and depth limits, authorization, numeric-precision decisions, and a tested parser boundary.
import assert from "node:assert/strict";
const isObject = value => value !== null
&& typeof value === "object" && !Array.isArray(value);
function mergePatch(target, patch) {
if (!isObject(patch)) return structuredClone(patch);
const result = Object.create(null);
if (isObject(target)) {
for (const [key, value] of Object.entries(target)) {
result[key] = structuredClone(value);
}
}
for (const [key, value] of Object.entries(patch)) {
if (value === null) delete result[key];
else result[key] = mergePatch(result[key], value);
}
return result;
}
const plain = value => JSON.parse(JSON.stringify(value));
const before = { name: "Demo", phone: "555", tags: ["a", "b"],
address: { city: "Ahmedabad", postalCode: "380001" } };
const after = mergePatch(before, { phone: null, tags: ["b"],
address: { postalCode: "380015" } });
assert.deepEqual(plain(after), { name: "Demo", tags: ["b"],
address: { city: "Ahmedabad", postalCode: "380015" } });
assert.equal(Object.hasOwn(after, "phone"), false);
assert.equal(before.phone, "555");
assert.deepEqual(plain(mergePatch({ value: 3 }, { value: {} })), { value: {} });
assert.deepEqual(plain(mergePatch({ value: { a: 1 } }, { value: null })), {});
assert.deepEqual(plain(mergePatch({ a: 1 }, {})), { a: 1 });
assert.deepEqual(plain(mergePatch(3, {})), {});
assert.equal(mergePatch({ a: 1 }, null), null);
assert.deepEqual(mergePatch({ a: 1 }, [2]), [2]);
assert.deepEqual(plain(mergePatch({ a: null }, { b: false })), { a: null, b: false });
const special = mergePatch({}, JSON.parse('{"__proto__":{"flag":true}}'));
assert.equal(Object.hasOwn(special, "__proto__"), true);
assert.equal(Object.getPrototypeOf(special), null);
assert.equal(Object.prototype.flag, undefined);
console.log("Merge-patch fixture assertions passed");
The assertions cover untouched values, deletion, replacement, type transitions, and input preservation. RFC 7396, which supersedes RFC 7386, provides the processing definition and additional test cases. Run equivalent fixtures against your actual library; a demonstration function passing does not certify another implementation or its surrounding persistence code.
6. Can a caller change a protected property through a parent object?
Test property-level authorization with direct edits, nested edits, deletions, and parent replacements. Permission to update a resource does not imply permission to change every member. Evaluate the proposed change against the caller's rights before persistence, and ensure that replacing or deleting a parent cannot bypass restrictions on protected descendants.
Imagine a customer may edit a profile name but not account status or billing ownership. Send a valid name update alongside a forbidden status update. The endpoint should enforce its documented failure policy without partially saving the allowed field. Next attempt to remove the parent that contains the protected status.
The OWASP property-level authorization guidance describes the risk of accepting sensitive property changes without appropriate checks. An allowlist is useful, but checking only top-level names is insufficient when permitted objects contain protected children. Deletion also changes state and needs authorization.
Check unknown properties, owner identifiers, server timestamps, and role fields with realistic user roles. Read back the resource as an authorized observer to verify that rejection truly preserved it. Log a safe error category rather than copying a body containing personal information into application logs. These tests belong beside authentication tests, not inside the merge algorithm.
7. Does validation check the complete candidate before any write?
Validate the incoming patch for permitted shape and then validate the complete candidate resource after applying it to a copy. Test required-member deletion and invalid combinations of individually acceptable fields. Reject invalid candidates before writing, and verify that failure leaves both the stored resource and associated business state unchanged.
A booking can require an end date after its start date. A patch containing only a valid-looking end date might still violate that relationship with the existing start date. Similarly, deleting a required name is meaningful merge-patch syntax but can be unacceptable for the resource. Patch validity and resource validity answer different questions.
Use a staged flow: load the current state, check permission, build the candidate, validate the candidate, and persist under a concurrency condition. Keep database transactions or equivalent safeguards around the write boundary. Avoid saving fields one at a time while validating later fields; rejection after the first save is not an atomic update.
The JSON Schema validation pitfalls guide helps with required fields and validation configuration. Add domain checks that a schema alone does not express. In integration tests, assert the stored record and any scheduled work after rejection. A correct error message cannot compensate for a half-applied business change.
8. Does a stale editor fail without overwriting another update?
Read the same resource in two clients, save a change from the first, and submit the second client's stale patch with its original version precondition. Assert that the second write fails and the first change remains stored. Enforce the precondition atomically with persistence; checking a version before an unconditional write still leaves a race.
Collection replacement is a particularly clear test. Both clients load two channels. One adds a third channel; the other removes a channel using the old array. Without a version check, the later replacement also removes the newly added channel. Sending fewer fields did not make the second client's snapshot current.
MDN's If-Match reference explains strong entity-tag comparison and a failed precondition's 412 response. An entity tag, or ETag, represents a resource version under your server's contract. Test stale tags, current tags, and missing preconditions according to that contract; do not assume every endpoint requires the same policy.
Also inject two simultaneous writes that both reach the storage boundary. A sequential test catches stale clients but cannot reveal a gap between checking and saving. After failure, the client should reload and reconsider its intended edit rather than automatically replaying the old full array with a fresh tag.
The JavaScript fixture above was executed locally. These eight endpoint tests are a release checklist to implement in your application; no live API or database is claimed to have passed them. Keep the serializer, merge library, authorization, validator, and conditional write in one testable request path. That is where a small partial update becomes a trustworthy saved change.
For deeper coverage of validator scope, competing writers, alternate mutation routes, recreated resources, and client recovery, run the JSON API concurrency tests.