Modeling PATCH Correctly: Merge Patch vs. JSON Patch
AI generated
{ }
GET
REST API · HTTP PATCH · RFC 7396 · RFC 6902
Modeling PATCH Correctly
Merge Patch vs. JSON Patch

HTTP PATCH is the most commonly misimplemented method in REST APIs. The reason: there are two standardized variants with very different semantics, and most implementations invent a third, proprietary variant that is neither documented nor consistent. Merge Patch and JSON Patch solve real problems, but different ones.

16 min read Merge Patch · JSON Patch · null semantics · OpenAPI RFC 7396 · RFC 6902 · HTTP/1.1

1. The PATCH problem: why so many APIs get it wrong

HTTP PATCH was defined in RFC 5789 (2010) as a method for partial modifications to a resource. What RFC 5789 explicitly does not define is how these partial changes should be specified. That is deliberate: PATCH is media-type agnostic. The concrete semantics are determined by the Content-Type. This opens the door to two standardized approaches (Merge Patch and JSON Patch) and countless proprietary variants.

The most common problem in practice: an API accepts PATCH requests with application/json as the Content-Type and a partial resource as the body, without specifying how null values are handled, whether missing fields are ignored or reset to their default, and how array fields are updated. This creates ambiguity for clients and implementation errors on the server side, because different developers on the team interpret the same PATCH logic differently.

The solution is simple: use either application/merge-patch+json (Merge Patch per RFC 7396) or application/json-patch+json (JSON Patch per RFC 6902) as the Content-Type. Both RFCs define exactly how the request body should be interpreted. This eliminates ambiguity for clients and simplifies implementation, because the server has a clear specification to follow.

2. PUT vs. PATCH: the semantic foundation

The difference between PUT and PATCH is fundamental to understanding PATCH semantics. PUT is a complete replacement: the request body contains the entire new representation of the resource. Fields missing from the body are either reset to their default value or deleted. PATCH is a partial modification: only the fields contained in the body are changed, all others remain unchanged.

PUT has the advantage of simple semantics: what you send is what you get. The downside: for large resources, clients must load the complete current version, change one field, and send the entire resource back, with the risk of overwriting changes made in the meantime (the lost-update problem). PATCH solves the lost-update problem when modeled precisely, but is semantically more complex. Both methods have their place. The rule of thumb: PUT for simple, small resources; PATCH for complex resources with many fields or frequent partial updates.

3. Merge Patch (RFC 7396): simple and intuitive

Merge Patch is the simpler of the two PATCH standards. The algorithm is intuitive: the request body is a JSON object that is merged with the resource. Fields in the body overwrite the corresponding fields in the resource. Fields that are not present in the body remain unchanged in the resource. Fields explicitly set to null in the body are removed (deleted) from the resource.

This semantic is sufficient for most use cases: updating individual string fields, toggling boolean fields, partially updating objects. The Content-Type for Merge Patch is application/merge-patch+json. When an API team explicitly uses this Content-Type, it clearly signals: "Our PATCH implementation follows RFC 7396." Clients and tooling can then handle it accordingly.


# Merge Patch, examples with request and effect

# Resource before PATCH:
# {
#   "id": "abc-123",
#   "name": "Summer Sale",
#   "description": "Best deals of the year",
#   "active": true,
#   "tags": ["sale", "summer"],
#   "metadata": { "createdBy": "admin", "version": 1 }
# }

# --- Example 1: Update single field ---
# PATCH /campaigns/abc-123
# Content-Type: application/merge-patch+json
# { "name": "Winter Sale" }
#
# Result:
# { "id": "abc-123", "name": "Winter Sale", "description": "Best deals...", ... }
# Only "name" is changed. All other fields are untouched.

# --- Example 2: Delete a field (set to null) ---
# PATCH /campaigns/abc-123
# Content-Type: application/merge-patch+json
# { "description": null }
#
# Result:
# { "id": "abc-123", "name": "Summer Sale", "active": true, "tags": [...] }
# "description" is REMOVED. This is the key merge-patch null semantic.

# --- Example 3: Partially update a nested object ---
# PATCH /campaigns/abc-123
# Content-Type: application/merge-patch+json
# { "metadata": { "version": 2 } }
#
# Result:
# metadata: { "createdBy": "admin", "version": 2 }
# merge-patch recurses into objects, "createdBy" is preserved.

