9

I'm making a Core 3.1 web API and using JsonPatch to create a PATCH action. I have an action named Patch which has a JsonPatchDocument parameter. Here is the action's signature:

[HttpPatch("{id}")]
public ActionResult<FileRecordDto> Patch(int id, [FromBody] JsonPatchDocument<FileRecordQueryParams> patchDoc)

As I understand, the parameter needs to receive JSON data in the following structure, which I've successfully tested with the action:

[
  {
    "op": "operationName",
    "path": "/propertyName",
    "value": "newPropertyValue"
  }
]

However, the action's documentation generated by Swagger has a different structure: enter image description here

I'm not familiar with this structure and even "value" property is missing from it, which a JsonPatchDocument object has. Every example of patching with the replace operation I've seen has had the first structure.

Why is Swagger generating an alternate structure for a JsonPatchDocument object in the request body for the PATCH endpoint? How do I fix this?

The NuGet package installed for Swagger: Swashbuckle.AspNetCore v5.6.3

Lukas
  • 1,699
  • 1
  • 16
  • 49
  • What version of swagger? There are multiple standards – Liam Jan 06 '21 at 16:19
  • Here's the [latest speciation (v 3.0.3)](https://swagger.io/specification/). It's not clear from the question what this JSON is supposed to represent so it's hard to be specific – Liam Jan 06 '21 at 16:21
  • Thanks. I have the NuGet package Swashbuckle.AspNetCore v5.6.3 installed to get Swagger tools, though I'm not sure if its version is separate from Swaggers, or if it's even a separate thing altogether. – Lukas Jan 06 '21 at 16:25
  • I've updated the question to show a picture of what the JSON is supposed to represent. – Lukas Jan 06 '21 at 16:28
  • and whats `JsonPatchDocument `? – Liam Jan 06 '21 at 16:30
  • It's ASP.NET Core's implementation of handling JSON Patch requests. `FileRecordQueryParams` is the parameter constraint for `JsonPatchDocument` and then I can use methods like `.ApplyTo()` to actually do the patching. – Lukas Jan 06 '21 at 16:39

1 Answers1

12

Swashbuckle.AspNetCore doesn't work propertly with this type JsonPatchDocument<UpdateModel>, which doesn’t represent the expected patch request doument.

You need to custome a document filter to modify the generated specification.

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.Single(c => c.Key == "application/json-patch+json");
            response.Value.Schema = new OpenApiSchema
            {
                Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "JsonPatchDocument" }
            };
        }
    }
}

Register the filter:

services.AddSwaggerGen(c => c.DocumentFilter<JsonPatchDocumentFilter>());

Result:

enter image description here

mj1313
  • 7,930
  • 2
  • 12
  • 32
  • This worked for me, although I did need to add in some lines to remove unused schemas – Dan Mar 14 '21 at 14:55
  • 1
    Can this be made to work if the JsonPatchDocument is itself within a class that's being bound? My PatchDocument is a property of a Command dto which includes the id from the route and the document from the body of the request. This filter isn't working as-is. – ssmith Jun 16 '22 at 18:24
  • @ssmith Did you find a solution? We have the exact same problem. – Sam Carlson Jan 30 '23 at 09:19
  • I think I got it working - not 100% sure. The repo is here; https://github.com/ardalis/WebApiBestPractices/blob/main/api_templates/ApiBestPractices.Endpoints/Endpoints/Authors/Patch.cs you can clone it and see if it's behaving the way you need. – ssmith Jan 31 '23 at 16:58