# PUT vs JSON Merge Patch vs JSON Patch: modeling partial updates in OpenAPI without the null ambiguity

`PATCH` is the most under-specified verb in real APIs. One team treats it as "send the fields you changed," another expects the full resource, and a third uses `null` to mean both "leave this alone" and "clear this value," so clients cannot win. The HTTP standard deliberately does not define patch semantics; it only says the media type does. Pick a concrete patch format, put its media type in the OpenAPI, and the ambiguity disappears.

## PUT and PATCH answer different questions

|                | PUT                                    | PATCH                                        |
|----------------|----------------------------------------|----------------------------------------------|
| Meaning        | Replace the entire resource at the URL | Apply a set of changes to the resource       |
| Body           | The complete new representation        | A patch document in a defined format         |
| Idempotent     | Yes, by definition                     | Depends on the format and operations         |
| Missing fields | Reset to default or rejected           | Merge Patch: untouched; JSON Patch: explicit |
| Good for       | Full replacement, upsert               | Small edits to large resources               |

A `PUT` that silently ignores fields the client omitted is a bug, because the client believes it replaced the resource. If you want partial updates, use `PATCH` and say which patch language you speak.

## JSON Merge Patch (RFC 7386)

Media type: `application/merge-patch+json`. The patch looks like the resource, but with three rules that catch people out.

1.  A present field replaces the server's value.
2.  A field set to `null` **deletes** it.
3.  Arrays are replaced wholesale; there is no element-by-element merge.

Given the stored resource:

``` json
{ "name": "Acme", "tags": ["vip", "mfg"], "address": { "city": "Berlin", "zip": "10115" } }
```

and this merge patch:

``` json
{ "name": "Acme GmbH", "address": { "city": "Munich" }, "tags": ["vip"], "creditLimit": null }
```

the result is:

``` json
{ "name": "Acme GmbH", "tags": ["vip"], "address": { "city": "Munich" } }
```

Note that `address.zip` is gone, because nested objects merge but the patch supplied a new `address` containing only `city`; `tags` is the single-element array, not an append; and `creditLimit` was deleted. Merge Patch is simple and readable, but it cannot express "set this field to null" or "append one array item" without sending the whole collection.

## JSON Patch (RFC 6902)

Media type: `application/json-patch+json`. The body is an ordered array of operations addressed by JSON Pointer paths. The whole sequence is atomic; any one failure applies nothing.

``` json
[
  { "op": "replace", "path": "/name", "value": "Acme GmbH" },
  { "op": "add", "path": "/tags/-", "value": "mfg" },
  { "op": "remove", "path": "/creditLimit" },
  { "op": "test", "path": "/version", "value": 7 },
  { "op": "replace", "path": "/address/zip", "value": "80331" }
]
```

| Operation       | Effect                                                |
|-----------------|-------------------------------------------------------|
| `add`           | Insert a value; `/tags/-` appends to an array         |
| `remove`        | Delete a field or array element                       |
| `replace`       | Replace an existing value                             |
| `move` / `copy` | Relocate or duplicate a value with `from`             |
| `test`          | Assert a current value; abort the batch if it differs |

The `test` operation is the standout feature: it builds optimistic concurrency into the patch itself. The client asserts the version it read, and the server rejects the batch with `409` or `412` if another write landed first, without a separate `If-Match` header. JSON Patch is more verbose than Merge Patch, but it is the only one that edits arrays precisely and records intent an audit log can replay.

## How to choose

| Need                                                     | Choose                         |
|----------------------------------------------------------|--------------------------------|
| Full replace or client-side upsert                       | `PUT`                          |
| Simple field edits, human-written clients, small objects | Merge Patch                    |
| Append/remove specific array items                       | JSON Patch                     |
| Explicit null that means "store null"                    | `PUT`, or JSON Patch `replace` |
| Atomic multi-field change with a version guard           | JSON Patch with `test`         |
| Consumers that cannot build operation arrays             | Merge Patch                    |

Supporting both is reasonable for different endpoints, but do not accept both media types on one route with subtly different null handling; clients will confuse them.

## Modeling both in OpenAPI

``` yaml
paths:
  /customers/{customerId}:
    patch:
      operationId: patchCustomer
      parameters:
        - $ref: '#/components/parameters/CustomerId'
        - name: If-Match
          in: header
          required: false
          schema: { type: string }
          description: ETag from the last GET; recommended for Merge Patch.
      requestBody:
        required: true
        content:
          application/merge-patch+json:
            schema:
              $ref: '#/components/schemas/CustomerMergePatch'
          application/json-patch+json:
            schema:
              type: array
              minItems: 1
              items: { $ref: '#/components/schemas/JsonPatchOperation' }
      responses:
        '200':
          description: Updated resource
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Customer' }
        '204':
          description: Applied, no body returned
        '409': { description: Version conflict from a test op or If-Match }
        '422': { $ref: '#/components/responses/ValidationError' }
```

For Merge Patch, the request schema is deliberately looser than the full `Customer` (all fields optional), and you document the null-means-delete rule in its description. Do not reuse the full resource schema with `required` intact, or the contract demands fields the client is allowed to omit. For JSON Patch, define `JsonPatchOperation` once with the `op` enum, `path`, optional `from`, and optional `value`, and reference it everywhere.

## What clients, mocks, and AI agents do

Codegen turns the two media types into two methods or two accepted body types, so the choice is explicit at compile time. A spec-driven mock can apply a Merge Patch or a JSON Patch sequence to a fixture and return the patched result, which lets you test the tricky cases: deleting with null, appending to an array, and a failing `test` that must roll the whole batch back.

AI agents benefit from the explicit format more than anyone. Given "PATCH with partial JSON and null means ignore," models send inconsistent bodies; given `application/json-patch+json` and an operation schema, they emit a deterministic, replayable change set. Treating the patch language as part of the contract removes the largest source of AI-generated update bugs.

## Common mistakes

-   Documenting `PATCH` with `application/json` and no patch rules, leaving null undefined.
-   Reusing the full resource schema for a Merge Patch body so "optional" fields are marked required.
-   Expecting Merge Patch to append arrays; it replaces them.
-   Using null to mean both "clear the field" and "no change" on the same endpoint.
-   Applying JSON Patch operations one by one without atomicity, leaving half-applied state.
-   Making `PATCH` casually idempotent claims; a Merge Patch that only sets values is idempotent, but an `add` to `/tags/-` is not, so document per-operation behavior.

## Checklist

1.  Every `PATCH` declares a concrete media type and patch language.
2.  Merge Patch documents null-as-delete and array replacement; its request schema is all-optional.
3.  JSON Patch defines the operation schema and is applied atomically.
4.  Concurrency uses `If-Match` for Merge Patch and a `test` op (or both) for JSON Patch.
5.  Responses distinguish `200` with the new body, `204`, `409`/`412`, and `422`.
6.  Mocks apply the patch to a fixture and verify delete, append, and rollback cases.

Name the patch format and "what does null mean" stops being a support ticket.

Patch a fixture with both media types, run the conflict cases against a mock, and generate explicit client methods, [in the browser app](https://www.powerduck.com/app/?ref=powerduck.com). Tightening how fields can change over time is a compatibility decision too; catch it before release with the [OpenAPI breaking-change diff in CI guide](https://www.powerduck.com/blog/detect-breaking-api-changes-openapi-diff-ci/).