# --- Example 4: LIMITATION, you cannot remove an array item ---
# PATCH /campaigns/abc-123
# Content-Type: application/merge-patch+json
# { "tags": ["sale"] }
#
# Result: tags is REPLACED with ["sale"], the whole array is swapped.
# Merge Patch cannot add or remove individual array items, use JSON Patch for that.

4. The null pitfall in Merge Patch

The most dangerous aspect of Merge Patch is its null semantics: null means "remove field," not "set field to null." That is counterintuitive for developers used to JSON, where null is often a valid value. The consequence: if a resource has a field that can legitimately be null (for example, endDate: null for an ongoing campaign with no end date), Merge Patch cannot distinguish between "set endDate to null" and "remove endDate from the resource", both are expressed as "endDate": null.

For APIs where fields can legitimately hold null as a value, Merge Patch is the wrong choice. JSON Patch, or a proprietary PATCH semantic with explicit operations, is better suited in that case. This pitfall should be documented explicitly in the OpenAPI documentation of the PATCH endpoint: "Null values remove the field from the resource. To set a nullable field to null, use PUT instead of PATCH."

Another pitfall: Merge Patch cannot modify arrays granularly. An array field in the body always replaces the entire array in the resource. If a client wants to add or remove one element from an array without affecting the others, it must load the entire current array, modify it, and send it back. That is the lost-update problem PATCH is actually supposed to solve, but for arrays, Merge Patch does not solve it.

5. JSON Patch (RFC 6902): powerful and precise

JSON Patch is the more powerful and precise PATCH standard. Instead of a partial resource, the client sends a list of operations to be applied to the resource. Each operation has a type (add, remove, replace, move, copy, test), a path (a JSON Pointer per RFC 6901), and, where applicable, a value.

JSON Patch solves every problem Merge Patch has: fields can be set to null (with replace), arrays can be modified granularly (with add and remove on array indices), and the test operation enables optimistic locking: if a field has the expected value before the change, the operation is applied; if not, the entire patch fails atomically. The price for this power: a more complex syntax that is overkill for simple use cases.


[
  {
    "comment": "Example 1: Replace a field value",
    "op": "replace",
    "path": "/name",
    "value": "Winter Sale 2026"
  },
  {
    "comment": "Example 2: Set a nullable field to null",
    "op": "replace",
    "path": "/endDate",
    "value": null
  },
  {
    "comment": "Example 3: Remove a field entirely",
    "op": "remove",
    "path": "/description"
  },
  {
    "comment": "Example 4: Add an item to an array",
    "op": "add",
    "path": "/tags/-",
    "value": "winter"
  },
  {
    "comment": "Example 5: Remove specific array item by index",
    "op": "remove",
    "path": "/tags/0"
  },
  {
    "comment": "Example 6: test operation, optimistic locking",
    "op": "test",
    "path": "/metadata/version",
    "value": 1
  },
  {
    "comment": "Example 7: Increment version (requires test first)",
    "op": "replace",
    "path": "/metadata/version",
    "value": 2
  },
  {
    "comment": "Example 8: Move a field",
    "op": "move",
    "from": "/oldName",
    "path": "/name"
  },
  {
    "comment": "Example 9: Copy a field",
    "op": "copy",
    "from": "/template/description",
    "path": "/description"
  }
]

6. JSON Patch operations in detail

The six JSON Patch operations cover every modification case. add: adds a value at a path. For arrays, index /- appends at the end. remove: removes the value at the given path. replace: replaces the value (equivalent to remove + add). move: moves a value from one path to another. copy: copies a value from one path to another. test: checks whether the value at the given path matches the expected value; if the check fails, the entire patch is rolled back atomically.

The test operation is the most powerful feature of JSON Patch in practice: it implements optimistic locking at the field level, without needing a separate If-Match header mechanism. If a client wants to ensure that the version field still has the value 1 before setting it to 2, it writes {"op": "test", "path": "/version", "value": 1} before the replace operation. If another client has already changed it to 2 in the meantime, the test operation fails and the entire patch request returns 409 Conflict.

7. Documenting Merge Patch in OpenAPI

Documenting Merge Patch in OpenAPI requires special care with the request body schema. Because Merge Patch makes all fields optional and null carries a special meaning (remove field), the normal resource schema cannot be reused directly. Instead, a separate "patch schema" must be defined that makes all fields optional and nullable, without those fields actually being nullable in the API itself.

