Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 37 additions & 37 deletions aspnetcore/web-api/jsonpatch.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
---
title: JsonPatch in ASP.NET Core web API
author: wadepickett
description: Learn how to handle JSON Patch requests in an ASP.NET Core web API.
description: "JSON Patch in ASP.NET Core web API: Learn how to handle JSON Patch requests, apply partial updates, and improve API efficiency with System.Text.Json."
monikerRange: '>= aspnetcore-3.1'
ms.author: wpickett
ms.reviewer: wpickett
ms.custom: mvc
ms.date: 06/03/2025
ms.date: 05/27/2026
uid: web-api/jsonpatch
---
# JSON Patch support in ASP.NET Core web API
Expand Down Expand Up @@ -37,14 +38,14 @@ For an overview of the JSON Patch standard, see [jsonpatch.com](https://jsonpatc

## JSON Patch support in ASP.NET Core web API

JSON Patch support in ASP.NET Core web API is based on <xref:System.Text.Json> serialization, starting with .NET 10, implementing <xref:Microsoft.AspNetCore.JsonPatch> based on <xref:System.Text.Json> serialization. This feature:
JSON Patch support in ASP.NET Core web API is based on <xref:System.Text.Json> serialization, starting with .NET 10. It implements <xref:Microsoft.AspNetCore.JsonPatch> based on <xref:System.Text.Json> serialization. This feature:

* Requires the [`Microsoft.AspNetCore.JsonPatch.SystemTextJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.JsonPatch.SystemTextJson) NuGet package.
* Aligns with modern .NET practices by leveraging the <xref:System.Text.Json> library, which is optimized for .NET.
* Provides improved performance and reduced memory usage compared to the legacy `Newtonsoft.Json`-based implementation. For more information on the legacy `Newtonsoft.Json`-based implementation, see the [.NET 9 version of this article](?view=aspnetcore-9.0&preserve-view=true).

> [!NOTE]
> The implementation of <xref:Microsoft.AspNetCore.JsonPatch> based on <xref:System.Text.Json?displayProperty=fullName> serialization isn't a drop-in replacement for the legacy `Newtonsoft.Json`-based implementation. It doesn't support dynamic types, for example <xref:System.Dynamic.ExpandoObject>.
> The implementation of <xref:Microsoft.AspNetCore.JsonPatch> based on <xref:System.Text.Json?displayProperty=fullName> serialization isn't a drop-in replacement for the legacy `Newtonsoft.Json`-based implementation. It doesn't support dynamic types, such as <xref:System.Dynamic.ExpandoObject>.

> [!IMPORTANT]
> The JSON Patch standard has ***inherent security risks***. Since these risks are inherent to the JSON Patch standard, the ASP.NET Core implementation ***doesn't attempt to mitigate inherent security risks***. It's the responsibility of the developer to ensure that the JSON Patch document is safe to apply to the target object. For more information, see the [Mitigating Security Risks](#mitigating-security-risks) section.
Expand All @@ -57,49 +58,53 @@ To enable JSON Patch support with <xref:System.Text.Json>, install the [`Microso
dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson
```

This package provides a <xref:Microsoft.AspNetCore.JsonPatch.SystemTextJson.JsonPatchDocument%601> class to represent a JSON Patch document for objects of type `T` and custom logic for serializing and deserializing JSON Patch documents using <xref:System.Text.Json>. The key method of the <xref:Microsoft.AspNetCore.JsonPatch.SystemTextJson.JsonPatchDocument%601> class is <xref:Microsoft.AspNetCore.JsonPatch.SystemTextJson.JsonPatchDocument.ApplyTo(System.Object)>, which applies the patch operations to a target object of type `T`.
This package provides a <xref:Microsoft.AspNetCore.JsonPatch.SystemTextJson.JsonPatchDocument%601> class to represent a JSON Patch document for objects of type `TModel` and custom logic for serializing and deserializing JSON Patch documents using <xref:System.Text.Json>. The key method of the <xref:Microsoft.AspNetCore.JsonPatch.SystemTextJson.JsonPatchDocument%601> class is <xref:Microsoft.AspNetCore.JsonPatch.SystemTextJson.JsonPatchDocument.ApplyTo(System.Object)>, which applies the patch operations to a target object of type `TModel`.

## Action method code applying JSON Patch
## Minimal API PATCH endpoint applying JSON Patch

In an API controller, an action method for JSON Patch:
In a Minimal API, a PATCH endpoint for JSON Patch:

* Is annotated with the <xref:Microsoft.AspNetCore.Mvc.HttpPatchAttribute> attribute.
* Accepts a <xref:Microsoft.AspNetCore.JsonPatch.SystemTextJson.JsonPatchDocument%601>, typically with [<xref:Microsoft.AspNetCore.Mvc.FromBodyAttribute>](xref:Microsoft.AspNetCore.Mvc.FromBodyAttribute).
* Uses `MapPatch` to define the route.
* Accepts a <xref:Microsoft.AspNetCore.JsonPatch.SystemTextJson.JsonPatchDocument%601> parameter.
* Calls <xref:Microsoft.AspNetCore.JsonPatch.SystemTextJson.JsonPatchDocument.ApplyTo(System.Object)> on the patch document to apply the changes.

### Example Controller Action method:
### Example Minimal API PATCH endpoint

:::code language="csharp" source="~/web-api/jsonpatch/samples/10.x/JsonPatchSample/Controllers/CustomerController.cs" id="snippet_PatchAction" highlight="1,2,14-19":::
:::code language="csharp" source="~/web-api/jsonpatch/samples/10.x/JsonPatchSample/CustomerApi.cs" id="snippet_PatchMethod":::

This code from the sample app works with the following `Customer` and `Order` models:

:::code language="csharp" source="~/web-api/jsonpatch/samples/10.x/JsonPatchSample/Models/Customer.cs":::

:::code language="csharp" source="~/web-api/jsonpatch/samples/10.x/JsonPatchSample/Models/Order.cs":::

The sample action method's key steps:
The sample PATCH endpoint's key steps:

* **Retrieve the Customer**:
* The method retrieves a `Customer` object from the database `AppDb` using the provided id.
* If no `Customer` object is found, it returns a `404 Not Found` response.
* The endpoint retrieves a `Customer` object from the database `AppDb` using the provided `id`.
* If no `Customer` object is found, it returns a `404 Not Found` response via `TypedResults.NotFound()`.
* **Apply JSON Patch**:
* The <xref:Microsoft.AspNetCore.JsonPatch.SystemTextJson.JsonPatchDocument.ApplyTo(System.Object)> method applies the JSON Patch operations from the patchDoc to the retrieved `Customer` object.
* If errors occur during the patch application, such as invalid operations or conflicts, they are captured by an error handling delegate. This delegate adds error messages to the `ModelState` using the type name of the affected object and the error message.
* **Validate ModelState**:
* After applying the patch, the method checks the `ModelState` for errors.
* If the `ModelState` is invalid, such as due to patch errors, it returns a `400 Bad Request` response with the validation errors.
* **Return the Updated Customer**:
* If the patch is successfully applied and the `ModelState` is valid, the method returns the updated `Customer` object in the response.
* The <xref:Microsoft.AspNetCore.JsonPatch.SystemTextJson.JsonPatchDocument.ApplyTo(System.Object)> method applies the JSON Patch operations from the `patchDoc` to the retrieved `Customer` object.
* If errors occur during the patch application, such as invalid operations or conflicts, an error handling delegate captures them. This delegate collects error messages into a dictionary keyed by the type name of the affected object.
* **Return validation errors**:
* If the error handling delegate captures any errors during the patch application, the endpoint returns a `ValidationProblem` response containing the error details via `TypedResults.ValidationProblem(errors)`.
* **Save and return the Updated Customer**:
* If the patch is successfully applied with no errors, the changes are saved to the database and the endpoint returns the updated `Customer` object via `TypedResults.Ok(customer)`.

### Example error response:
### Example error response

The following example shows the body of a `400 Bad Request` response for a JSON Patch operation when the specified path is invalid:
The following example shows the body of a validation problem response for a JSON Patch operation when the specified path is invalid:

```json
{
"Customer": [
"The target location specified by path segment 'foobar' was not found."
]
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Customer": [
"The target location specified by path segment 'foobar' was not found."
]
}
}
```

Expand Down Expand Up @@ -189,7 +194,7 @@ Key differences between <xref:System.Text.Json> and the new <xref:Microsoft.AspN

### Example: Apply a JsonPatchDocument with error handling

There are various errors that can occur when applying a JSON Patch document. For example, the target object may not have the specified property, or the value specified might be incompatible with the property type.
Various errors can occur when applying a JSON Patch document. For example, the target object might not have the specified property, or the value specified might be incompatible with the property type.

JSON `Patch` supports the `test` operation, which checks if a specified value equals the target property. If it doesn't, it returns an error.

Expand Down Expand Up @@ -290,7 +295,7 @@ public void Validate(JsonPatchDocument<T> patch)
}
```

