Skip to content

Commit fe2dba7

Browse files
Handle derived types
- Handle custom types derived from `JsonPatchDocument` or `JsonPatchDocument<T>`. - Add extension method to remove duplicated type checks.
1 parent 449d575 commit fe2dba7

File tree

6 files changed

+107
-13
lines changed

6 files changed

+107
-13
lines changed

src/OpenApi/src/Extensions/JsonTypeInfoExtensions.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Text.Json;
77
using System.Text.Json.Serialization.Metadata;
88
using Microsoft.AspNetCore.Http;
9-
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
109

1110
namespace Microsoft.AspNetCore.OpenApi;
1211

@@ -33,7 +32,6 @@ internal static class JsonTypeInfoExtensions
3332
[typeof(string)] = "string",
3433
[typeof(IFormFile)] = "IFormFile",
3534
[typeof(IFormFileCollection)] = "IFormFileCollection",
36-
[typeof(JsonPatchDocument)] = "JsonPatchDocument",
3735
[typeof(PipeReader)] = "PipeReader",
3836
[typeof(Stream)] = "Stream"
3937
};
@@ -68,11 +66,12 @@ internal static class JsonTypeInfoExtensions
6866
return simpleName;
6967
}
7068

71-
// Use the same JSON Patch schema for all generic JsonPatchDocument<T> types
72-
// as otherwise we'll generate a schema per type argument which are otherwise identical.
73-
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(JsonPatchDocument<>))
69+
// Use the same JSON Patch schema for all JSON Patch document types (JsonPatchDocument,
70+
// JsonPatchDocument<T>, derived types, etc.) as otherwise we'll generate a schema
71+
// per unique type which are otherwise identical to each other.
72+
if (type.IsJsonPatchDocument())
7473
{
75-
return _simpleTypeToName[typeof(JsonPatchDocument)];
74+
return "JsonPatchDocument";
7675
}
7776

7877
// Although arrays are enumerable types they are not encoded correctly
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
5+
6+
namespace Microsoft.AspNetCore.OpenApi;
7+
8+
internal static class TypeExtensions
9+
{
10+
public static bool IsJsonPatchDocument(this Type type)
11+
{
12+
if (type.IsAssignableTo(typeof(JsonPatchDocument)))
13+
{
14+
return true;
15+
}
16+
17+
var modelType = type;
18+
19+
while (modelType != null && modelType != typeof(object))
20+
{
21+
if (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(JsonPatchDocument<>))
22+
{
23+
return true;
24+
}
25+
26+
modelType = modelType.BaseType;
27+
}
28+
29+
return false;
30+
}
31+
}

src/OpenApi/src/Services/OpenApiDocumentService.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
using Microsoft.AspNetCore.Http;
1818
using Microsoft.AspNetCore.Http.Extensions;
1919
using Microsoft.AspNetCore.Http.Metadata;
20-
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
2120
using Microsoft.AspNetCore.Mvc;
2221
using Microsoft.AspNetCore.Mvc.ApiExplorer;
2322
using Microsoft.AspNetCore.Mvc.Infrastructure;
@@ -721,7 +720,7 @@ private async Task<OpenApiRequestBody> GetJsonRequestBody(
721720
// for stream-based parameter types.
722721
supportedRequestFormats = [new ApiRequestFormat { MediaType = "application/octet-stream" }];
723722
}
724-
else if (bodyParameter.Type == typeof(JsonPatchDocument) || (bodyParameter.Type.IsGenericType && bodyParameter.Type.GetGenericTypeDefinition() == typeof(JsonPatchDocument<>)))
723+
else if (bodyParameter.Type.IsJsonPatchDocument())
725724
{
726725
// Assume "application/json-patch+json" as the default media type
727726
// for JSON Patch documents.

src/OpenApi/src/Services/OpenApiGenerator.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using System.Security.Claims;
1010
using Microsoft.AspNetCore.Http;
1111
using Microsoft.AspNetCore.Http.Metadata;
12-
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
1312
using Microsoft.AspNetCore.Mvc;
1413
using Microsoft.AspNetCore.Mvc.ApiExplorer;
1514
using Microsoft.AspNetCore.Mvc.Formatters;
@@ -450,8 +449,7 @@ private List<IOpenApiParameter> GetOpenApiParameters(MethodInfo methodInfo, Rout
450449
}
451450
else if (parameter.ParameterType == typeof(IFormFile) ||
452451
parameter.ParameterType == typeof(IFormFileCollection) ||
453-
parameter.ParameterType == typeof(JsonPatchDocument) ||
454-
(parameter.ParameterType.IsGenericType && parameter.ParameterType.GetGenericTypeDefinition() == typeof(JsonPatchDocument<>)))
452+
parameter.ParameterType.IsJsonPatchDocument())
455453
{
456454
return (true, null, null);
457455
}

