Skip to content

How to use JSONPatch in .net core

REST API's often require a PATCH method to partially update resources. A typical scenario for this would be when updating a survey questionnaire type workflow or even, as in the example I will demonstrate, updating a customers shopping cart in an e-commerce Microservice. As a customer updates their cart by adding, removing or editing products in the cart you'll need to cater for a number of operations.

Its in situations like that a JSONPatch can help update these document resources in a very explicit way. It was only by reading API design patterns and the chapter on Partial updates and retrievals and the alternative recommendations that I stumbled across JSON Patch approach.

After perusing the Javascript Object Notation (JSON) Patch documentation when I then discovered that there was an implementation of this standard available in Dotnet, that my interest really piqued.

API Design Patterns lays out a set of design principles for building internal and public-facing APIs. Providing a collection of best practices and design standards for web and internal APIs.

HTTP Patch method, pointing to specific resource using its unique identifier and returning the newly modified resource. One key aspect of the PATCH method is that it does a partial modification of a resource rather than a full replacement.

API Design Patterns

What is JSON Patch

JSON Patch is a format for describing changes to a JSON document. It can be used to avoid sending a whole document when only a part has changed. When used in combination with the HTTP PATCH method, it allows partial updates for HTTP APIs in a standards compliant way.

jsonpatch.com

JSON Patch defines a JSON document structure for expressing a sequence of operations to apply to a JavaScript Object Notation (JSON) document; it is suitable for use with the HTTP PATCH method. The application/json-patch+json media type is used to identify such patch documents.

JSON Patch is a format (identified by the media type application/json-patch+json for expressing a sequence of operations to apply to a target JSON document; it is suitable for use with the HTTP PATCH method.

A JSON Patch document is a JSON document that represents an array of objects. Each object represents a single operation to be applied to the target JSON document.

The patch operations supported by JSON Patch are:

  • add - adds a property to an object 
  • remove - removes a property from an object or and item from an array
  • replace - Replaces a value.
  • move - Moves a value from one location to the other
  • copy - Copies a value from one location to another within the JSON document
  • test - Tests that the specified value is set in the document

The operations are applied in order: if any of them fail then the whole patch operation should abort.

JavaScript Object Notation (JSON) Patch

JSON Patch documents classify documents with media type application/json-patch+json technically it is associated with RFC-6902 standard. A typical JSON Patch request is represented as follows:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[
{ "op": "add", "path": "/path", "value": ["value"] }
]
[ { "op": "add", "path": "/path", "value": ["value"] } ]
[
    { "op": "add", "path": "/path", "value": ["value"] }
]

You'll notice that it is defined as an array, enabling multiple operations for the op field

  • add
  • remove
  • replace
  • copy
  • test

The path value represents a nested object in the JSON document, and the value property contains the value need to be updated on the given path.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[
{ "op": "replace", "path": "/foo/", "value": "bar" },
{ "op": "add", "path": "/foo/1", "value": "bar" }, // Add to specific position array
{ "op": "add", "path": "/foo/-", "value": "bar" }, // append to an array
{ "op": "remove", "path": "/foo/1" }, // remove an item from array
{ "op": "test", "path": "/baz", "value": "qux" } //Testing if patch is applicable
]
[ { "op": "replace", "path": "/foo/", "value": "bar" }, { "op": "add", "path": "/foo/1", "value": "bar" }, // Add to specific position array { "op": "add", "path": "/foo/-", "value": "bar" }, // append to an array { "op": "remove", "path": "/foo/1" }, // remove an item from array { "op": "test", "path": "/baz", "value": "qux" } //Testing if patch is applicable ]
[
   { "op": "replace", "path": "/foo/", "value": "bar" },
   { "op": "add", "path": "/foo/1", "value": "bar" }, // Add to specific position array
   { "op": "add", "path": "/foo/-", "value": "bar" }, // append to an array
   { "op": "remove", "path": "/foo/1" },  // remove an item from array
   { "op": "test", "path": "/baz", "value": "qux" } //Testing if patch is applicable
]

Using in JSON Patch in dotnet core

Dotnet has support for making use of JSON Patch, but it requires the addition of a Microsoft.AspNetCore.JsonPatch Nuget Package to your application.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
dotnet add package Microsoft.AspNetCore.JsonPatch
dotnet add package Microsoft.AspNetCore.JsonPatch
dotnet add package Microsoft.AspNetCore.JsonPatch

Once this has been added to your project you can now start to make use of the JsonPatchDocument. In my example I will be using it to enable the update of items in a typical Shopping Cart of an ecommerce store in my example of state management in Dapr Tutorials series.

In the example code we make use of the API Endpoints and following the CQRS pattern to define our REST API Endpoint and we'll be make use of the Mediator Pattern and mediatr.

We will create a Command object that will have a JsonPatchDocument as a property item, we will also create a simple POCO class for our object with 2 properties for a SKU (stock-keeping unit) code and the Quantity.

You will notice that in Command object uses the JsonPatchDocument using a List of Items,

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public class Command : IRequest<Response>
{
[FromHeader(Name = "x-session-id")] public string Session { get; set; }
[FromBody] public JsonPatchDocument<List<Item>> Items { get; set; }
}
public class Item
{
public string Sku { get; set; }
public int Quantity { get; set; }
public decimal Amount { get; set; }
}
public class Command : IRequest<Response> { [FromHeader(Name = "x-session-id")] public string Session { get; set; } [FromBody] public JsonPatchDocument<List<Item>> Items { get; set; } } public class Item { public string Sku { get; set; } public int Quantity { get; set; } public decimal Amount { get; set; } }
 public class Command : IRequest<Response>
 {
    [FromHeader(Name = "x-session-id")] public string Session { get; set; }
    [FromBody] public JsonPatchDocument<List<Item>> Items { get; set; }
 }

 public class Item
 {
   public string Sku { get; set; }
   public int Quantity { get; set; }
   public decimal Amount { get; set; }
 }

We'll create a simple PATCH endpoint on our API

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[Route(Routes.Cart)]
public class Patch : BaseAsyncEndpoint.WithRequest<Command>.WithResponse<Response>
{
private readonly IMediator _mediator;
public Patch(IMediator mediator)
{
_mediator = mediator;
}
[HttpPatch]
[SwaggerOperation(
Summary = "Update a draft import application",
Description = "Update a draft import application",
OperationId = "3E520260-2CB4-462A-BA04-C2F7CFAB1EEE",
Tags = new[] { Routes.Cart })
]
public override async Task<ActionResult<Response>> HandleAsync([FromRoute] Command request,
CancellationToken cancellationToken = new CancellationToken())
{
var response = await _mediator.Send(request, cancellationToken);
return new OkObjectResult(response);
}
}
[Route(Routes.Cart)] public class Patch : BaseAsyncEndpoint.WithRequest<Command>.WithResponse<Response> { private readonly IMediator _mediator; public Patch(IMediator mediator) { _mediator = mediator; } [HttpPatch] [SwaggerOperation( Summary = "Update a draft import application", Description = "Update a draft import application", OperationId = "3E520260-2CB4-462A-BA04-C2F7CFAB1EEE", Tags = new[] { Routes.Cart }) ] public override async Task<ActionResult<Response>> HandleAsync([FromRoute] Command request, CancellationToken cancellationToken = new CancellationToken()) { var response = await _mediator.Send(request, cancellationToken); return new OkObjectResult(response); } }
 [Route(Routes.Cart)]
 public class Patch : BaseAsyncEndpoint.WithRequest<Command>.WithResponse<Response>
 {
   private readonly IMediator _mediator;

   public Patch(IMediator mediator)
   {
     _mediator = mediator;
   }

 [HttpPatch]
 [SwaggerOperation(
  Summary = "Update a draft import application",
  Description = "Update a draft import application",
  OperationId = "3E520260-2CB4-462A-BA04-C2F7CFAB1EEE",
  Tags = new[] { Routes.Cart })
 ]
 public override async Task<ActionResult<Response>> HandleAsync([FromRoute] Command request,
     CancellationToken cancellationToken = new CancellationToken())
 {
    var response = await _mediator.Send(request, cancellationToken);
    return new OkObjectResult(response);
 }
}

We start our application now and navigate to our Swagger page, we'll see that the default swagger documentation doesn't really provide enough information for us to learn how to use our JsonPatchDocument. We just get a rather unhelpful "contractResolver": {} for our schema

Formatting JSON Patch for swagger

We're using REST API and are documenting our API's for development use using swagger.

At least at the time of writing, and I'm not sure if this will eventually be cleaned up when the .net core development finishes moving NewtonSoft JSON into the System.Text.Json but we sill need to add to our controllers . We do this by editing our ConfigureServices method in StartUp.cs

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddNewtonsoftJson();
//// .... more code
}
public void ConfigureServices(IServiceCollection services) { services.AddControllers().AddNewtonsoftJson(); //// .... more code }
 public void ConfigureServices(IServiceCollection services)
{ 
   services.AddControllers().AddNewtonsoftJson();

//// .... more code 
}

This dramatically improves our documentation but unfortunately does provide a little too much information and can be a little misleading to others. We just need to add and an additional file with the following below.

Using IDocumentFilter provides more control over document definition before submitting the generated document to the user using the Swagger UI tool. In basic terms what this code does is remove OperationType and from properties from the documentation because the user does not need to know these and will not be making use of them

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public class JsonPatchDocumentFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var schemas = swaggerDoc.Components.Schemas.ToList();
foreach (var item in schemas)
{
if (item.Key.StartsWith("Operation") || item.Key.StartsWith("JsonPatchDocument"))
swaggerDoc.Components.Schemas.Remove(item.Key);
}
swaggerDoc.Components.Schemas.Add("Operation", new OpenApiSchema
{
Type = "object",
Properties = new Dictionary<string, OpenApiSchema>
{
{ "op", new OpenApiSchema { Type = "string" } },
{ "value", new OpenApiSchema { Type = "string" } },
{ "path", new OpenApiSchema { Type = "string" } }
}
});
swaggerDoc.Components.Schemas.Add("JsonPatchDocument", new OpenApiSchema
{
Type = "array",
Items = new OpenApiSchema
{
Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "Operation" }
},
Description = "Array of operations to perform"
});
foreach (var path in swaggerDoc.Paths.SelectMany(p => p.Value.Operations)
.Where(p => p.Key == Microsoft.OpenApi.Models.OperationType.Patch))
{
foreach (var item in path.Value.RequestBody.Content.Where(c => c.Key != "application/json-patch+json"))
path.Value.RequestBody.Content.Remove(item.Key);
var response = path.Value.RequestBody.Content.SingleOrDefault(c => c.Key == "application/json-patch+json");
response.Value.Schema = new OpenApiSchema
{
Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "JsonPatchDocument" }
};
}
}
}
public class JsonPatchDocumentFilter : IDocumentFilter { public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { var schemas = swaggerDoc.Components.Schemas.ToList(); foreach (var item in schemas) { if (item.Key.StartsWith("Operation") || item.Key.StartsWith("JsonPatchDocument")) swaggerDoc.Components.Schemas.Remove(item.Key); } swaggerDoc.Components.Schemas.Add("Operation", new OpenApiSchema { Type = "object", Properties = new Dictionary<string, OpenApiSchema> { { "op", new OpenApiSchema { Type = "string" } }, { "value", new OpenApiSchema { Type = "string" } }, { "path", new OpenApiSchema { Type = "string" } } } }); swaggerDoc.Components.Schemas.Add("JsonPatchDocument", new OpenApiSchema { Type = "array", Items = new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "Operation" } }, Description = "Array of operations to perform" }); foreach (var path in swaggerDoc.Paths.SelectMany(p => p.Value.Operations) .Where(p => p.Key == Microsoft.OpenApi.Models.OperationType.Patch)) { foreach (var item in path.Value.RequestBody.Content.Where(c => c.Key != "application/json-patch+json")) path.Value.RequestBody.Content.Remove(item.Key); var response = path.Value.RequestBody.Content.SingleOrDefault(c => c.Key == "application/json-patch+json"); response.Value.Schema = new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "JsonPatchDocument" } }; } } }
 public class JsonPatchDocumentFilter : IDocumentFilter
 {
   public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
  {
    var schemas = swaggerDoc.Components.Schemas.ToList();
    foreach (var item in schemas)
    {
       if (item.Key.StartsWith("Operation") || item.Key.StartsWith("JsonPatchDocument"))
           swaggerDoc.Components.Schemas.Remove(item.Key);
    }

   swaggerDoc.Components.Schemas.Add("Operation", new OpenApiSchema
   {
      Type = "object",
      Properties = new Dictionary<string, OpenApiSchema>
     {
        { "op", new OpenApiSchema { Type = "string" } },
                    { "value", new OpenApiSchema { Type = "string" } },
                    { "path", new OpenApiSchema { Type = "string" } }
                }
            });

            swaggerDoc.Components.Schemas.Add("JsonPatchDocument", new OpenApiSchema
            {
                Type = "array",
                Items = new OpenApiSchema
                {
                    Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "Operation" }
                },
                Description = "Array of operations to perform"
            });

            foreach (var path in swaggerDoc.Paths.SelectMany(p => p.Value.Operations)
                .Where(p => p.Key == Microsoft.OpenApi.Models.OperationType.Patch))
            {
                foreach (var item in path.Value.RequestBody.Content.Where(c => c.Key != "application/json-patch+json"))
                    path.Value.RequestBody.Content.Remove(item.Key);
                
               var response = path.Value.RequestBody.Content.SingleOrDefault(c => c.Key == "application/json-patch+json");
                
                response.Value.Schema = new OpenApiSchema
                {
                    Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "JsonPatchDocument" }
                };
            }
        }
    }

We now just need to update our StartUp.cs again to update the AddSwaggernGen method to include our Document Filter

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddNewtonsoftJson();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo {Title = "ShoppingCart", Version = "v1"});
c.CustomSchemaIds(x => x.FullName);
c.EnableAnnotations();
c.DocumentFilter<JsonPatchDocumentFilter>();
});
}
public void ConfigureServices(IServiceCollection services) { services.AddControllers().AddNewtonsoftJson(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo {Title = "ShoppingCart", Version = "v1"}); c.CustomSchemaIds(x => x.FullName); c.EnableAnnotations(); c.DocumentFilter<JsonPatchDocumentFilter>(); }); }
 public void ConfigureServices(IServiceCollection services)
 {
           
    services.AddControllers().AddNewtonsoftJson();
    services.AddSwaggerGen(c =>
    {
      c.SwaggerDoc("v1", new OpenApiInfo {Title = "ShoppingCart", Version = "v1"});
      c.CustomSchemaIds(x => x.FullName);
      c.EnableAnnotations();
      c.DocumentFilter<JsonPatchDocumentFilter>();
    });
}

Now if we run our application we'll see our documentation is a lot cleaner and simpler

Apply Changes

In our example we will implement a very simplistic implementation of the JSON Patch update, with just enough steps to illustrate the entire process.

