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
moveandcopy)
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
-
Use DTOs: Apply patches to DTOs, not directly to domain models. This maintains your validation boundaries.
-
Validate After Patching: The patch might create an invalid state. Always call
TryValidateModel()after applying the patch. -
Return 204 No Content: On successful update, return 204 (not 200), as there's no content to return.
-
Error Handling:
ApplyTo()has an overload that acceptsModelStateDictionaryfor 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: