PUT vs PATCH

Two methods that both write to an existing resource, and one of them will quietly erase data if you misuse it. This guide takes the comparison further than a one-line summary: what each body actually is, the two standardised patch document formats, why idempotency is a statement about server state rather than about responses, and how to stop concurrent writers overwriting each other. Send both against a live endpoint from the API tester while you read.

Two methods, two completely different contracts

The temptation is to file these as “big update” and “small update”. They are not two sizes of the same operation. They differ in what the request body means, and everything else follows from that.

PUTPATCH
Defined byRFC 9110 §9.3.4RFC 5789
The body isA complete new representation of the resourceA patch document — a set of instructions
Effect on omitted fieldsThey are removed or reset to defaultsThey are untouched
IdempotentYes, alwaysNot guaranteed; depends on the document
SafeNoNo
May create the resourceYes, at a client-chosen URLOnly if the API says so
Content-TypeThe resource's own media typeThe patch format's media type

Read the last row twice, because it is the one that trips people. A PUT body carries Content-Type: application/json because the thing you are sending is the JSON resource. A PATCH body is not the resource at all — it is a description of edits — so its media type names the patch language, not the resource. Sending a patch with the resource's media type is legal in the sense that nothing explodes, but you have thrown away the only signal that tells the server how to interpret the bytes.

RFC 9110 puts the PUT requirement plainly: the target resource's state is to be created or replaced with the state defined by the enclosed representation. Replaced. RFC 5789 was written years later precisely because there was no way to express a partial change, and it is explicit that PATCH is neither safe nor idempotent. If you want the wider context on where these sit among the other verbs, the HTTP methods reference covers all nine with their safety and cacheability properties.

A worked example of what a partial PUT destroys

Abstract statements about “omitted fields” do not land until you watch a field vanish. Suppose the server stores this at /v1/profiles/usr_8812:

{
  "id": "usr_8812",
  "displayName": "Ada Lovelace",
  "email": "ada@example.com",
  "timezone": "Europe/London",
  "marketingOptIn": true
}

A settings screen lets the user change their display name and nothing else. The developer, reasoning that only two fields changed, sends only two fields:

PUT /v1/profiles/usr_8812 HTTP/1.1
Content-Type: application/json

{
  "displayName": "Ada King",
  "email": "ada@example.com"
}

A server that implements PUT correctly now stores exactly what it was given, plus whatever the schema fills in for absent members:

{
  "id": "usr_8812",
  "displayName": "Ada King",
  "email": "ada@example.com",
  "timezone": null,
  "marketingOptIn": false
}

The timezone is gone. The marketing preference has flipped to whatever the default happens to be — and if that default is false, the user has been silently unsubscribed by a request that was supposed to change a name. Nobody sent a delete. Nobody sent false. The absence was the instruction. This is not a bug in the server; it is the server doing what PUT means.

The same change as a patch document touches one member and states nothing about the rest:

PATCH /v1/profiles/usr_8812 HTTP/1.1
Content-Type: application/merge-patch+json

{ "displayName": "Ada King" }

Afterwards, timezone is still Europe/London and marketingOptIn is still true. If you are unsure which behaviour your own API implements, the quickest check is to send both requests from the API tester and read the resource back between them. The correct PUT for this edit is not smaller — it is the whole object with one member altered, which means the client has to have fetched it first. That fetch is not free, and as the concurrency section below explains, it opens a window in which somebody else's edit can be lost.

The three patch body formats

PATCH is deliberately a framework rather than a format. RFC 5789 defines the method and leaves the patch language entirely to the media type. Three options dominate in practice: two standardised, one merely popular.

JSON Merge Patch — RFC 7386

Media type application/merge-patch+json. The document looks like a fragment of the resource: an object whose members name the fields you want to change. The server walks it recursively against the stored document. Three rules define the whole format:

  • A member present with a value sets that field.
  • A member present with the value null deletes that field.
  • A member absent from the patch leaves the stored field untouched.

Nesting is recursive: patching {"address":{"city":"Bath"}} changes only the city and leaves the postcode and country inside address alone. Arrays are the exception, and it is a sharp one — an array in a merge patch replaces the stored array wholesale. There is no element-wise merge, no append, no “change item 3”. To add one tag you must send every tag you want to end up with.

{ "timezone": "Europe/Paris", "nickname": null, "tags": ["staff", "beta"] }

That patch sets the timezone, deletes the nickname member entirely, and replaces the tag list with exactly those two entries regardless of what was there before. The consequence of the null rule is worth stating baldly: merge patch cannot store a literal JSON null. The syntax is spent on “delete”. If your schema distinguishes “absent” from “present but null” — and plenty of schemas do, for tri-state flags or nullable foreign keys — merge patch simply cannot express one of those states, and you need JSON Patch or a full PUT.

In exchange you get a format that is trivial to read, trivial to generate from a form, and idempotent in practice: every operation it can express is an assignment or a deletion, so applying it twice lands where applying it once did.

JSON Patch — RFC 6902

Media type application/json-patch+json. The document is not an object but an array of operations, applied in order. Six operations exist:

OperationRequired membersEffect
addpath, valueInsert a member or array element; on an existing object member, sets it
removepathDelete the member or element at the path
replacepath, valueSet an existing location; fails if it does not exist
movefrom, pathRemove from one location and add at another
copyfrom, pathDuplicate a value to another location
testpath, valueAssert a value; the whole patch fails if it does not match

Locations are given as JSON Pointer strings, defined in RFC 6901. A pointer is a sequence of /-separated reference tokens: /address/city descends two levels; /tags/0 selects the first array element; an empty pointer refers to the whole document. Because / and ~ are structural, they are escaped inside a token — ~1 means a literal / and ~0 means a literal ~. So a field genuinely named a/b is addressed as /a~1b, and unescaping must expand ~1 before ~0 or you will corrupt tokens containing ~01. There is one extra token, -, valid only in an add against an array, meaning “the position after the last element” — this is how you append.

[
  { "op": "test",    "path": "/timezone", "value": "Europe/London" },
  { "op": "replace", "path": "/timezone", "value": "Europe/Paris" },
  { "op": "add",     "path": "/tags/-",   "value": "vip" },
  { "op": "remove",  "path": "/nickname" }
]

The test operation is the underrated one. It is an optimistic-concurrency primitive built into the patch itself: assert the value you believe is stored, and if another writer changed it first, the whole patch is rejected rather than applied over the top. That gives you field-level conditional writes without needing an entity tag, and it composes — several test operations can guard a multi-field edit.

JSON Patch is atomic. RFC 6902 requires the operations to be applied as a single unit: if any one of them fails, none of them take effect and the resource is left exactly as it was. You never end up with the first two operations applied and the third not.

And it is not idempotent, which is not a flaw but a direct consequence of expressiveness. Send {"op":"add","path":"/tags/-","value":"vip"} once and the array grows by one element. Send the identical request again — because the connection dropped and your HTTP client retried — and it grows by another. Two vip entries, from one user action. A move is worse: replayed, it fails outright because the source path no longer exists.

A plain JSON object of changed fields

The third option is not standardised at all, and it is the most common thing you will meet in the wild: Content-Type: application/json with a body containing just the members you want changed. It behaves like merge patch for the simple cases and is what most web frameworks generate by default.

It works. The objection is not correctness but interoperability: the media type says “this is a JSON document”, which tells the server nothing about how to interpret it, so the semantics live only in your prose documentation. Does null delete the field or store a null? Does a nested object merge or replace? Does an array append or overwrite? Merge patch answers all three in a specification; a plain JSON body answers them in whatever the framework happened to implement.

Side by side