Our method accepts a JSONPatchDocument with our list of items. In our method we first go to the data store to get the current items, then we Apply our changes to the items then save them. Once complete we simply return the updated values to the calling process.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public async Task<List<Item>> Update(string session, JsonPatchDocument<List<Item>> items)
{
var currentItems = await Get(session);
items.ApplyTo(currentItems);
await Save(session, currentItems);
return currentItems;
}
public async Task<List<Item>> Update(string session, JsonPatchDocument<List<Item>> items) { var currentItems = await Get(session); items.ApplyTo(currentItems); await Save(session, currentItems); return currentItems; }
  public async Task<List<Item>> Update(string session, JsonPatchDocument<List<Item>> items)
  {
    var currentItems = await Get(session);
    items.ApplyTo(currentItems);
    await Save(session, currentItems);
    return currentItems;
 }

Sending the values to JSONPatch

In our example we'll be updating items in a Shopping cart, this based on assuming a cart already exists. i.e. the post method has already been executed. Below is an an example of the original post. Possibly we'll create the initial instance of the cart by adding 2 of a certain product.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
curl -X 'POST' \
'http://localhost:5001/cart' \
-H 'accept: application/json' \
-H 'x-session-id: 87159B5C-9257-44F9-A5B0-810A85997A0F' \
-H 'Content-Type: application/json-patch+json' \
-d '[
{
"sku": "abc-100",
"quantity": 2,
"amount": 19.99
}
]'
curl -X 'POST' \ 'http://localhost:5001/cart' \ -H 'accept: application/json' \ -H 'x-session-id: 87159B5C-9257-44F9-A5B0-810A85997A0F' \ -H 'Content-Type: application/json-patch+json' \ -d '[ { "sku": "abc-100", "quantity": 2, "amount": 19.99 } ]'
curl -X 'POST' \
  'http://localhost:5001/cart' \
  -H 'accept: application/json' \
  -H 'x-session-id: 87159B5C-9257-44F9-A5B0-810A85997A0F' \
  -H 'Content-Type: application/json-patch+json' \
  -d '[
  {
    "sku": "abc-100",
    "quantity": 2,
    "amount": 19.99
  }
]'

