Lukas
Lukas

Reputation: 2213

Swagger unexpected API PATCH action documentation of JsonPatchDocument in example request body

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

Upvotes: 9

Views: 6234

Answers (2)

Arekadiusz
Arekadiusz

Reputation: 575

Here's the updated code for OpenAPI3 (OAS3 - new SwaggerUI version) that handles schemas and paths.

public class JsonPatchDocumentFilter : IDocumentFilter
{
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        // Handle schemas
        var keysToRemove = swaggerDoc.Components.Schemas
            .Where(s => s.Key.StartsWith("SystemTextJsonPatch", StringComparison.OrdinalIgnoreCase))
            .Select(s => s.Key)
            .ToList();

        foreach (var key in keysToRemove)
        {
            swaggerDoc.Components.Schemas.Remove(key);
        }

        swaggerDoc.Components.Schemas.Add("JsonPatchDocument", new OpenApiSchema
        {
            Type = "object",
            Description = "Describes a single operation in a JSON Patch document. Includes the operation type, the target property path, and the value to be used.",
            Properties = new Dictionary<string, OpenApiSchema>
            {
                {
                    "op", new OpenApiSchema
                    {
                        Type = "string",
                        Description = "The operation type. Allowed values: 'add', 'remove', 'replace', 'move', 'copy', 'test'.",
                    }
                },
                {
                    "path", new OpenApiSchema
                    {
                        Type = "string",
                        Description = "The JSON Pointer path to the property in the target document where the operation is to be applied.",
                    }
                },
                {
                    "value", new OpenApiSchema
                    {
                        Type = "string",
                        Description = "The value to apply for 'add', 'replace', or 'test' operations. Not required for 'remove', 'move', or 'copy'.",
                    }
                },
            },
        });

        // Handle paths
        foreach (var path in swaggerDoc.Paths)
        {
            if (path.Value.Operations.TryGetValue(OperationType.Patch, out var patchOperation) && patchOperation.RequestBody != null)
            {
                foreach (var key in patchOperation.RequestBody.Content.Keys)
                {
                    patchOperation.RequestBody.Content.Remove(key);
                }

                if (patchOperation.OperationId.StartsWith("odata", StringComparison.OrdinalIgnoreCase))
                {
                    path.Value.Operations.Remove(OperationType.Patch);
                }

                patchOperation.RequestBody.Content.Add("application/json-patch+json", new OpenApiMediaType
                {
                    Schema = new OpenApiSchema
                    {
                        Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "JsonPatchDocument" },
                    },
                });
            }
        }
    }
}

Previous UI:

There was entry in /oauth/: enter image description here

And schemas looked like:

enter image description here

Description: enter image description here

Current version is:

enter image description here

enter image description here

enter image description here

Remember to change the Name of the HttpPatch attribute in your Controller and leave JsonPatchDocument schema name unchanged.

Upvotes: 0

mj1313
mj1313

Reputation: 8459

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

Upvotes: 14

Related Questions