The key element: the Content-Type application/merge-patch+json must be explicitly stated in the OpenAPI document as the Content-Type of the request body. This signals to tooling and consumers which standard is used. The description should explain the null semantics: "null removes the field from the resource." Many OpenAPI documents use application/json here and miss the chance to communicate the standard semantics.


# Merge Patch in OpenAPI, correct Content-Type and schema design
paths:
  /campaigns/{id}:
    patch:
      summary: Partially update campaign (Merge Patch, RFC 7396)
      description: |
        Updates campaign fields using JSON Merge Patch semantics (RFC 7396).

        **Merge Patch rules:**
        - Fields present in the request body overwrite the existing value.
        - Fields absent from the request body are left unchanged.
        - Fields explicitly set to `null` are **removed** from the resource.
          To set a nullable field to null, use PUT instead.
        - Arrays are replaced atomically. Individual array item changes are not possible.
          Use JSON Patch (PATCH with application/json-patch+json) for array item operations.
      operationId: patchCampaign
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/merge-patch+json:         # RFC 7396 content type
            schema:
              $ref: '#/components/schemas/CampaignMergePatch'
            examples:
              rename:
                summary: Rename campaign
                value: { "name": "Winter Sale 2026" }
              deactivate:
                summary: Deactivate campaign
                value: { "active": false }
              removeDescription:
                summary: Remove description field
                value: { "description": null }
      responses:
        '200':
          description: Updated campaign
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Campaign' }
        '400':
          description: Invalid patch document
        '404':
          description: Campaign not found
        '409':
          description: Conflict, resource modified by another request
        '422':
          description: Patch would result in invalid resource state

components:
  schemas:
    CampaignMergePatch:
      description: |
        Merge Patch document for Campaign. All fields are optional.
        Set a field to null to remove it. Absent fields are unchanged.
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
          nullable: true        # null = remove field
        description:
          type: string
          maxLength: 2000
          nullable: true
        active:
          type: boolean
          nullable: true
        tags:
          type: array
          items: { type: string }
          nullable: true        # null = remove field; array value = replace entire array
          description: Replaces the entire tags array. Cannot add/remove individual items.

8. Documenting JSON Patch in OpenAPI

Documenting JSON Patch in OpenAPI is straightforward: the Content-Type is application/json-patch+json, and the request body is an array of operation objects. Because JSON Patch has a standardized format, the schema from the RFC can be defined directly as an OpenAPI component. The schema is not especially complex, but the combination of oneOf for the different operation types and JSON Pointer paths makes it somewhat more verbose.

A pragmatic approach: define the complete JSON Patch schema as a reusable component that can be referenced by every PATCH endpoint that uses JSON Patch. This avoids repetition and ensures that all JSON Patch endpoints use the same schema format. Examples in the OpenAPI documentation are especially important for JSON Patch, because the syntax is unfamiliar to many developers.

Criterion Merge Patch (RFC 7396) JSON Patch (RFC 6902) Recommendation
Client complexity Low (intuitive) Medium (operation syntax) Merge Patch for simple cases
null semantics null = remove field null = actual null value JSON Patch for nullable fields
Array modification Full replacement only add/remove individual items JSON Patch for array operations
Optimistic locking No (via If-Match header) Yes (test operation) JSON Patch for locking
Content-Type application/merge-patch+json application/json-patch+json Always set explicitly

# JSON Patch in OpenAPI, schema and endpoint definition
components:
  schemas:
    JsonPatchOperation:
      type: object
      required: [op, path]
      properties:
        op:
          type: string
          enum: [add, remove, replace, move, copy, test]
          description: Operation type per RFC 6902
        path:
          type: string
          description: JSON Pointer (RFC 6901) to the target location
          example: /name
        value:
          description: Value to apply (required for add, replace, test)
        from:
          type: string
          description: Source path (required for move, copy)
          example: /oldName

    JsonPatchDocument:
      type: array
      items:
        $ref: '#/components/schemas/JsonPatchOperation'
      description: Array of JSON Patch operations applied atomically (RFC 6902)

