JsonPatch in ASP.NET Core web API (original) (raw)

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

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:

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:

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:

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:

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:

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:

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

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

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

The following sample patch document:

[
  {
    "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:

[
  {
    "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:

Additional resources

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

Package installation

To enable JSON Patch support in your app, complete the following steps:

  1. Install the Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet package.
  2. 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:

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:

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:

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

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

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

The following sample patch document:

[
  {
    "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:

[
  {
    "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:

Additional resources