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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 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,17 @@ 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>
static void IEndpointParameterMetadataProvider.PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
{
ArgumentNullException.ThrowIfNull(parameter);
ArgumentNullException.ThrowIfNull(builder);

builder.Metadata.Add(new AcceptsMetadata(["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,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>
static void IEndpointParameterMetadataProvider.PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
{
ArgumentNullException.ThrowIfNull(parameter);
ArgumentNullException.ThrowIfNull(builder);

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

// 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
25 changes: 25 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,19 @@ 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>
static void IEndpointParameterMetadataProvider.PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
{
ArgumentNullException.ThrowIfNull(parameter);
ArgumentNullException.ThrowIfNull(builder);

builder.Metadata.Add(new AcceptsMetadata(["application/json-patch+json"], parameter.ParameterType));
}
#endif
}
25 changes: 25 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,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>
static void IEndpointParameterMetadataProvider.PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
{
ArgumentNullException.ThrowIfNull(parameter);
ArgumentNullException.ThrowIfNull(builder);

builder.Metadata.Add(new AcceptsMetadata(["application/json-patch+json"], typeof(TModel)));
}
#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 @@ -16,6 +16,11 @@
<Compile Include="$(SharedSourceRoot)CallerArgument\CallerArgumentExpressionAttribute.cs" LinkBase="Shared" />
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="PublicAPI/$(TargetFramework)/PublicAPI.Shipped.txt" />
<AdditionalFiles Include="PublicAPI/$(TargetFramework)/PublicAPI.Unshipped.txt" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != '$(DefaultNetCoreTargetFramework)'">
<Compile Include="$(SharedSourceRoot)Nullable\NullableAttributes.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)TrimmingAttributes.cs" LinkBase="Shared" />
Expand All @@ -24,6 +29,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