Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Several improvements #20

Open
senketsu03 opened this issue Jun 14, 2023 · 7 comments
Open

Several improvements #20

senketsu03 opened this issue Jun 14, 2023 · 7 comments
Labels
enhancement New feature or request

Comments

@senketsu03
Copy link

The JsonPatchDocument<T> as an argument to a PATCH method on server is not recognized on swagger/redoc page and shown as {}.

The way to overcome this issue it to use List<Operation<T>> as argument instead and initialize JsonPatchDocument<T> from it in patch method:

public async Task<ActionResult<User>> PatchUserAsync(int id, List<Operation<User>> operations)
{
    var patch = new JsonPatchDocument<User>(operations, new());

    // some request  logics

    patch.ApplyTo(update);

    // more request logic
}

Sending collection of operations as application/json-patch+json works fine too. The problem is: initializing list of Operations isn't very convenient. Instead of:

var patch = new JsonPatchDocument<User>();
patch.Replace((u) => u.Name, "Tom");
patch.Replace((u) => u.Age, 40);

We have to write such code:

var operations = new List<Operation<User>>
{
    new Operation<User>("replace", "/name", null, "Tom"),
    new Operation<User>("replace", "/age", null, 40)
};

The problems of this code are obvious: we have to rely on string values when creating the operations (while it could have been safer to use OperationType enum) and we have to rely on string when resolving path.

Probably a static methods for Operation<T> class could be implemented, so the usage would look similar to this:

var operations = new List<Operation<User>>
{
    Operation<User>.Replace((u) => u.Name, "Tom"),
    Operation<User>.Replace((u) => u.Age, 40)
};

These static methods would also simplify related calls in JsonPatchDocumentOfT:

Operations.Add(new Operation<TModel>(

@kipusoep
Copy link

Just ran into the empty schema for PatchDocument in swagger, would love to have this supported properly OOTB.

@Havunen
Copy link
Owner

Havunen commented Jul 14, 2023

Yeah we could quite easily improve it to a point where swagger schema gets generated as path - string, op - string, but what can we do for the value property as that could be anything?

@kipusoep
Copy link

kipusoep commented Jul 18, 2023

Good question, I've chosen to use a string as well at the moment, as suggested here: https://stackoverflow.com/a/65607728/510149

/// <summary>
/// A swagger document filter to support json patch better.
/// </summary>
public class JsonPatchDocumentFilter : IDocumentFilter
{
    /// <inheritdoc />
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        // Replace schemas for Operation and JsonPatchDocument
        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);
            }
        }

        var jsonPatchDocumentOperationTypes = Enum.GetValues<PatchOperationType>()
            .Where(x => x != PatchOperationType.Invalid)
            .Select(x => new OpenApiString(x.ToString().ToLower())).ToList();
        swaggerDoc.Components.Schemas.Add("Operation", new OpenApiSchema
        {
            Type = "object",
            Properties = new Dictionary<string, OpenApiSchema>
            {
                { "op", new OpenApiSchema { Type = "string", Enum = new List<IOpenApiAny>(jsonPatchDocumentOperationTypes) } },
                { "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",
        });

        // Alter the content type for patch requests
        foreach (var path in swaggerDoc.Paths
                     .SelectMany(p => p.Value.Operations)
                     .Where(p => p.Key == OperationType.Patch))
        {
            path.Value.RequestBody.Content = new Dictionary<string, OpenApiMediaType>
            {
                { "application/json-patch+json", new OpenApiMediaType { Schema = new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "JsonPatchDocument" } } } },
            };
        }
    }
}

@Havunen
Copy link
Owner

Havunen commented Jul 18, 2023

Maybe a new Nuget package needs to be created to avoid dependency to Swashbuckle / NSwag

@Havunen Havunen added the enhancement New feature or request label Jul 18, 2023
@kipusoep
Copy link

Currently I'm also struggling getting a bulk patch endpoint to work, with a IDictionary<Guid, JsonPatchDocument<MyWriteDtoClass>> patchDocuments parameter.

@kipusoep
Copy link

This works when used with a parameter like [FromBody] IDictionary<Guid, JsonPatchDocument<WeatherForecast>> weatherForecasts:

/// <summary>
/// A swagger document filter to support json patch better.
/// </summary>
public class JsonPatchDocumentFilter : IDocumentFilter
{
    /// <inheritdoc />
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        // Replace schemas for Operation and JsonPatchDocument
        swaggerDoc.Components.Schemas
            .Where(x => x.Key.EndsWith("Operation") || x.Key.EndsWith("JsonPatchDocument"))
            .ToList()
            .ForEach(x => swaggerDoc.Components.Schemas.Remove(x));

        var jsonPatchDocumentOperationTypes = Enum.GetValues<PatchOperationType>()
            .Where(x => x != PatchOperationType.Invalid)
            .Select(x => new OpenApiString(x.ToString().ToLower())).ToList();
        swaggerDoc.Components.Schemas.Add("Operation", new OpenApiSchema
        {
            Type = "object",
            Properties = new Dictionary<string, OpenApiSchema>
            {
                { "op", new OpenApiSchema { Type = "string", Enum = new List<IOpenApiAny>(jsonPatchDocumentOperationTypes) } },
                { "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",
        });
        swaggerDoc.Components.Schemas.Add("BulkJsonPatchDocument", new OpenApiSchema
        {
            Type = "object",
            AdditionalProperties = new OpenApiSchema
            {
                Type = "array",
                Items = new OpenApiSchema
                {
                    Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "Operation" },
                },
            },
            Description = "A dictionary/map using the entity id (Guid) as key and JsonPatchDocument as value.",
        });

        // Alter the content type and schema for patch requests
        foreach (var path in swaggerDoc.Paths
                     .SelectMany(p => p.Value.Operations)
                     .Where(p => p.Key == OperationType.Patch))
        {
            var schemaReferenceId = "JsonPatchDocument";
            if (path.Value.RequestBody.Content.First().Value.Schema.AdditionalProperties != null)
            {
                // When AdditionalProperties is not null, it means a dictionary is used and thus it's a bulk request
                schemaReferenceId = "BulkJsonPatchDocument";
            }

            path.Value.RequestBody.Content = new Dictionary<string, OpenApiMediaType>
            {
                {
                    "application/json-patch+json",
                    new OpenApiMediaType
                    {
                        Schema = new OpenApiSchema
                        {
                            Reference = new OpenApiReference
                            {
                                Type = ReferenceType.Schema,
                                Id = schemaReferenceId,
                            },
                        },
                    }
                },
            };
        }
    }
}

@leoerlandsson
Copy link

leoerlandsson commented May 27, 2024

Here's the IDocumentFilter that we use successfully, inspired by the code examples on this Issue and in the linked StackOverflow thread.

public class JsonPatchDocumentFilter : IDocumentFilter
{
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        // Handle schemas
        var keysToRemove = swaggerDoc.Components.Schemas
            .Where(s =>
                (s.Key.EndsWith("Operation", StringComparison.OrdinalIgnoreCase) && s.Value.Properties.All(p => new string[] { "op", "path", "from", "value" }.Contains(p.Key))) ||
                (s.Key.EndsWith("JsonPatchDocument", StringComparison.OrdinalIgnoreCase))
            )
            .Select(s => s.Key)
            .ToList();

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

        swaggerDoc.Components.Schemas.Add("JsonPatchOperation", 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.",
            Required = new HashSet<string> { "op", "path", "value" },
            Properties = new Dictionary<string, OpenApiSchema>
            {
                {
                    "op", new OpenApiSchema
                    {
                        Type = "string",
                        Description = "The operation type. Allowed values: 'add', 'remove', 'replace', 'move', 'copy', 'test'.",
                        Enum = new List<IOpenApiAny>
                        {
                            new OpenApiString("add"),
                            new OpenApiString("remove"),
                            new OpenApiString("replace"),
                            new OpenApiString("move"),
                            new OpenApiString("copy"),
                            new OpenApiString("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.",
                    }
                },
                {
                    "from", new OpenApiSchema
                    {
                        Type = "string",
                        Description = "Should be a path, required when using move, copy",
                    }
                },
                {
                    "value", new OpenApiSchema
                    {
                        Nullable = true,
                        Description = "The value to apply for 'add', 'replace', or 'test' operations. Not required for 'remove', 'move', or 'copy'.",
                    }
                },
            },
        });

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

        // 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);
                }

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants