Skip to content

JsonPatchDocument: Use application/json-patch+json content type in OpenAPI #62057

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

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
20 changes: 19 additions & 1 deletion src/Features/JsonPatch.SystemTextJson/src/JsonPatchDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Adapters;
using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Converters;
using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Exceptions;
Expand All @@ -18,7 +21,7 @@ namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson;
// documents for cases where there's no class/DTO to work on. Typical use case: backend not built in
// .NET or architecture doesn't contain a shared DTO layer.
[JsonConverter(typeof(JsonPatchDocumentConverter))]
public class JsonPatchDocument : IJsonPatchDocument
public class JsonPatchDocument : IJsonPatchDocument, IEndpointParameterMetadataProvider
{
public List<Operation> Operations { get; private set; }

Expand Down Expand Up @@ -218,4 +221,19 @@ IList<Operation> IJsonPatchDocument.GetOperations()

return allOps;
}

/// <summary>
/// Populates metadata for the related endpoint when this type is used as a parameter.
/// </summary>
/// <param name="parameter">The <see cref="ParameterInfo"/> for the endpoint parameter.</param>
/// <param name="builder">The endpoint builder for the endpoint being constructed.</param>
#pragma warning disable RS0016 // Add public types and members to the declared API
public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
#pragma warning restore RS0016 // Add public types and members to the declared API
{
ArgumentNullException.ThrowIfNull(parameter);
ArgumentNullException.ThrowIfNull(builder);

builder.Metadata.Add(new AcceptsMetadata(new[] { "application/json-patch+json" }, parameter.ParameterType));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Adapters;
using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Converters;
using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Exceptions;
Expand All @@ -23,7 +25,7 @@ namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson;
// including type data in the JsonPatchDocument serialized as JSON (to allow for correct deserialization) - that's
// not according to RFC 6902, and would thus break cross-platform compatibility.
[JsonConverter(typeof(JsonPatchDocumentConverterFactory))]
public class JsonPatchDocument<TModel> : IJsonPatchDocument where TModel : class
public class JsonPatchDocument<TModel> : IJsonPatchDocument, IEndpointParameterMetadataProvider where TModel : class
{
public List<Operation<TModel>> Operations { get; private set; }

Expand Down Expand Up @@ -657,6 +659,21 @@ IList<Operation> IJsonPatchDocument.GetOperations()
return allOps;
}

/// <summary>
/// Populates metadata for the related endpoint when this type is used as a parameter.
/// </summary>
/// <param name="parameter">The <see cref="ParameterInfo"/> for the endpoint parameter.</param>
/// <param name="builder">The endpoint builder for the endpoint being constructed.</param>
#pragma warning disable RS0016 // Add public types and members to the declared API
public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
#pragma warning restore RS0016 // Add public types and members to the declared API
{
ArgumentNullException.ThrowIfNull(parameter);
ArgumentNullException.ThrowIfNull(builder);

builder.Metadata.Add(new AcceptsMetadata(new[] { "application/json-patch+json" }, parameter.ParameterType));
}

// Internal for testing
internal string GetPath<TProp>(Expression<Func<TModel, TProp>> expr, string position)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
<Compile Include="$(SharedSourceRoot)CallerArgument\CallerArgumentExpressionAttribute.cs" LinkBase="Shared" />
</ItemGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.AspNetCore.JsonPatch.SystemTextJson.Tests" />
</ItemGroup>
Expand Down
27 changes: 27 additions & 0 deletions src/Features/JsonPatch/src/JsonPatchDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.JsonPatch.Adapters;
using Microsoft.AspNetCore.JsonPatch.Converters;
using Microsoft.AspNetCore.JsonPatch.Exceptions;
Expand All @@ -12,13 +13,22 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

#if NET
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Metadata;
#endif

namespace Microsoft.AspNetCore.JsonPatch;

// Implementation details: the purpose of this type of patch document is to allow creation of such
// documents for cases where there's no class/DTO to work on. Typical use case: backend not built in
// .NET or architecture doesn't contain a shared DTO layer.
[JsonConverter(typeof(JsonPatchDocumentConverter))]
#if NET
public class JsonPatchDocument : IJsonPatchDocument, IEndpointParameterMetadataProvider
#else
public class JsonPatchDocument : IJsonPatchDocument
#endif
{
public List<Operation> Operations { get; private set; }

Expand Down Expand Up @@ -218,4 +228,21 @@ IList<Operation> IJsonPatchDocument.GetOperations()

return allOps;
}

#if NET
/// <summary>
/// Populates metadata for the related endpoint when this type is used as a parameter.
/// </summary>
/// <param name="parameter">The <see cref="ParameterInfo"/> for the endpoint parameter.</param>
/// <param name="builder">The endpoint builder for the endpoint being constructed.</param>
#pragma warning disable RS0016 // Add public types and members to the declared API
public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
#pragma warning restore RS0016 // Add public types and members to the declared API
{
ArgumentNullException.ThrowIfNull(parameter);
ArgumentNullException.ThrowIfNull(builder);

builder.Metadata.Add(new AcceptsMetadata(new[] { "application/json-patch+json" }, parameter.ParameterType));
}
#endif
}
27 changes: 27 additions & 0 deletions src/Features/JsonPatch/src/JsonPatchDocumentOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.AspNetCore.JsonPatch.Adapters;
using Microsoft.AspNetCore.JsonPatch.Converters;
using Microsoft.AspNetCore.JsonPatch.Exceptions;
Expand All @@ -15,14 +16,23 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

