Skip to main content

PATCH

The HTTP PATCH method enables partial updates to resources, allowing clients to modify specific properties without sending the entire resource representation. Unlike PUT, which replaces an entire resource, PATCH applies a set of changes described in the request payload. This approach reduces bandwidth, improves efficiency, and provides fine-grained control over resource modifications.

What is PATCH?

PATCH is an HTTP method defined in RFC 5789 (2010) specifically designed for partial resource updates. While REST purists might argue that PUT should only replace entire resources, the practical need for partial updates led to PATCH's creation.

The fundamental difference:

  • PUT: Replaces the entire resource with the provided representation
  • PATCH: Applies a set of changes to modify specific parts of the resource

Consider a user resource:

{
"id": 123,
"firstName": "Jane",
"lastName": "Smith",
"email": "jane.smith@example.com",
"phoneNumber": "+1-555-0100",
"address": "123 Main St",
"createdAt": "2024-01-15T10:30:00Z"
}

With PUT (to change just the phone number):

PUT /users/123
{
"firstName": "Jane",
"lastName": "Smith",
"email": "jane.smith@example.com",
"phoneNumber": "+1-555-0199", // Changed
"address": "123 Main St"
}

You must send the entire resource, even for a single field change.

With PATCH:

PATCH /users/123
[
{
"op": "replace",
"path": "/phoneNumber",
"value": "+1-555-0199"
}
]

You send only the change instruction.

Why Use PATCH?

1. Bandwidth Efficiency

For resources with many properties, PATCH significantly reduces payload size. This is especially valuable for:

  • Mobile applications with limited bandwidth
  • High-volume APIs processing millions of requests
  • Resources with large nested objects or arrays

2. Partial Knowledge

Clients may not have complete knowledge of a resource but need to update specific fields. PATCH allows focused updates without requiring the client to first fetch the entire resource.

3. Concurrency and Optimistic Locking

PATCH can include operations that test current values before applying changes, enabling safe concurrent updates:

[
{
"op": "test",
"path": "/version",
"value": 5
},
{
"op": "replace",
"path": "/status",
"value": "active"
}
]

This updates status only if the version is still 5, preventing lost updates.

4. Complex Modifications

PATCH operations can express complex changes like array manipulations, property moves, and conditional updates that would be cumbersome or impossible with PUT.

JSON Patch (RFC 6902)

JSON Patch is the standard format for describing PATCH operations. It defines a JSON document structure for expressing a sequence of operations to apply to a target JSON document.

Patch Document Structure

A patch document is a JSON array of operation objects. Each operation has:

  • op: The operation type
  • path: JSON Pointer to the target location
  • value: The value for the operation (not used by all operations)
  • from: Source location (used by move and copy)

The Six Operations

1. Add

Adds a value to an object or inserts into an array.

[
{
"op": "add",
"path": "/tags/-",
"value": "urgent"
}
]

The - character appends to an array.

2. Remove

Removes a value from an object or array.

[
{
"op": "remove",
"path": "/tags/2"
}
]

3. Replace

Replaces a value. Equivalent to remove followed by add.

[
{
"op": "replace",
"path": "/status",
"value": "completed"
}
]

4. Move

Removes a value from one location and adds it to another.

[
{
"op": "move",
"from": "/address/shipping",
"path": "/address/billing"
}
]

5. Copy

Copies a value from one location to another.

[
{
"op": "copy",
"from": "/address/billing",
"path": "/address/shipping"
}
]

6. Test

Tests that a value at the target location equals the specified value. Used for validation and concurrency control.

[
{
"op": "test",
"path": "/version",
"value": 42
}
]

If the test fails, the entire patch is rejected.

JSON Pointer

JSON Pointer (RFC 6901) is the syntax used in the path and from fields:

/property              → obj.property
/property/nested → obj.property.nested
/array/0 → array[0]
/array/- → append to array
/path~0with~1slashes → Escape ~ as ~0, / as ~1

Multiple Operations

Operations are applied sequentially. Each operation sees the result of previous operations:

[
{
"op": "add",
"path": "/tags",
"value": []
},
{
"op": "add",
"path": "/tags/-",
"value": "new"
},
{
"op": "add",
"path": "/tags/-",
"value": "reviewed"
}
]

This creates a tags array and adds two items.

PATCH in ASP.NET Core

ASP.NET Core provides first-class support for PATCH through the JsonPatchDocument<T> class in the Microsoft.AspNetCore.JsonPatch package.

Basic Implementation

[HttpPatch("{id}")]
public async Task<IActionResult> PartiallyUpdateCommand(
int id,
JsonPatchDocument<CommandUpdateDto> patchDoc)
{
if (patchDoc == null)
return BadRequest();

// Get existing resource
var commandModel = await _repository.GetCommandByIdAsync(id);
if (commandModel == null)
return NotFound();

// Map to DTO
var commandToPatch = _mapper.Map<CommandUpdateDto>(commandModel);

// Apply patch
patchDoc.ApplyTo(commandToPatch, ModelState);

// Validate after patching
if (!TryValidateModel(commandToPatch))
return ValidationProblem(ModelState);

// Map back and save
_mapper.Map(commandToPatch, commandModel);
await _repository.UpdateCommandAsync(commandModel);
await _repository.SaveChangesAsync();

return NoContent();
}

Key Implementation Points

  1. Use DTOs: Apply patches to DTOs, not directly to domain models. This maintains your validation boundaries.

  2. Validate After Patching: The patch might create an invalid state. Always call TryValidateModel() after applying the patch.

  3. Return 204 No Content: On successful update, return 204 (not 200), as there's no content to return.

  4. Error Handling: ApplyTo() has an overload that accepts ModelStateDictionary for error reporting.

Content Type

PATCH requests must use the application/json-patch+json content type (not application/json):

PATCH /api/commands/5
Content-Type: application/json-patch+json

[
{
"op": "replace",
"path": "/howTo",
"value": "Updated description"
}
]

ASP.NET Core won't bind the JsonPatchDocument<T> parameter without this content type.

Testing PATCH Endpoints

Using a tool like Postman or curl:

curl -X PATCH "https://localhost:5001/api/commands/5" \
-H "Content-Type: application/json-patch+json" \
-d '[
{
"op": "replace",
"path": "/howTo",
"value": "List all Docker containers"
}
]'

When to Use PATCH vs PUT

Use PUT when:

  • Clients typically update the entire resource
  • Your resource model is simple with few fields
  • Complete replacement semantics are clearer for your use case
  • You want simpler client implementation

Use PATCH when:

  • Resources have many fields, but updates typically affect only a few
  • Bandwidth optimization is important
  • You need fine-grained update control (arrays, nested objects)
  • You want to support optimistic locking with test operations
  • Clients may have partial knowledge of the resource

The Pragmatic Middle Ground

Many REST APIs implement PUT with partial update semantics—you send only the fields you want to update, and omitted fields remain unchanged. This is technically incorrect according to REST purists, but it's pragmatic and widely accepted.

The reality: PATCH endpoints are less common in production APIs than PUT endpoints that allow partial updates. Choose based on your team's preferences, client needs, and whether the additional complexity of JSON Patch provides real value.

Best Practices

1. Validate Paths

Ensure patch operations only target allowed properties:

var allowedPaths = new[] { "/howTo", "/commandLine" };
foreach (var operation in patchDoc.Operations)
{
if (!allowedPaths.Any(p => operation.path.StartsWith(p)))
return BadRequest($"Path {operation.path} is not allowed");
}

2. Don't Allow Patching Computed or Sensitive Fields

Fields like Id, CreatedAt, or computed properties shouldn't be patchable. Keep them out of your update DTOs.

3. Document Expected Operations

Not all six operations may make sense for your resource. Document which operations clients should use.

4. Consider Atomicity

JSON Patch operations are meant to be atomic—either all succeed or all fail. Ensure your implementation maintains this guarantee.

5. Version Your Resources

If using test operations for concurrency control, include version fields in your resources:

[
{
"op": "test",
"path": "/version",
"value": 3
},
{
"op": "replace",
"path": "/status",
"value": "completed"
},
{
"op": "replace",
"path": "/version",
"value": 4
}
]

6. Provide Clear Error Messages

When a patch fails, explain which operation failed and why:

patchDoc.ApplyTo(commandToPatch, ModelState);

if (!ModelState.IsValid)
{
return new UnprocessableEntityObjectResult(ModelState);
}

Common Pitfalls

1. Case Sensitivity

JSON Patch paths are case-sensitive. /Name and /name are different paths. Ensure your serialization settings match your API's casing convention (camelCase vs PascalCase).

2. Missing Validation

Forgetting to validate the model after applying the patch can lead to invalid data being persisted.

3. Not Handling Null Patches

Always check if the JsonPatchDocument parameter is null before using it.

4. Array Index Out of Bounds

Array operations can fail if indices don't exist. Handle these gracefully:

[
{
"op": "remove",
"path": "/tags/99"
}
]

5. Forgetting Content Type

The request must use application/json-patch+json. Without it, ASP.NET Core won't bind the parameter.

The Philosophical Debate

The REST community has long debated PATCH vs. PUT with partial updates. Purists argue:

  • PUT means "replace entirely" as defined by HTTP semantics
  • PATCH is the correct tool for partial updates
  • APIs should be semantically correct

Pragmatists counter:

  • Most developers find PUT with partial updates intuitive
  • JSON Patch adds complexity many use cases don't need
  • Semantic correctness matters less than developer experience

Both perspectives have merit. The key is consistency—whatever approach you choose, apply it consistently across your API and document it clearly.

Further Reading

For official specifications and ASP.NET Core implementation details: