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.

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:

JSON

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.

JSON

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.

Shell

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,

 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

 [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

 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

 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

 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.

  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

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.

JSON

if we execute the request

Shell

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.

Please let me know in the comments section below if you think I left some vital information out or you would like me to elaborate a bit more. I am always looking to improve my articles.

    Like this content ?

    Sign up to my newsletter and I'll send you updates and more!

    Latest posts by Gary Woodfine (see all)