#if NET
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Metadata;
#endif

namespace Microsoft.AspNetCore.JsonPatch;

// Implementation details: the purpose of this type of patch document is to ensure we can do type-checking
// when producing a JsonPatchDocument. However, we cannot send this "typed" over the wire, as that would require
// including type data in the JsonPatchDocument serialized as JSON (to allow for correct deserialization) - that's
// not according to RFC 6902, and would thus break cross-platform compatibility.
[JsonConverter(typeof(TypedJsonPatchDocumentConverter))]
#if NET
public class JsonPatchDocument<TModel> : IJsonPatchDocument, IEndpointParameterMetadataProvider where TModel : class
#else
public class JsonPatchDocument<TModel> : IJsonPatchDocument where TModel : class
#endif
{
public List<Operation<TModel>> Operations { get; private set; }

Expand Down Expand Up @@ -656,6 +666,23 @@ IList<Operation> IJsonPatchDocument.GetOperations()
return allOps;
}

#if NET
/// <summary>
/// Populates metadata for the related endpoint when this type is used as a parameter.
/// </summary>
/// <param name="parameter">The <see cref="ParameterInfo"/> for the endpoint parameter.</param>
/// <param name="builder">The endpoint builder for the endpoint being constructed.</param>
#pragma warning disable RS0016 // Add public types and members to the declared API
public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
#pragma warning restore RS0016 // Add public types and members to the declared API
{
ArgumentNullException.ThrowIfNull(parameter);
ArgumentNullException.ThrowIfNull(builder);

builder.Metadata.Add(new AcceptsMetadata(new[] { "application/json-patch+json" }, parameter.ParameterType));
}
#endif

// Internal for testing
internal string GetPath<TProp>(Expression<Func<TModel, TProp>> expr, string position)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<ItemGroup>
<Reference Include="Microsoft.CSharp" Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'" />
<Reference Include="Newtonsoft.Json" />
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions src/OpenApi/sample/Endpoints/MapSchemasEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Immutable;
using System.ComponentModel;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;

public static class SchemasEndpointsExtensions
{
Expand Down Expand Up @@ -36,6 +37,7 @@ public static IEndpointRouteBuilder MapSchemasEndpoints(this IEndpointRouteBuild
schemas.MapPost("/location", (LocationContainer location) => { });
schemas.MapPost("/parent", (ParentObject parent) => Results.Ok(parent));
schemas.MapPost("/child", (ChildObject child) => Results.Ok(child));
schemas.MapPatch("/json-patch", (JsonPatchDocument<ParentObject> patchDoc) => Results.NoContent());

return endpointRouteBuilder;
}
Expand Down
1 change: 1 addition & 0 deletions src/OpenApi/sample/Sample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
<Reference Include="Microsoft.Extensions.FileProviders.Embedded" />
<Reference Include="Microsoft.AspNetCore.Mvc" />
<Reference Include="Microsoft.AspNetCore.JsonPatch.SystemTextJson" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,28 @@
}
}
}
},
"/schemas-by-ref/json-patch": {
"patch": {
"tags": [
"Sample"
],
"requestBody": {
"content": {
"application/json-patch+json": {
"schema": {
"$ref": "#/components/schemas/JsonPatchDocumentOfParentObject"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK"
}
}
}
}
},
"components": {
Expand Down Expand Up @@ -593,6 +615,7 @@
}
}
},
"JsonPatchDocumentOfParentObject": { },
"LocationContainer": {
"required": [
"location"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,28 @@
}
}
}
},
"/schemas-by-ref/json-patch": {
"patch": {
"tags": [
"Sample"
],
"requestBody": {
"content": {
"application/json-patch+json": {
"schema": {
"$ref": "#/components/schemas/JsonPatchDocumentOfParentObject"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK"
}
}
}
}
},
"components": {
Expand Down Expand Up @@ -593,6 +615,7 @@
}
}
},
"JsonPatchDocumentOfParentObject": { },
"LocationContainer": {
"required": [
"location"
Expand Down
Loading