src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
using System.Text.Json.Serialization.Metadata;
1414
using Microsoft.AspNetCore.Http;
1515
using Microsoft.AspNetCore.Http.Json;
16-
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
1716
using Microsoft.AspNetCore.Mvc.ApiExplorer;
1817
using Microsoft.AspNetCore.Mvc.Infrastructure;
1918
using Microsoft.Extensions.DependencyInjection;
@@ -83,7 +82,7 @@ internal sealed class OpenApiSchemaService(
8382
}
8483
};
8584
}
86-
else if (type == typeof(JsonPatchDocument) || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(JsonPatchDocument<>)))
85+
else if (type.IsJsonPatchDocument())
8786
{
8887
schema = CreateSchemaForJsonPatch();
8988
}

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.RequestBody.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,8 @@ await VerifyOpenApiDocument(builder, document =>
11531153
Assert.NotNull(operation.RequestBody.Content);
11541154
var content = Assert.Single(operation.RequestBody.Content);
11551155
Assert.Equal("application/json-patch+json", content.Key);
1156+
var schema = Assert.IsType<OpenApiSchemaReference>(content.Value.Schema);
1157+
Assert.Equal("JsonPatchDocument", schema.Reference.Id);
11561158
});
11571159
}
11581160

@@ -1206,6 +1208,8 @@ await VerifyOpenApiDocument(builder, document =>
12061208
Assert.NotNull(operation.RequestBody.Content);
12071209
var content = Assert.Single(operation.RequestBody.Content);
12081210
Assert.Equal("application/json-patch+json", content.Key);
1211+
var schema = Assert.IsType<OpenApiSchemaReference>(content.Value.Schema);
1212+
Assert.Equal("JsonPatchDocument", schema.Reference.Id);
12091213
});
12101214
}
12111215

@@ -1227,6 +1231,8 @@ await VerifyOpenApiDocument(builder, document =>
12271231
Assert.NotNull(operation.RequestBody.Content);
12281232
var content = Assert.Single(operation.RequestBody.Content);
12291233
Assert.Equal("application/vnd.github.patch+json", content.Key);
1234+
var schema = Assert.IsType<OpenApiSchemaReference>(content.Value.Schema);
1235+
Assert.Equal("JsonPatchDocument", schema.Reference.Id);
12301236
});
12311237
}
12321238

@@ -1248,6 +1254,8 @@ await VerifyOpenApiDocument(builder, document =>
12481254
Assert.NotNull(operation.RequestBody.Content);
12491255
var content = Assert.Single(operation.RequestBody.Content);
12501256
Assert.Equal("application/vnd.github.patch+json", content.Key);
1257+
var schema = Assert.IsType<OpenApiSchemaReference>(content.Value.Schema);
1258+
Assert.Equal("JsonPatchDocument", schema.Reference.Id);
12511259
});
12521260
}
12531261

@@ -1270,6 +1278,8 @@ await VerifyOpenApiDocument(builder, document =>
12701278
Assert.NotNull(operation.RequestBody.Content);
12711279
var content = Assert.Single(operation.RequestBody.Content);
12721280
Assert.Equal("application/json-patch+json", content.Key);
1281+
var schema = Assert.IsType<OpenApiSchemaReference>(content.Value.Schema);
1282+
Assert.Equal("JsonPatchDocument", schema.Reference.Id);
12731283
});
12741284
}
12751285

@@ -1323,6 +1333,8 @@ await VerifyOpenApiDocument(builder, document =>
13231333
Assert.NotNull(operation.RequestBody.Content);
13241334
var content = Assert.Single(operation.RequestBody.Content);
13251335
Assert.Equal("application/json-patch+json", content.Key);
1336+
var schema = Assert.IsType<OpenApiSchemaReference>(content.Value.Schema);
1337+
Assert.Equal("JsonPatchDocument", schema.Reference.Id);
13261338
});
13271339
}
13281340

@@ -1345,6 +1357,8 @@ await VerifyOpenApiDocument(builder, document =>
13451357
Assert.NotNull(operation.RequestBody.Content);
13461358
var content = Assert.Single(operation.RequestBody.Content);
13471359
Assert.Equal("application/vnd.github.patch+json", content.Key);
1360+
var schema = Assert.IsType<OpenApiSchemaReference>(content.Value.Schema);
1361+
Assert.Equal("JsonPatchDocument", schema.Reference.Id);
13481362
});
13491363
}
13501364

@@ -1366,6 +1380,8 @@ await VerifyOpenApiDocument(builder, document =>
13661380
Assert.NotNull(operation.RequestBody.Content);
13671381
var content = Assert.Single(operation.RequestBody.Content);
13681382
Assert.Equal("application/vnd.github.patch+json", content.Key);
1383+
var schema = Assert.IsType<OpenApiSchemaReference>(content.Value.Schema);
1384+
Assert.Equal("JsonPatchDocument", schema.Reference.Id);
13691385
});
13701386
}
13711387

@@ -1379,4 +1395,56 @@ private sealed class JsonPatchModel
13791395
public string? Second { get; set; }
13801396
}
13811397
#nullable restore
1398+
1399+
[Fact]
1400+
public async Task GetRequestBody_HandlesCustomJsonPatchBody()
1401+
{
1402+
// Arrange
1403+
var builder = CreateBuilder();
1404+
1405+
// Act
1406+
builder.MapPatch("/", (CustomJsonPatchDocument patch) => { });
1407+
1408+
// Assert
1409+
await VerifyOpenApiDocument(builder, document =>
1410+
{
1411+
var paths = Assert.Single(document.Paths.Values);
1412+
var operation = paths.Operations[HttpMethod.Patch];
1413+
Assert.NotNull(operation.RequestBody);
1414+
Assert.False(operation.RequestBody.Required);
1415+
Assert.NotNull(operation.RequestBody.Content);
1416+
var content = Assert.Single(operation.RequestBody.Content);
1417+
Assert.Equal("application/json-patch+json", content.Key);
1418+
var schema = Assert.IsType<OpenApiSchemaReference>(content.Value.Schema);
1419+
Assert.Equal("JsonPatchDocument", schema.Reference.Id);
1420+
});
1421+
}
1422+
1423+
private class CustomJsonPatchDocument : JsonPatchDocument;
1424+
1425+
[Fact]
1426+
public async Task GetRequestBody_HandlesGenericCustomJsonPatchBody()
1427+
{
1428+
// Arrange
1429+
var builder = CreateBuilder();
1430+
1431+
// Act
1432+
builder.MapPatch("/", (CustomJsonPatchDocument<JsonPatchModel> patch) => { });
1433+
1434+
// Assert
1435+
await VerifyOpenApiDocument(builder, document =>
1436+
{
1437+
var paths = Assert.Single(document.Paths.Values);
1438+
var operation = paths.Operations[HttpMethod.Patch];
1439+
Assert.NotNull(operation.RequestBody);
1440+
Assert.False(operation.RequestBody.Required);
1441+
Assert.NotNull(operation.RequestBody.Content);
1442+
var content = Assert.Single(operation.RequestBody.Content);
1443+
Assert.Equal("application/json-patch+json", content.Key);
1444+
var schema = Assert.IsType<OpenApiSchemaReference>(content.Value.Schema);
1445+
Assert.Equal("JsonPatchDocument", schema.Reference.Id);
1446+
});
1447+
}
1448+
1449+
private class CustomJsonPatchDocument<T> : JsonPatchDocument<T> where T : class;
13821450
}

0 commit comments

Comments
 (0)