Skip to content

JsonPatch System.Text.Json - based .NET 10 Preview 4 update #35571

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7463a15
JsonPatch System.Text.Json Update .NET 10 Preview 4
wadepickett Jun 4, 2025
fffc025
Fixed link, updated doc meta data
wadepickett Jun 4, 2025
45f0a2f
Fix link
wadepickett Jun 4, 2025
3ac8957
Added Mitigating security risks section to version 9 of article
wadepickett Jun 4, 2025
2637fb4
Added mitigating security section to all previous versions of article
wadepickett Jun 4, 2025
4a9dc04
Removed bold on test behavior
wadepickett Jun 4, 2025
175a38d
Moved Action Method code section back in
wadepickett Jun 4, 2025
9d7a34f
Corrected NuGet link, and mentioned the package link again at top
wadepickett Jun 11, 2025
c0ea3dd
Small edits
wadepickett Jun 11, 2025
47eab6a
Added xref links for all API mention.
wadepickett Jun 12, 2025
f331b66
Per review: For v9 moved up moniker to avoid repeate of security section
wadepickett Jun 12, 2025
7fae625
Per Review: Removed JsonPatch Operations section
wadepickett Jun 12, 2025
6f9b1fa
Fixed dupe mitigate security section
wadepickett Jun 12, 2025
766520f
Apply suggestions from code review
wadepickett Jun 13, 2025
57d4db4
Applied changes per tdystra recommendations
wadepickett Jun 14, 2025
edd8b74
Removed real app warning
wadepickett Jun 16, 2025
5a9b06e
Added Mike's sample and updated action method section
wadepickett Jun 18, 2025
4a6deac
Moved app sample to JsonPatchSample folder
wadepickett Jun 18, 2025
6604d2c
Update aspnetcore/web-api/jsonpatch.md
wadepickett Jun 24, 2025
511a0b6
Updated JsonPatch. to JsonPatch.SystemTextJson. throughout jsonpatch.md
wadepickett Jun 24, 2025
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
378 changes: 225 additions & 153 deletions aspnetcore/web-api/jsonpatch.md

Large diffs are not rendered by default.

65 changes: 57 additions & 8 deletions aspnetcore/web-api/jsonpatch/includes/jsonpatch9.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

This article explains how to handle JSON Patch requests in an ASP.NET Core web API.

> [!IMPORTANT]
> The JSON Patch standard has ***inherent security risks***. This implementation ***doesn't attempt to mitigate these 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.

## Package installation

JSON Patch support in ASP.NET Core web API is based on `Newtonsoft.Json` and requires the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson/) NuGet package. To enable JSON Patch support:
JSON Patch support in ASP.NET Core web API is based on `Newtonsoft.Json` and requires the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson/) NuGet package.

To enable JSON Patch support:

* Install the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson/) NuGet package.
* Call <xref:Microsoft.Extensions.DependencyInjection.NewtonsoftJsonMvcBuilderExtensions.AddNewtonsoftJson%2A>. For example:
Expand Down Expand Up @@ -234,6 +239,53 @@ To test the sample, run the app and send HTTP requests with the following settin
* Header: `Content-Type: application/json-patch+json`
* Body: Copy and paste one of the JSON patch document samples from the *JSON* project folder.

## Mitigating security risks

When using the `Microsoft.AspNetCore.JsonPatch` package with the `Newtonsoft.Json`-based implementation, it's critical to understand and mitigate potential security risks. The following sections outline the identified security risks associated with JSON Patch and provide recommended mitigations to ensure secure usage of the package.

> [!IMPORTANT]
> ***This is not an exhaustive list of threats.*** App developers must conduct their own threat model reviews to determine an app-specific comprehensive list and come up with appropriate mitigations as needed. For example, apps which expose collections to patch operations should consider the potential for algorithmic complexity attacks if those operations insert or remove elements at the beginning of the collection.

By running comprehensive threat models for their own apps and addressing identified threats while following the recommended mitigations below, consumers of these packages can integrate JSON Patch functionality into their apps while minimizing security risks.

### Denial of Service (DoS) via memory amplification

* **Scenario**: A malicious client submits a `copy` operation that duplicates large object graphs multiple times, leading to excessive memory consumption.
* **Impact**: Potential Out-Of-Memory (OOM) conditions, causing service disruptions.
* **Mitigation**:
* Validate incoming JSON Patch documents for size and structure before calling `ApplyTo`.
* The validation needs to be app specific, but an example validation can look similar to the following:

```csharp
public void Validate(JsonPatchDocument patch)
{
// This is just an example. It's up to the developer to make sure that
// this case is handled properly, based on the app needs.
if (patch.Operations.Where(op => op.OperationType == OperationType.Copy).Count()
> MaxCopyOperationsCount)
{
throw new InvalidOperationException();
}
}
```

### 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.
* **Mitigation**:
* Use POCO objects with explicitly defined properties that are safe to modify.
* Avoid exposing sensitive or security-critical properties in the target object.
* If no POCO object is used, validate the patched object after applying operations to ensure business rules and invariants aren't violated.

