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
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
API Design PatternsPATCH
method is that it does a partial modification of a resource rather than a full replacement.
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:
[ { "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.
[ { "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.
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,
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.
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.
[ { "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
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.
- What is this Directory.Packages.props file all about? - January 25, 2024
- How to add Tailwind CSS to Blazor website - November 20, 2023
- How to deploy a Blazor site to Netlify - November 17, 2023