paths:
  /campaigns/{id}:
    patch:
      summary: Patch campaign (JSON Patch, RFC 6902)
      description: |
        Applies a JSON Patch document to the campaign resource (RFC 6902).

        Operations are applied atomically. If any operation fails, the entire
        patch is rolled back and the resource is unchanged.

        Use the **test** operation for optimistic locking:
        ```json
        [
          {"op": "test", "path": "/metadata/version", "value": 5},
          {"op": "replace", "path": "/name", "value": "New Name"},
          {"op": "replace", "path": "/metadata/version", "value": 6}
        ]
        ```
        Returns 409 if the test fails (concurrent modification detected).
      requestBody:
        required: true
        content:
          application/json-patch+json:          # RFC 6902 content type
            schema:
              $ref: '#/components/schemas/JsonPatchDocument'
            examples:
              addTag:
                summary: Add a tag to the array
                value:
                  - op: add
                    path: /tags/-
                    value: winter
              removeTag:
                summary: Remove first tag
                value:
                  - op: remove
                    path: /tags/0
              setNullableField:
                summary: Set nullable endDate to null
                value:
                  - op: replace
                    path: /endDate
                    value: null
      responses:
        '200': { description: Patched campaign }
        '400': { description: Invalid patch document syntax }
        '404': { description: Campaign not found }
        '409': { description: test operation failed, optimistic lock conflict }
        '422': { description: Patch results in invalid resource state }

10. Summary

Modeling PATCH correctly means committing to one of the two standards and consistently using it with the right Content-Type. Merge Patch (RFC 7396) is the right choice for simple partial updates where no fields are nullable and no granular array operations are needed. JSON Patch (RFC 6902) is the right choice when nullable fields, array operations, or optimistic locking are needed.

Both standards should be documented in OpenAPI with the correct Content-Type (application/merge-patch+json or application/json-patch+json respectively), not with the generic application/json. The null semantics of Merge Patch must be explained explicitly in the description field of the PATCH endpoint. JSON Patch needs good examples in the OpenAPI documentation, because the syntax is unfamiliar. Avoid proprietary PATCH implementations without a Content-Type convention, they create ambiguity for clients and implementation problems within the team.

Modeling PATCH: the essentials at a glance

Merge Patch

RFC 7396. Content-Type: application/merge-patch+json. Simple, intuitive. null = remove field (pitfall!). Arrays are replaced entirely. No optimistic locking.

JSON Patch

RFC 6902. Content-Type: application/json-patch+json. Atomic operations. null is an actual null value. Granular array operations. test operation for optimistic locking.

Decision guide

Merge Patch for simple fields without nullable values and without array operations. JSON Patch for nullable fields, array modification, or locking. Avoid proprietary PATCH semantics.

OpenAPI

State the correct Content-Type (not application/json). Keep the patch schema separate from the resource schema. Document null semantics in the description. Examples for JSON Patch are mandatory.

11. FAQ: PATCH, Merge Patch, and JSON Patch

1PUT vs. PATCH: main difference?
PUT replaces the resource completely. PATCH modifies it partially. PUT = simple semantics. PATCH = prevents the lost-update problem for large resources.
2null in Merge Patch?
null = remove the field from the resource. Not a valid value for nullable fields. Pitfall: use JSON Patch or PUT for nullable fields.
3Single array element with Merge Patch?
Not possible. Merge Patch always replaces the entire array. For granular array operations, use JSON Patch with add on /tags/- or remove on /tags/0.
4JSON Patch test operation?
Checks a field value before the change. If it fails, 409, and all operations are rolled back. Optimistic locking at the field level, without a separate If-Match header.
5Which Content-Type for PATCH?
application/merge-patch+json for Merge Patch. application/json-patch+json for JSON Patch. Never application/json without specification, that creates ambiguity.
6Separate schema for PATCH body?
For Merge Patch, yes: all fields optional, nullable set. The normal resource schema cannot be used directly. For JSON Patch, the generic schema can be reused.
7What is a JSON Pointer?
RFC 6901. /name = the name field. /tags/0 = the first array element. /tags/- = the end of the array. Slashes in field names are escaped with ~1.
8Are JSON Patch operations atomic?
Yes. If an operation fails, all previous ones are rolled back. The resource remains unchanged. This also applies to the test operation.
9Both variants on the same endpoint?
Valid. The server dispatches based on the Content-Type header. Useful when clients have different requirements: simple clients use Merge Patch, complex clients use JSON Patch.
10Testing Merge Patch with curl?
curl -X PATCH URL -H 'Content-Type: application/merge-patch+json' -d '{"name": "New Name"}'. The Content-Type header is essential for the correct patch semantics on the server side.