### Authentication and authorization

* **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.
* Restrict access to trusted clients or users with appropriate permissions.

## Additional resources

* [IETF RFC 5789 PATCH method specification](https://tools.ietf.org/html/rfc5789)
Expand All @@ -247,6 +299,9 @@ To test the sample, run the app and send HTTP requests with the following settin

This article explains how to handle JSON Patch requests in an ASP.NET Core web API.

> [!IMPORTANT]
> The JSON Patch standard has ***inherent security risks***. Since these risks are inherent to the JSON Patch standard, this 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.

## Package installation

To enable JSON Patch support in your app, complete the following steps:
Expand Down Expand Up @@ -476,11 +531,5 @@ To test the sample, run the app and send HTTP requests with the following settin
* Header: `Content-Type: application/json-patch+json`
* Body: Copy and paste one of the JSON patch document samples from the *JSON* project folder.

## 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 Patch path format spec](https://tools.ietf.org/html/rfc6901)
* [ASP.NET Core JSON Patch source code](https://github.com/dotnet/AspNetCore/tree/main/src/Features/JsonPatch/src)

:::moniker-end

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;

using App.Data;
using App.Models;

namespace App.Controllers;

[ApiController]
[Route("/api/customers")]
public class CustomerController : ControllerBase
{
[HttpGet("{id}", Name = "GetCustomer")]
public Customer Get(AppDb db, string id)
{
// Retrieve the customer by ID
var customer = db.Customers.FirstOrDefault(c => c.Id == id);

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

return customer;
}

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

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

patchDoc.ApplyTo(customer, jsonPatchError =>
{
var key = jsonPatchError.AffectedObject.GetType().Name;
ModelState.AddModelError(key, jsonPatchError.ErrorMessage);
}
);

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

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

using App.Data;
using App.Models;

internal static class CustomerApi {
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) =>
{
return await db.Customers.Include(c => c.Orders).FirstOrDefaultAsync(c => c.Id == id) is Customer customer
? TypedResults.Ok(customer)
: TypedResults.NotFound();
});

group.MapPatch("/{id}", async Task<Results<Ok<Customer>,NotFound,BadRequest, ValidationProblem>> (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();
}
if (patchDoc != null)
{
Dictionary<string, string[]>? errors = null;
patchDoc.ApplyTo(customer, jsonPatchError =>
{
errors ??= new ();
var key = jsonPatchError.AffectedObject.GetType().Name;
if (!errors.ContainsKey(key))
{
errors.Add(key, new string[] { });
}
errors[key] = errors[key].Append(jsonPatchError.ErrorMessage).ToArray();
});
if (errors != null)
{
return TypedResults.ValidationProblem(errors);
}
await db.SaveChangesAsync();
}

return TypedResults.Ok(customer);
})
.Accepts<JsonPatchDocument<Customer>>("application/json-patch+json");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore;
using App.Models;

namespace App.Data;

public class AppDb : DbContext
{
public required DbSet<Customer> Customers { get; set; }

public AppDb(DbContextOptions<AppDb> options) : base(options)
{
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

// Configure entity relationships here if needed
modelBuilder.Entity<Customer>()
.HasKey(c => c.Id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using App.Models;
using Microsoft.EntityFrameworkCore;

namespace App.Data;

public static class AppDbSeeder
{
public static async Task Seed(WebApplication app)
{
// Create and seed the database
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
var context = services.GetRequiredService<AppDb>();
context.Database.EnsureCreated();

if (context.Customers.Any())
{
return;
}

Customer[] customers = {
new Customer
{
Id = "1",
Name = "John Doe",
Email = "john.doe@example.com",
PhoneNumber = "555-123-4567",
Address = "123 Main St, Anytown, USA"
},
new Customer
{
Id = "2",
Name = "Jane Smith",
Email = "jane.smith@example.com",
PhoneNumber = "555-987-6543",
Address = "456 Oak Ave, Somewhere, USA"
},
new Customer
{
Id = "3",
Name = "Bob Johnson",
Email = "bob.johnson@example.com",
PhoneNumber = "555-555-5555",
Address = "789 Pine Rd, Elsewhere, USA"
}
};

await context.Customers.AddRangeAsync(customers);
await context.SaveChangesAsync();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.JsonPatch.SystemTextJson" Version="10.0.0-preview.5.25277.114" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.5.25277.114" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0-preview.5.25277.114" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0-preview.5.25277.114" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
@HostAddress = http://localhost:5221

GET {{HostAddress}}/openapi/v1.json
Accept: application/json

###

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

###

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

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

###

# Error response

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

[
{
"op": "add",
"path": "/foobar",
"value": 42
}
]

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

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

###

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

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

###

# Error response

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

[
{
"op": "add",
"path": "/foobar",
"value": 42
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace App.Models;

public class Customer
{
public string Id { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string? PhoneNumber { get; set; }
public string? Address { get; set; }
public List<Order>? Orders { get; set; }

public Customer()
{
Id = Guid.NewGuid().ToString();
}
}
Loading