### Business Logic Subversion
### Business logic subversion

* **Scenario**: Patch operations can manipulate fields with implicit invariants (for example, internal flags, IDs, or computed fields), violating business constraints.
* **Impact**: Data integrity issues and unintended app behavior.
Expand All @@ -304,26 +309,21 @@ public void Validate(JsonPatchDocument<T> patch)
* **Scenario**: Unauthenticated or unauthorized clients send malicious JSON Patch requests.
* **Impact**: Unauthorized access to modify sensitive data or disrupt app behavior.
* **Mitigation**:
* Protect endpoints accepting JSON Patch requests with proper authentication and authorization mechanisms.
* Protect endpoints that accept JSON Patch requests by using proper authentication and authorization mechanisms.
* Restrict access to trusted clients or users with appropriate permissions.

## Get the code

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/jsonpatch/samples). ([How to download](xref:fundamentals/index#how-to-download-a-sample)).

To test the sample, run the app and send HTTP requests with the following settings:
[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/jsonpatch/samples/10.x/JsonPatchSample). ([How to download](xref:fundamentals/index#how-to-download-a-sample)).

* URL: `http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate`
* HTTP method: `PATCH`
* Header: `Content-Type: application/json-patch+json`
* Body: Copy and paste one of the JSON patch document samples from the *JSON* project folder.
To test the sample, run the app and send HTTP requests by using the included `.http` file.

## Additional resources

* [IETF RFC 5789 PATCH method specification](https://tools.ietf.org/html/rfc5789)
* [IETF RFC 6902 JSON Patch specification](https://tools.ietf.org/html/rfc6902)
* [IETF RFC 6901 JSON Pointer](https://tools.ietf.org/html/rfc6901)
* [ASP.NET Core JSON Patch source code](https://github.com/dotnet/AspNetCore/tree/main/src/Features/JsonPatch/src)
* [ASP.NET Core JSON Patch source code](https://github.com/dotnet/aspnetcore/tree/main/src/Features/JsonPatch.SystemTextJson/src)

:::moniker-end

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;

using App.Data;
using App.Models;
Expand All @@ -11,27 +13,48 @@ namespace App.Controllers;
public class CustomerController : ControllerBase
{
[HttpGet("{id}", Name = "GetCustomer")]
public Customer Get(AppDb db, string id)
public async Task<IActionResult> Get(AppDb db, string id)
{
// Retrieve the customer by ID
var customer = db.Customers.FirstOrDefault(c => c.Id == id);
var customer = await db.Customers.FirstOrDefaultAsync(c => c.Id == id);

// Return 404 Not Found if customer doesn't exist
if (customer == null)
{
Response.StatusCode = 404;
return null;
return NotFound();
}

return Ok(customer);
}

[HttpPut("{id}", Name = "PutCustomer")]
public async Task<IActionResult> Put(AppDb db, string id, [FromBody] Customer body)
{
var customer = await db.Customers.Include(c => c.Orders).FirstOrDefaultAsync(c => c.Id == id);
if (customer is null)
{
body.Id = id;
db.Customers.Add(body);
await db.SaveChangesAsync();
return CreatedAtRoute("GetCustomer", new { id }, body);
}

return customer;
customer.Name = body.Name;
customer.Email = body.Email;
customer.PhoneNumber = body.PhoneNumber;
customer.Address = body.Address;

await db.SaveChangesAsync();

return Ok(customer);
}

// <snippet_PatchAction>
[HttpPatch("{id}", Name = "UpdateCustomer")]
public IActionResult Update(AppDb db, string id, [FromBody] JsonPatchDocument<Customer> patchDoc)
public async Task<IActionResult> Update(AppDb db, string id, [FromBody] JsonPatchDocument<Customer> patchDoc)
{
// Retrieve the customer by ID
var customer = db.Customers.FirstOrDefault(c => c.Id == id);
var customer = await db.Customers.FirstOrDefaultAsync(c => c.Id == id);

// Return 404 Not Found if customer doesn't exist
if (customer == null)
Expand All @@ -48,9 +71,13 @@ public IActionResult Update(AppDb db, string id, [FromBody] JsonPatchDocument<Cu

if (!ModelState.IsValid)
{
return BadRequest(ModelState);
// return BadRequest(ModelState);
return ValidationProblem(ModelState);
}

// Only save if there are no errors
await db.SaveChangesAsync();

return new ObjectResult(customer);
}
// </snippet_PatchAction>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

using System.ComponentModel.DataAnnotations;
using App.Data;
using App.Models;

Expand All @@ -10,20 +11,42 @@ public static void MapCustomerApi(this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/customers").WithTags("Customers");

group.MapGet("/{id}", async Task<Results<Ok<Customer>, NotFound>> (AppDb db, string id) =>
group.MapGet("/{id}", async Task<Results<Ok<Customer>, NotFound<ProblemDetails>>> (AppDb db, string id) =>
{
return await db.Customers.Include(c => c.Orders).FirstOrDefaultAsync(c => c.Id == id) is Customer customer
? TypedResults.Ok(customer)
: TypedResults.NotFound();
: TypedResults.NotFound<ProblemDetails>(new ());
});

group.MapPatch("/{id}", async Task<Results<Ok<Customer>,NotFound,BadRequest, ValidationProblem>> (AppDb db, string id,
group.MapPut("/{id}", async Task<Results<Ok<Customer>,Created<Customer>,ValidationProblem>> (AppDb db, string id, Customer body) =>
{
var customer = await db.Customers.Include(c => c.Orders).FirstOrDefaultAsync(c => c.Id == id);
if (customer is null)
{
body.Id = id;
db.Customers.Add(body);
await db.SaveChangesAsync();
return TypedResults.Created($"/customers/{id}", body);
}

customer.Name = body.Name;
customer.Email = body.Email;
customer.PhoneNumber = body.PhoneNumber;
customer.Address = body.Address;

await db.SaveChangesAsync();

return TypedResults.Ok(customer);
});

// <snippet_PatchMethod>
group.MapPatch("/{id}", async Task<Results<Ok<Customer>,ValidationProblem,NotFound<ProblemDetails>>> (AppDb db, string id,
JsonPatchDocument<Customer> patchDoc) =>
{
var customer = await db.Customers.Include(c => c.Orders).FirstOrDefaultAsync(c => c.Id == id);
if (customer is null)
{
return TypedResults.NotFound();
return TypedResults.NotFound<ProblemDetails>(new ());
}
if (patchDoc != null)
{
Expand All @@ -38,15 +61,19 @@ public static void MapCustomerApi(this IEndpointRouteBuilder routes)
}
errors[key] = errors[key].Append(jsonPatchError.ErrorMessage).ToArray();
});

if (errors != null)
{
return TypedResults.ValidationProblem(errors);
}

// Only save if there are no errors
await db.SaveChangesAsync();
}

return TypedResults.Ok(customer);
})
.Accepts<JsonPatchDocument<Customer>>("application/json-patch+json");
// </snippet_PatchMethod>
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,32 @@
GET {{HostAddress}}/openapi/v1.json
Accept: application/json

###
### Minimal API requests
###

GET {{HostAddress}}/api/customers/1
GET {{HostAddress}}/customers/1
Accept: application/json

###

PATCH {{HostAddress}}/api/customers/1
PATCH {{HostAddress}}/customers/1
Content-Type: application/json-patch+json
Accept: application/json

[
{
"op": "replace",
"path": "/email",
"value": "foo@bar.baz"
"value": "jdoe456@outlook.com"
}
]

###

# Error response

PATCH {{HostAddress}}/api/customers/1
PATCH {{HostAddress}}/customers/1
Content-Type: application/json-patch+json
Accept: application/json

Expand All @@ -39,31 +41,31 @@ Accept: application/json
]

###
### Minimal API requests
### Controllers requests
###

GET {{HostAddress}}/customers/1
GET {{HostAddress}}/api/customers/1
Accept: application/json

###

PATCH {{HostAddress}}/customers/1
PATCH {{HostAddress}}/api/customers/1
Content-Type: application/json-patch+json
Accept: application/json

[
{
"op": "replace",
"path": "/email",
"value": "foo@bar.baz"
"value": "baz@foo.qux"
}
]

###

# Error response

PATCH {{HostAddress}}/customers/1
PATCH {{HostAddress}}/api/customers/1
Content-Type: application/json-patch+json
Accept: application/json

Expand Down
Loading
Loading