Σημείωση
Η πρόσβαση σε αυτήν τη σελίδα απαιτεί εξουσιοδότηση. Μπορείτε να δοκιμάσετε να εισέλθετε ή να αλλάξετε καταλόγους.
Η πρόσβαση σε αυτήν τη σελίδα απαιτεί εξουσιοδότηση. Μπορείτε να δοκιμάσετε να αλλάξετε καταλόγους.
This article explains how to handle JSON Patch requests in an ASP.NET Core web API.
JSON Patch support in ASP.NET Core web API is based on System.Text.Json serialization, and requires the Microsoft.AspNetCore.JsonPatch.SystemTextJson
NuGet package.
What is the JSON Patch standard?
The JSON Patch standard:
Is a standard format for describing changes to apply to a JSON document.
Is defined in RFC 6902 and is widely used in RESTful APIs to perform partial updates to JSON resources.
Describes a sequence of operations that modify a JSON document such as:
add
remove
replace
move
copy
test
In web apps, JSON Patch is commonly used in a PATCH operation to perform partial updates of a resource. Rather than sending the entire resource for an update, clients can send a JSON Patch document containing only the changes. Patching reduces payload size and improves efficiency.
For an overview of the JSON Patch standard, see jsonpatch.com.
JSON Patch support in ASP.NET Core web API
JSON Patch support in ASP.NET Core web API is based on System.Text.Json serialization, starting with .NET 10, implementing Microsoft.AspNetCore.JsonPatch based on System.Text.Json serialization. This feature:
- Requires the
Microsoft.AspNetCore.JsonPatch.SystemTextJson
NuGet package. - Aligns with modern .NET practices by leveraging the 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 legacyNewtonsoft.Json
-based implementation, see the .NET 9 version of this article.
Note
The implementation of Microsoft.AspNetCore.JsonPatch based on System.Text.Json serialization isn't a drop-in replacement for the legacy Newtonsoft.Json
-based implementation. It doesn't support dynamic types, for example 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 section.
Enable JSON Patch support with System.Text.Json
To enable JSON Patch support with System.Text.Json, install the Microsoft.AspNetCore.JsonPatch.SystemTextJson
NuGet package.
dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson --prerelease
This package provides a JsonPatchDocument<TModel> class to represent a JSON Patch document for objects of type T
and custom logic for serializing and deserializing JSON Patch documents using System.Text.Json. The key method of the JsonPatchDocument<TModel> class is ApplyTo(Object), which applies the patch operations to a target object of type T
.
Action method code applying JSON Patch
In an API controller, an action method for JSON Patch:
- Is annotated with the HttpPatchAttribute attribute.
- Accepts a JsonPatchDocument<TModel>, typically with FromBodyAttribute.
- Calls ApplyTo(Object) on the patch document to apply the changes.
Example Controller Action method:
[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);
}
This code from the sample app works with the following Customer
and Order
models:
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();
}
}
namespace App.Models;
public class Order
{
public string Id { get; set; }
public DateTime? OrderDate { get; set; }
public DateTime? ShipDate { get; set; }
public decimal TotalAmount { get; set; }
public Order()
{
Id = Guid.NewGuid().ToString();
}
}
The sample action method's key steps:
- Retrieve the Customer:
- The method retrieves a
Customer
object from the databaseAppDb
using the provided id. - If no
Customer
object is found, it returns a404 Not Found
response.
- The method retrieves a
- Apply JSON Patch:
- The ApplyTo(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.
- The ApplyTo(Object) method applies the JSON Patch operations from the patchDoc to the retrieved
- 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 a400 Bad Request
response with the validation errors.
- After applying the patch, the method checks the
- Return the Updated Customer:
- If the patch is successfully applied and the
ModelState
is valid, the method returns the updatedCustomer
object in the response.
- If the patch is successfully applied and the
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:
{
"Customer": [
"The target location specified by path segment 'foobar' was not found."
]
}
Apply a JSON Patch document to an object
The following examples demonstrate how to use the ApplyTo(Object) method to apply a JSON Patch document to an object.
Example: Apply a JsonPatchDocument<TModel> to an object
The following example demonstrates:
- The
add
,replace
, andremove
operations. - Operations on nested properties.
- Adding a new item to an array.
- Using a JSON String Enum Converter in a JSON patch document.
// Original object
var person = new Person {
FirstName = "John",
LastName = "Doe",
Email = "johndoe@gmail.com",
PhoneNumbers = [new() {Number = "123-456-7890", Type = PhoneNumberType.Mobile}],
Address = new Address
{
Street = "123 Main St",
City = "Anytown",
State = "TX"
}
};
// Raw JSON patch document
string jsonPatch = """
[
{ "op": "replace", "path": "/FirstName", "value": "Jane" },
{ "op": "remove", "path": "/Email"},
{ "op": "add", "path": "/Address/ZipCode", "value": "90210" },
{ "op": "add", "path": "/PhoneNumbers/-", "value": { "Number": "987-654-3210",
"Type": "Work" } }
]
""";
// Deserialize the JSON patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);
// Apply the JSON patch document
patchDoc!.ApplyTo(person);
// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));
The previous example results in the following output of the updated object:
{
"firstName": "Jane",
"lastName": "Doe",
"address": {
"street": "123 Main St",
"city": "Anytown",
"state": "TX",
"zipCode": "90210"
},
"phoneNumbers": [
{
"number": "123-456-7890",
"type": "Mobile"
},
{
"number": "987-654-3210",
"type": "Work"
}
]
}
The ApplyTo(Object) method generally follows the conventions and options of System.Text.Json for processing the JsonPatchDocument<TModel>, including the behavior controlled by the following options:
- JsonNumberHandling: Whether numeric properties are read from strings.
- PropertyNameCaseInsensitive: Whether property names are case-sensitive.
Key differences between System.Text.Json and the new JsonPatchDocument<TModel> implementation:
- The runtime type of the target object, not the declared type, determines which properties ApplyTo(Object) patches.
- System.Text.Json deserialization relies on the declared type to identify eligible properties.
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.
JSON Patch
supports the test
operation, which checks if a specified value equals the target property. If it doesn't, it returns an error.
The following example demonstrates how to handle these errors gracefully.
Important
The object passed to the ApplyTo(Object) method is modified in place. The caller is responsible for discarding changes if any operation fails.
// Original object
var person = new Person {
FirstName = "John",
LastName = "Doe",
Email = "johndoe@gmail.com"
};
// Raw JSON patch document
string jsonPatch = """
[
{ "op": "replace", "path": "/Email", "value": "janedoe@gmail.com"},
{ "op": "test", "path": "/FirstName", "value": "Jane" },
{ "op": "replace", "path": "/LastName", "value": "Smith" }
]
""";
// Deserialize the JSON patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);
// Apply the JSON patch document, catching any errors
Dictionary<string, string[]>? errors = null;
patchDoc!.ApplyTo(person, 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)
{
// Print the errors
foreach (var error in errors)
{
Console.WriteLine($"Error in {error.Key}: {string.Join(", ", error.Value)}");
}
}
// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));
The previous example results in the following output:
Error in Person: The current value 'John' at path 'FirstName' is not equal
to the test value 'Jane'.
{
"firstName": "John",
"lastName": "Smith", <<< Modified!
"email": "janedoe@gmail.com", <<< Modified!
"phoneNumbers": []
}
Mitigating security risks
When using the Microsoft.AspNetCore.JsonPatch.SystemTextJson
package, 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.
To minimize security risks when integrating JSON Patch functionality into their apps, developers should:
- Run comprehensive threat models for their own apps.
- Address identified threats.
- Follow the recommended mitigations in the following sections.
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(Object).
- The validation must be app specific, but an example validation can look similar to the following:
public void Validate(JsonPatchDocument<T> 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 POCOs (Plain Old CLR Objects) with explicitly defined properties that are safe to modify.
- Avoid exposing sensitive or security-critical properties in the target object.
- If a POCO object isn't used, validate the patched object after applying operations to ensure business rules and invariants aren't violated.
- Use POCOs (Plain Old CLR Objects) with explicitly defined properties that are safe to modify.
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.
Get the code
View or download sample code. (How to download).
To test the sample, run the app and send HTTP requests with the following settings:
- 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.
Additional resources
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 section.
Package installation
JSON Patch support in ASP.NET Core web API is based on Newtonsoft.Json
and requires the Microsoft.AspNetCore.Mvc.NewtonsoftJson
NuGet package.
To enable JSON Patch support:
Install the
Microsoft.AspNetCore.Mvc.NewtonsoftJson
NuGet package.Call AddNewtonsoftJson. For example:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers() .AddNewtonsoftJson(); var app = builder.Build(); app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
AddNewtonsoftJson
replaces the default System.Text.Json
-based input and output formatters used for formatting all JSON content. This extension method is compatible with the following MVC service registration methods:
JsonPatch requires setting the Content-Type
header to application/json-patch+json
.
Add support for JSON Patch when using System.Text.Json
The System.Text.Json
-based input formatter doesn't support JSON Patch. To add support for JSON Patch using Newtonsoft.Json
, while leaving the other input and output formatters unchanged:
Install the
Microsoft.AspNetCore.Mvc.NewtonsoftJson
NuGet package.Update
Program.cs
:using JsonPatchSample; using Microsoft.AspNetCore.Mvc.Formatters; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(options => { options.InputFormatters.Insert(0, MyJPIF.GetJsonPatchInputFormatter()); }); var app = builder.Build(); app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.Options; namespace JsonPatchSample; public static class MyJPIF { public static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter() { var builder = new ServiceCollection() .AddLogging() .AddMvc() .AddNewtonsoftJson() .Services.BuildServiceProvider(); return builder .GetRequiredService<IOptions<MvcOptions>>() .Value .InputFormatters .OfType<NewtonsoftJsonPatchInputFormatter>() .First(); } }
The preceding code creates an instance of NewtonsoftJsonPatchInputFormatter and inserts it as the first entry in the MvcOptions.InputFormatters collection. This order of registration ensures that:
NewtonsoftJsonPatchInputFormatter
processes JSON Patch requests.- The existing
System.Text.Json
-based input and formatters process all other JSON requests and responses.
Use the Newtonsoft.Json.JsonConvert.SerializeObject
method to serialize a JsonPatchDocument.
PATCH HTTP request method
The PUT and PATCH methods are used to update an existing resource. The difference between them is that PUT replaces the entire resource, while PATCH specifies only the changes.
JSON Patch
JSON Patch is a format for specifying updates to be applied to a resource. A JSON Patch document has an array of operations. Each operation identifies a particular type of change. Examples of such changes include adding an array element or replacing a property value.
For example, the following JSON documents represent a resource, a JSON Patch document for the resource, and the result of applying the Patch operations.
Resource example
{
"customerName": "John",
"orders": [
{
"orderName": "Order0",
"orderType": null
},
{
"orderName": "Order1",
"orderType": null
}
]
}
JSON patch example
[
{
"op": "add",
"path": "/customerName",
"value": "Barry"
},
{
"op": "add",
"path": "/orders/-",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
In the preceding JSON:
- The
op
property indicates the type of operation. - The
path
property indicates the element to update. - The
value
property provides the new value.
Resource after patch
Here's the resource after applying the preceding JSON Patch document:
{
"customerName": "Barry",
"orders": [
{
"orderName": "Order0",
"orderType": null
},
{
"orderName": "Order1",
"orderType": null
},
{
"orderName": "Order2",
"orderType": null
}
]
}
The changes made by applying a JSON Patch document to a resource are atomic. If any operation in the list fails, no operation in the list is applied.
Path syntax
The path property of an operation object has slashes between levels. For example, "/address/zipCode"
.
Zero-based indexes are used to specify array elements. The first element of the addresses
array would be at /addresses/0
. To add
to the end of an array, use a hyphen (-
) rather than an index number: /addresses/-
.
Operations
The following table shows supported operations as defined in the JSON Patch specification:
Operation | Notes |
---|---|
add |
Add a property or array element. For existing property: set value. |
remove |
Remove a property or array element. |
replace |
Same as remove followed by add at same location. |
move |
Same as remove from source followed by add to destination using value from source. |
copy |
Same as add to destination using value from source. |
test |
Return success status code if value at path = provided value . |
JSON Patch in ASP.NET Core
The ASP.NET Core implementation of JSON Patch is provided in the Microsoft.AspNetCore.JsonPatch NuGet package.
Action method code
In an API controller, an action method for JSON Patch:
- Is annotated with the
HttpPatch
attribute. - Accepts a JsonPatchDocument<TModel>, typically with
[FromBody]
. - Calls ApplyTo(Object) on the patch document to apply the changes.
Here's an example:
[HttpPatch]
public IActionResult JsonPatchWithModelState(
[FromBody] JsonPatchDocument<Customer> patchDoc)
{
if (patchDoc != null)
{
var customer = CreateCustomer();
patchDoc.ApplyTo(customer, ModelState);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return new ObjectResult(customer);
}
else
{
return BadRequest(ModelState);
}
}
This code from the sample app works with the following Customer
model:
namespace JsonPatchSample.Models;
public class Customer
{
public string? CustomerName { get; set; }
public List<Order>? Orders { get; set; }
}
namespace JsonPatchSample.Models;
public class Order
{
public string OrderName { get; set; }
public string OrderType { get; set; }
}
The sample action method:
- Constructs a
Customer
. - Applies the patch.
- Returns the result in the body of the response.
In a real app, the code would retrieve the data from a store such as a database and update the database after applying the patch.
Model state
The preceding action method example calls an overload of ApplyTo
that takes model state as one of its parameters. With this option, you can get error messages in responses. The following example shows the body of a 400 Bad Request response for a test
operation:
{
"Customer": [
"The current value 'John' at path 'customerName' != test value 'Nancy'."
]
}
Dynamic objects
The following action method example shows how to apply a patch to a dynamic object:
[HttpPatch]
public IActionResult JsonPatchForDynamic([FromBody]JsonPatchDocument patch)
{
dynamic obj = new ExpandoObject();
patch.ApplyTo(obj);
return Ok(obj);
}
The add operation
- If
path
points to an array element: inserts new element before the one specified bypath
. - If
path
points to a property: sets the property value. - If
path
points to a nonexistent location:- If the resource to patch is a dynamic object: adds a property.
- If the resource to patch is a static object: the request fails.
The following sample patch document sets the value of CustomerName
and adds an Order
object to the end of the Orders
array.
[
{
"op": "add",
"path": "/customerName",
"value": "Barry"
},
{
"op": "add",
"path": "/orders/-",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
The remove operation
- If
path
points to an array element: removes the element. - If
path
points to a property:- If resource to patch is a dynamic object: removes the property.
- If resource to patch is a static object:
- If the property is nullable: sets it to null.
- If the property is non-nullable, sets it to
default<T>
.
The following sample patch document sets CustomerName
to null and deletes Orders[0]
:
[
{
"op": "remove",
"path": "/customerName"
},
{
"op": "remove",
"path": "/orders/0"
}
]
The replace operation
This operation is functionally the same as a remove
followed by an add
.
The following sample patch document sets the value of CustomerName
and replaces Orders[0]
with a new Order
object:
[
{
"op": "replace",
"path": "/customerName",
"value": "Barry"
},
{
"op": "replace",
"path": "/orders/0",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
The move operation
- If
path
points to an array element: copiesfrom
element to location ofpath
element, then runs aremove
operation on thefrom
element. - If
path
points to a property: copies value offrom
property topath
property, then runs aremove
operation on thefrom
property. - If
path
points to a nonexistent property:- If the resource to patch is a static object: the request fails.
- If the resource to patch is a dynamic object: copies
from
property to location indicated bypath
, then runs aremove
operation on thefrom
property.
The following sample patch document:
- Copies the value of
Orders[0].OrderName
toCustomerName
. - Sets
Orders[0].OrderName
to null. - Moves
Orders[1]
to beforeOrders[0]
.
[
{
"op": "move",
"from": "/orders/0/orderName",
"path": "/customerName"
},
{
"op": "move",
"from": "/orders/1",
"path": "/orders/0"
}
]
The copy operation
This operation is functionally the same as a move
operation without the final remove
step.
The following sample patch document:
- Copies the value of
Orders[0].OrderName
toCustomerName
. - Inserts a copy of
Orders[1]
beforeOrders[0]
.
[
{
"op": "copy",
"from": "/orders/0/orderName",
"path": "/customerName"
},
{
"op": "copy",
"from": "/orders/1",
"path": "/orders/0"
}
]
The test operation
If the value at the location indicated by path
is different from the value provided in value
, the request fails. In that case, the whole PATCH request fails even if all other operations in the patch document would otherwise succeed.
The test
operation is commonly used to prevent an update when there's a concurrency conflict.
The following sample patch document has no effect if the initial value of CustomerName
is "John", because the test fails:
[
{
"op": "test",
"path": "/customerName",
"value": "Nancy"
},
{
"op": "add",
"path": "/customerName",
"value": "Barry"
}
]
Get the code
View or download sample code. (How to download).
To test the sample, run the app and send HTTP requests with the following settings:
- 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.
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:
- Validate incoming JSON Patch documents for size and structure before calling
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
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 section.
Package installation
To enable JSON Patch support in your app, complete the following steps:
Install the
Microsoft.AspNetCore.Mvc.NewtonsoftJson
NuGet package.Update the project's
Startup.ConfigureServices
method to call AddNewtonsoftJson. For example:services .AddControllersWithViews() .AddNewtonsoftJson();
AddNewtonsoftJson
is compatible with the MVC service registration methods:
JSON Patch, AddNewtonsoftJson, and System.Text.Json
AddNewtonsoftJson
replaces the System.Text.Json
-based input and output formatters used for formatting all JSON content. To add support for JSON Patch using Newtonsoft.Json
, while leaving the other formatters unchanged, update the project's Startup.ConfigureServices
method as follows:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews(options =>
{
options.InputFormatters.Insert(0, GetJsonPatchInputFormatter());
});
}
private static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter()
{
var builder = new ServiceCollection()
.AddLogging()
.AddMvc()
.AddNewtonsoftJson()
.Services.BuildServiceProvider();
return builder
.GetRequiredService<IOptions<MvcOptions>>()
.Value
.InputFormatters
.OfType<NewtonsoftJsonPatchInputFormatter>()
.First();
}
The preceding code requires the Microsoft.AspNetCore.Mvc.NewtonsoftJson
package and the following using
statements:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using System.Linq;
Use the Newtonsoft.Json.JsonConvert.SerializeObject
method to serialize a JsonPatchDocument.
PATCH HTTP request method
The PUT and PATCH methods are used to update an existing resource. The difference between them is that PUT replaces the entire resource, while PATCH specifies only the changes.
JSON Patch
JSON Patch is a format for specifying updates to be applied to a resource. A JSON Patch document has an array of operations. Each operation identifies a particular type of change. Examples of such changes include adding an array element or replacing a property value.
For example, the following JSON documents represent a resource, a JSON Patch document for the resource, and the result of applying the Patch operations.
Resource example
{
"customerName": "John",
"orders": [
{
"orderName": "Order0",
"orderType": null
},
{
"orderName": "Order1",
"orderType": null
}
]
}
JSON patch example
[
{
"op": "add",
"path": "/customerName",
"value": "Barry"
},
{
"op": "add",
"path": "/orders/-",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
In the preceding JSON:
- The
op
property indicates the type of operation. - The
path
property indicates the element to update. - The
value
property provides the new value.
Resource after patch
Here's the resource after applying the preceding JSON Patch document:
{
"customerName": "Barry",
"orders": [
{
"orderName": "Order0",
"orderType": null
},
{
"orderName": "Order1",
"orderType": null
},
{
"orderName": "Order2",
"orderType": null
}
]
}
The changes made by applying a JSON Patch document to a resource are atomic. If any operation in the list fails, no operation in the list is applied.
Path syntax
The path property of an operation object has slashes between levels. For example, "/address/zipCode"
.
Zero-based indexes are used to specify array elements. The first element of the addresses
array would be at /addresses/0
. To add
to the end of an array, use a hyphen (-
) rather than an index number: /addresses/-
.
Operations
The following table shows supported operations as defined in the JSON Patch specification:
Operation | Notes |
---|---|
add |
Add a property or array element. For existing property: set value. |
remove |
Remove a property or array element. |
replace |
Same as remove followed by add at same location. |
move |
Same as remove from source followed by add to destination using value from source. |
copy |
Same as add to destination using value from source. |
test |
Return success status code if value at path = provided value . |
JSON Patch in ASP.NET Core
The ASP.NET Core implementation of JSON Patch is provided in the Microsoft.AspNetCore.JsonPatch NuGet package.
Action method code
In an API controller, an action method for JSON Patch:
- Is annotated with the
HttpPatch
attribute. - Accepts a
JsonPatchDocument<T>
, typically with[FromBody]
. - Calls
ApplyTo
on the patch document to apply the changes.
Here's an example:
[HttpPatch]
public IActionResult JsonPatchWithModelState(
[FromBody] JsonPatchDocument<Customer> patchDoc)
{
if (patchDoc != null)
{
var customer = CreateCustomer();
patchDoc.ApplyTo(customer, ModelState);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return new ObjectResult(customer);
}
else
{
return BadRequest(ModelState);
}
}
This code from the sample app works with the following Customer
model:
using System.Collections.Generic;
namespace JsonPatchSample.Models
{
public class Customer
{
public string CustomerName { get; set; }
public List<Order> Orders { get; set; }
}
}
namespace JsonPatchSample.Models
{
public class Order
{
public string OrderName { get; set; }
public string OrderType { get; set; }
}
}
The sample action method:
- Constructs a
Customer
. - Applies the patch.
- Returns the result in the body of the response.
In a real app, the code would retrieve the data from a store such as a database and update the database after applying the patch.
Model state
The preceding action method example calls an overload of ApplyTo
that takes model state as one of its parameters. With this option, you can get error messages in responses. The following example shows the body of a 400 Bad Request response for a test
operation:
{
"Customer": [
"The current value 'John' at path 'customerName' is not equal to the test value 'Nancy'."
]
}
Dynamic objects
The following action method example shows how to apply a patch to a dynamic object:
[HttpPatch]
public IActionResult JsonPatchForDynamic([FromBody]JsonPatchDocument patch)
{
dynamic obj = new ExpandoObject();
patch.ApplyTo(obj);
return Ok(obj);
}
The add operation
- If
path
points to an array element: inserts new element before the one specified bypath
. - If
path
points to a property: sets the property value. - If
path
points to a nonexistent location:- If the resource to patch is a dynamic object: adds a property.
- If the resource to patch is a static object: the request fails.
The following sample patch document sets the value of CustomerName
and adds an Order
object to the end of the Orders
array.
[
{
"op": "add",
"path": "/customerName",
"value": "Barry"
},
{
"op": "add",
"path": "/orders/-",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
The remove operation
- If
path
points to an array element: removes the element. - If
path
points to a property:- If resource to patch is a dynamic object: removes the property.
- If resource to patch is a static object:
- If the property is nullable: sets it to null.
- If the property is non-nullable, sets it to
default<T>
.
The following sample patch document sets CustomerName
to null and deletes Orders[0]
:
[
{
"op": "remove",
"path": "/customerName"
},
{
"op": "remove",
"path": "/orders/0"
}
]
The replace operation
This operation is functionally the same as a remove
followed by an add
.
The following sample patch document sets the value of CustomerName
and replaces Orders[0]
with a new Order
object:
[
{
"op": "replace",
"path": "/customerName",
"value": "Barry"
},
{
"op": "replace",
"path": "/orders/0",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
The move operation
- If
path
points to an array element: copiesfrom
element to location ofpath
element, then runs aremove
operation on thefrom
element. - If
path
points to a property: copies value offrom
property topath
property, then runs aremove
operation on thefrom
property. - If
path
points to a nonexistent property:- If the resource to patch is a static object: the request fails.
- If the resource to patch is a dynamic object: copies
from
property to location indicated bypath
, then runs aremove
operation on thefrom
property.
The following sample patch document:
- Copies the value of
Orders[0].OrderName
toCustomerName
. - Sets
Orders[0].OrderName
to null. - Moves
Orders[1]
to beforeOrders[0]
.
[
{
"op": "move",
"from": "/orders/0/orderName",
"path": "/customerName"
},
{
"op": "move",
"from": "/orders/1",
"path": "/orders/0"
}
]
The copy operation
This operation is functionally the same as a move
operation without the final remove
step.
The following sample patch document:
- Copies the value of
Orders[0].OrderName
toCustomerName
. - Inserts a copy of
Orders[1]
beforeOrders[0]
.
[
{
"op": "copy",
"from": "/orders/0/orderName",
"path": "/customerName"
},
{
"op": "copy",
"from": "/orders/1",
"path": "/orders/0"
}
]
The test operation
If the value at the location indicated by path
is different from the value provided in value
, the request fails. In that case, the whole PATCH request fails even if all other operations in the patch document would otherwise succeed.
The test
operation is commonly used to prevent an update when there's a concurrency conflict.
The following sample patch document has no effect if the initial value of CustomerName
is "John", because the test fails:
[
{
"op": "test",
"path": "/customerName",
"value": "Nancy"
},
{
"op": "add",
"path": "/customerName",
"value": "Barry"
}
]
Get the code
View or download sample code. (How to download).
To test the sample, run the app and send HTTP requests with the following settings:
- 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.
ASP.NET Core