PropertyJSON Merge PatchJSON PatchPlain JSON object
SpecificationRFC 7386RFC 6902None
Media typeapplication/merge-patch+jsonapplication/json-patch+jsonapplication/json
Document shapeObject mirroring the resourceArray of operationsObject mirroring the resource
Delete a fieldSet it to nullremove operationUndefined by convention
Store a literal nullImpossibleYes, via replaceAmbiguous
Append to an arrayImpossible — send the whole arrayYes, add to /path/-Usually impossible
Nested objectsMerged recursivelyAddressed by pointerFramework-dependent, often shallow
Conditional applicationNoYes, test operationNo
AtomicNaturally, single documentRequired by the RFCImplementation-defined
IdempotentIn practice, yesNoUsually, by accident
Human-readable diffVeryVerbose but preciseVery

If either body is being rejected and you cannot see why, run it through the JSON formatter — a trailing comma inside an operations array is easy to miss and produces a 400 that says nothing useful.

Idempotency, defined properly

A method is idempotent when N identical requests leave the server in the same state as one. That is the whole definition, and the emphasis belongs on server state. It is not a claim that the responses will match. A PUT that creates on the first call returns 201 and then 200 on the second; the stored state is identical both times, so it is idempotent. Conversely an endpoint that returns a byte-identical response every single time is still non-idempotent if it appends a row on each call.

PUT is idempotent because the client supplies the complete target state. “Make it look like this” has the same outcome whether you say it once or ten times. PATCH is idempotent only when the specific patch document happens to describe absolute values rather than relative changes:

Patch documentIdempotent?Why
{"status":"archived"} (merge patch)YesAssigns a fixed value; the second application changes nothing
[{"op":"replace","path":"/status","value":"archived"}]YesAlso an absolute assignment
[{"op":"add","path":"/tags/-","value":"vip"}]NoAppends again on every replay
A custom {"loginCount":{"increment":1}}NoRelative to the current value
[{"op":"remove","path":"/nickname"}]YesSecond removal is a no-op or an error, but state matches
[{"op":"move","from":"/a","path":"/b"}]NoReplay fails; the source is gone

This matters for exactly one practical reason: what a client does when a write times out. A timeout tells you nothing about whether the server processed the request. The response may have been lost on the way back. If the method is idempotent, the client can simply send it again and the worst case is wasted work. If it is not, a blind retry may append a duplicate tag, double an increment or, in the worst designs, charge somebody twice. HTTP client libraries, service meshes and load balancers know this and retry idempotent methods automatically — so a non-idempotent PATCH can be replayed by infrastructure you did not even write.

When you genuinely need a non-idempotent operation to be retry-safe, bolt idempotency on at the application layer with an Idempotency-Key header. The client generates a unique key per logical operation — a UUID is the usual choice — and the server records the key with the result. A repeat arriving with a key it has already seen returns the stored outcome instead of applying the patch again. It is a de facto convention rather than a standard, but it is the pattern every serious payments API uses, and it is the right answer for an appending PATCH too.

Concurrency: whose write wins

Because PUT needs the complete representation, the client that only has one field to change must first fetch the resource, alter its copy and send the whole thing back. That read-modify-write cycle has a gap, and anything another client writes inside the gap is destroyed:

Client A: GET  /v1/profiles/usr_8812   → timezone "Europe/London", tier "free"
Client B: PATCH /v1/profiles/usr_8812  → tier becomes "pro"
Client A: PUT  /v1/profiles/usr_8812   → sends its whole copy, tier "free"

Stored result: tier "free". B's upgrade is gone, and nobody got an error.

PATCH narrows this window but does not close it. A patch that only mentions timezone cannot clobber tier, so unrelated fields are safe. Two clients patching the same field still race, and a patch built from a stale read — “set the total to 90 because I saw 100 and applied a discount” — is just as lossy as a full replacement.

The real fix is a precondition. Take the ETag from your GET, send it back on the write as If-Match, and the server compares before applying:

GET /v1/profiles/usr_8812 HTTP/1.1
→ 200 OK
   ETag: "v41"

PATCH /v1/profiles/usr_8812 HTTP/1.1
Content-Type: application/merge-patch+json
If-Match: "v41"

{ "timezone": "Europe/Paris" }

If the stored entity tag is still "v41", the patch applies. If somebody wrote in between and it is now "v42", the server rejects the request with 412 Precondition Failed and the client re-reads, rebases its change and retries. A lost update has become a visible error, which is the entire point. A server that refuses to accept unconditional writes at all should say so with 428 Precondition Required rather than silently applying them — that status exists precisely to tell a client “resend this with If-Match”.

With JSON Patch you have a second, finer-grained option: a leading test operation on just the field you are changing. If-Match guards the whole resource, so any concurrent edit anywhere in the document causes a conflict; a test guards one path, so unrelated edits pass through. Which you want depends on whether your invariants span fields. ETag and the conditional request headers are covered further in the HTTP headers guide.

Status codes, and advertising your patch formats

Both methods share most of their response vocabulary, but a few codes are specific enough to get wrong.

CodeWhenApplies to
200 OKWrite applied and you are returning the new representationBoth
204 No ContentWrite applied, nothing to send backBoth
201 CreatedPUT created a resource at a client-chosen URLMostly PUT
202 AcceptedThe change is queued and will be applied laterBoth
400 Bad RequestThe patch document is malformed — invalid JSON, missing opMostly PATCH
409 ConflictThe change conflicts with the current state of the resourceBoth
412 Precondition FailedIf-Match did not match the current entity tagBoth
415 Unsupported Media TypeYou sent a patch format the server does not implementPATCH
422 Unprocessable ContentThe document is valid but cannot be applied — a test failed, a pointer targets nothing, the result breaks a business ruleMostly PATCH
428 Precondition RequiredThe server insists on conditional writes and you sent noneBoth

The 415 versus 400 versus 422 distinction is the one worth internalising. 415 means “I do not speak this patch language at all”. 400 means “I speak it, but that is not valid grammar”. 422 means “that is a perfectly good sentence and I still cannot do what it says”. Three genuinely different client fixes: change the media type, fix the syntax, or change the request.

RFC 5789 also defines a response header for discovery. Accept-Patch lists the patch media types a resource understands, and it is appropriate on a 415, on an OPTIONS response, or on any response from a patchable resource:

Accept-Patch: application/merge-patch+json, application/json-patch+json

Almost nobody sends it, which is a shame — it turns “read the documentation and guess” into a machine-readable answer. If you are building an API that accepts PATCH, send it. And whether or not the server does, the presence of PATCH in the Allow header is what tells you the method is supported at all; a resource that does not accept it should answer 405 Method Not Allowed.

Choosing between them, by scenario

The general rule is a question about ownership: does the client hold the complete, current state of the resource? If it genuinely does, PUT is honest and gives you free idempotent retries. If it holds a fragment, describing the fragment is more truthful than reconstructing a whole document around it. Some concrete cases:

ScenarioMethodReasoning
Syncing a config file from a repo to an APIPUTThe file is the desired state; replacement is the correct semantic, and re-running the sync is harmless
A mobile app toggling one notification setting offlinePATCHThe device may hold a stale copy of everything else; sending it back would resurrect old values
Bulk migration setting one field across many recordsPATCHMerge patch per record; no need to read each document first, so the job is far cheaper
Appending an item to an arrayPATCH, JSON PatchOnly add to /path/- expresses append; guard the retry with an idempotency key
Clearing a field back to emptyPUT, or merge patch with nullOmitting from a PUT clears it; merge patch needs an explicit null
Storing a genuine JSON nullPUT or JSON PatchMerge patch reads null as “delete” and cannot express it
Client generates the identifier (a slug, a UUID)PUTThe client knows the URL, so it can create-or-replace there without a separate creation call
Two users editing the same recordEither, with If-MatchWithout a precondition, whoever writes last silently wins
Replacing an entire nested object, not merging itPUTMerge patch would recurse into it and keep members you meant to drop

Common mistakes

Sending a partial body with PUT

The mistake at the top of this page, and the one that costs real data. It survives for years because most frameworks are lenient and merge instead of replacing, so the code appears correct — until the API moves behind a strict gateway, or a library upgrade tightens the behaviour, and suddenly every save wipes the fields the form does not show.

Treating PATCH as a smaller PUT

They are different languages, not different sizes. If you find yourself sending a complete resource representation with the PATCH method, you have written a PUT with the wrong verb on it — and you have lost the guarantee of idempotency for nothing in return.

Reaching for merge patch when you need to append

Merge patch replaces arrays outright. Teams discover this after shipping, when a patch meant to add one tag deletes the other four. The fixes are: send the full array you want to end up with, or switch that endpoint to JSON Patch, or expose a sub-resource so the append becomes POST /v1/profiles/usr_8812/tags — often the cleanest answer of the three.

Assuming your framework does a deep merge

Plenty of PATCH implementations do a shallow merge: a nested object in the request replaces the stored nested object entirely rather than merging into it. Patch {"preferences":{"theme":"dark"}} against a preferences object holding four settings and you may find the other three deleted. Test this against your actual server before relying on either behaviour — send the request from the REST API tester, then GET the resource back and look at what survived.

Forgetting that PUT is how you clear a field

Because PATCH ignores absences, there is no way to say “remove this” by leaving it out. Merge patch needs an explicit null, JSON Patch needs a remove operation, and a plain JSON body may have no vocabulary for it at all. If your API only offers PATCH with a plain body, clearing an optional field can be genuinely impossible.

Returning 200 with the request echoed back

If you return a body from a write, return the resource as it now stands after the change — not the patch document, and not the representation the client sent. Clients use that body to refresh local state, and echoing the input hides any normalisation, server-set timestamp or recalculated field the write produced.

Try both against a live endpoint

Nothing settles this faster than watching two requests come back. Open the API tester and send PUT https://httpbin.org/put with a small JSON body, then the same body as PATCH https://httpbin.org/patch. The service echoes what it received — method, headers and parsed body — so you can confirm your client is sending the Content-Type you think it is, that the patch media type survived, and that the body arrived intact rather than being re-encoded as form data. Change the type to application/merge-patch+json and send it again to see the header pass through untouched.

From there, the HTTP methods reference puts these two in context with the rest of the verbs, what a REST API is covers the resource modelling that decides whether you need PATCH at all, and the HTTP status code reference has every code the table above mentions.

Frequently asked questions

Does PUT really delete fields I leave out of the body?

On a spec-compliant server, yes. The body is a complete new representation, so a member absent from it is absent from the stored state afterwards — removed, or reset to its schema default. Many frameworks merge instead, which is convenient until the day one does not.

Is PATCH idempotent?

Not by definition. RFC 5789 defines it as neither safe nor idempotent, because a patch document may express a relative change. A merge patch that assigns fixed values happens to be idempotent; a JSON Patch appending to an array is not.

What Content-Type should a PATCH request use?

Whichever format the server documents — application/merge-patch+json, application/json-patch+json, or a plain application/json object of changed fields. An unsupported format should come back as 415.

How do I set a JSON field to null with a merge patch?

You cannot — null is the delete instruction, so the value itself is unreachable. Use JSON Patch with a replace, or a full PUT.

Which status code means the patch format is not supported?

415 Unsupported Media Type, ideally with an Accept-Patch header listing what the server does understand. Use 400 for malformed and 422 for valid-but-inapplicable.

Can PUT create a resource that does not exist yet?

Yes, when the client chooses the URL — the server answers 201 on creation and 200 or 204 when it replaced something. PATCH creating a resource is unusual and should be documented explicitly.