At some point later in the cycle the user would like to update the value in their cart, by reducing the quanity of the original order and also add another item to the cart

We will now make use of JSON Patch to update our cart. Important to note here we make use of the REPLACE and ADD methods. The replace method updates the original item and the add method.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[
{
"op": "replace",
"value": { "sku": "abc-100", "quantity": 1, "amount": 9.99 } ,
"path": "/0"
},
{
"op": "add",
"value": { "sku": "abc-200", "quantity": 1, "amount": 29.99} ,
"path": "/-"
}
]
[ { "op": "replace", "value": { "sku": "abc-100", "quantity": 1, "amount": 9.99 } , "path": "/0" }, { "op": "add", "value": { "sku": "abc-200", "quantity": 1, "amount": 29.99} , "path": "/-" } ]
[
 {
    "op": "replace",
    "value": {  "sku": "abc-100",  "quantity": 1,  "amount": 9.99 } ,
    "path": "/0"
  },
  {
    "op": "add",
    "value": {  "sku": "abc-200",  "quantity": 1,  "amount": 29.99} ,
    "path": "/-"
  }
]

if we execute the request

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
curl -X 'PATCH' \
'http://localhost:5001/cart' \
-H 'accept: text/plain' \
-H 'x-session-id: 87159B5C-9257-44F9-A5B0-810A85997A0F' \
-H 'Content-Type: application/json-patch+json' \
-d '[
{
"op": "replace",
"value": { "sku": "abc-100", "quantity": 1, "amount": 9.99 } ,
"path": "/0"
},
{
"op": "add",
"value": { "sku": "abc-200", "quantity": 1, "amount": 29.99} ,
"path": "/-"
}
]'
curl -X 'PATCH' \ 'http://localhost:5001/cart' \ -H 'accept: text/plain' \ -H 'x-session-id: 87159B5C-9257-44F9-A5B0-810A85997A0F' \ -H 'Content-Type: application/json-patch+json' \ -d '[ { "op": "replace", "value": { "sku": "abc-100", "quantity": 1, "amount": 9.99 } , "path": "/0" }, { "op": "add", "value": { "sku": "abc-200", "quantity": 1, "amount": 29.99} , "path": "/-" } ]'
curl -X 'PATCH' \
  'http://localhost:5001/cart' \
  -H 'accept: text/plain' \
  -H 'x-session-id: 87159B5C-9257-44F9-A5B0-810A85997A0F' \
  -H 'Content-Type: application/json-patch+json' \
  -d '[
  {
    "op": "replace",
    "value": {  "sku": "abc-100",  "quantity": 1,  "amount": 9.99 } ,
    "path": "/0"
  },
  {
    "op": "add",
    "value": {  "sku": "abc-200",  "quantity": 1,  "amount": 29.99} ,
    "path": "/-"
  }
]'

if we check the database we'll see the value updated

Conclusion

Use the ASP.NET Core Json Patch library to support partial updates in your APIs using JSON Patch operations.

Gary Woodfine
Latest posts by Gary Woodfine (see all)