Several improvements
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:
https://github.com/Havunen/SystemTextJsonPatch/blob/c7ffbaf82e62bf860c9ac120665dbe3318dc0dfc/SystemTextJsonPatch/JsonPatchDocumentOfT.cs#L189
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" },
},
});
}
}
}