Open senketsu03 opened 1 year ago
Just ran into the empty schema for PatchDocument in swagger, would love to have this supported properly OOTB.
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?
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" } } } },
};
}
}
}
Maybe a new Nuget package needs to be created to avoid dependency to Swashbuckle / NSwag
Currently I'm also struggling getting a bulk patch endpoint to work, with a IDictionary<Guid, JsonPatchDocument<MyWriteDtoClass>> patchDocuments
parameter.
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,
},
},
}
},
};
}
}
}
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" },
},
});
}
}
}
The
JsonPatchDocument<T>
as an argument to aPATCH
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 initializeJsonPatchDocument<T>
from it in patch method:Sending collection of operations as
application/json-patch+json
works fine too. The problem is: initializing list ofOperation
s isn't very convenient. Instead of:We have to write such code:
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 useOperationType
enum) and we have to rely onstring
when resolvingpath
.Probably a static methods for
Operation<T>
class could be implemented, so the usage would look similar to this:These static methods would also simplify related calls in
JsonPatchDocumentOfT
:https://github.com/Havunen/SystemTextJsonPatch/blob/c7ffbaf82e62bf860c9ac120665dbe3318dc0dfc/SystemTextJsonPatch/JsonPatchDocumentOfT.cs#L189