From 5d72abee99c1131c96e3c2aa3ebd2af6c65d9ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Thu, 18 Jul 2024 17:24:11 +0200 Subject: [PATCH 1/4] Take into account JsonRequired of STJ --- .../JsonSerializerDataContractResolver.cs | 9 +++++++-- .../Fixtures/JsonRequiredAnnotatedType.cs | 13 +++++++++++++ .../JsonSerializerSchemaGeneratorTests.cs | 14 ++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/JsonRequiredAnnotatedType.cs diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs index fde8deaad9..c048d970ed 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs @@ -210,11 +210,13 @@ private List GetDataPropertiesFor(Type objectType, out Type extens // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-immutability?pivots=dotnet-6-0 var isDeserializedViaConstructor = false; + var isRequired = false; + #if NET5_0_OR_GREATER var deserializationConstructor = propertyInfo.DeclaringType?.GetConstructors() .OrderBy(c => { - if (c.GetCustomAttribute() != null) return 1; + if (c.GetCustomAttribute() != null) return 1; if (c.GetParameters().Length == 0) return 2; return 3; }) @@ -228,11 +230,14 @@ private List GetDataPropertiesFor(Type objectType, out Type extens string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase); }); #endif +#if NET7_0_OR_GREATER + isRequired = propertyInfo.GetCustomAttribute() != null; +#endif dataProperties.Add( new DataProperty( name: name, - isRequired: false, + isRequired: isRequired, isNullable: propertyInfo.PropertyType.IsReferenceOrNullableType(), isReadOnly: propertyInfo.IsPubliclyReadable() && !propertyInfo.IsPubliclyWritable() && !isDeserializedViaConstructor, isWriteOnly: propertyInfo.IsPubliclyWritable() && !propertyInfo.IsPubliclyReadable(), diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/JsonRequiredAnnotatedType.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/JsonRequiredAnnotatedType.cs new file mode 100644 index 0000000000..e7d1867109 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/JsonRequiredAnnotatedType.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures +{ + internal class JsonRequiredAnnotatedType + { + +#if NET7_0_OR_GREATER + [JsonRequired] +#endif + public string StringWithJsonRequired { get; set; } + } +} diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs index 11d1be49a4..6e2c10240a 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs @@ -885,6 +885,20 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonPropertyName() Assert.Equal(new[] { "string-with-json-property-name" }, schema.Properties.Keys.ToArray()); } +#if NET7_0_OR_GREATER + [Fact] + public void GenerateSchema_HonorsSerializerAttribute_JsonRequired() + { + var schemaRepository = new SchemaRepository(); + + var referenceSchema = Subject().GenerateSchema(typeof(JsonRequiredAnnotatedType), schemaRepository); + + var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; + Assert.Equal(new[] { "StringWithJsonRequired" }, schema.Required.ToArray()); + Assert.True(schema.Properties["StringWithJsonRequired"].Nullable); + } +#endif + [Fact] public void GenerateSchema_HonorsSerializerAttribute_JsonExtensionData() { From 55d8841fb035806ba5691bc48c1a1cb5bff98ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Thu, 18 Jul 2024 17:29:34 +0200 Subject: [PATCH 2/4] FileScope nameSpaces --- .../Fixtures/JsonRequiredAnnotatedType.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/JsonRequiredAnnotatedType.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/JsonRequiredAnnotatedType.cs index e7d1867109..200394acc6 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/JsonRequiredAnnotatedType.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/JsonRequiredAnnotatedType.cs @@ -1,13 +1,12 @@ using System.Text.Json.Serialization; -namespace Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures +namespace Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures; + +internal class JsonRequiredAnnotatedType { - internal class JsonRequiredAnnotatedType - { #if NET7_0_OR_GREATER - [JsonRequired] + [JsonRequired] #endif - public string StringWithJsonRequired { get; set; } - } + public string StringWithJsonRequired { get; set; } } From 85d39c1e59a8ef80e644e2702c61bafc1b0afd3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Thu, 18 Jul 2024 18:28:46 +0200 Subject: [PATCH 3/4] Add VerificationTests --- ...r_WebApi_swaggerRequestUri=v1.verified.txt | 68 +++++++++++++++++++ ...est.TypesAreRenderedCorrectly.verified.txt | 68 +++++++++++++++++++ .../WebApi/EndPoints/OpenApiEndpoints.cs | 10 ++- 3 files changed, 145 insertions(+), 1 deletion(-) diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt index 7349ea852c..b5c438c6b2 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt @@ -330,6 +330,35 @@ } } }, + "/WithOpenApi/IFromBody": { + "post": { + "tags": [ + "WithOpenApi" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationCustomExchangeRatesDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, "/XmlComments/Car/{id}": { "get": { "tags": [ @@ -445,6 +474,29 @@ }, "additionalProperties": false }, + "CurrenciesRate": { + "required": [ + "currencyFrom", + "currencyTo", + "rate" + ], + "type": "object", + "properties": { + "currencyFrom": { + "type": "string", + "nullable": true + }, + "currencyTo": { + "type": "string", + "nullable": true + }, + "rate": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, "DateTimeKind": { "enum": [ 0, @@ -465,6 +517,22 @@ "additionalProperties": false, "description": "Description for Schema" }, + "OrganizationCustomExchangeRatesDto": { + "required": [ + "currenciesRates" + ], + "type": "object", + "properties": { + "currenciesRates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CurrenciesRate" + }, + "nullable": true + } + }, + "additionalProperties": false + }, "Person": { "type": "object", "properties": { diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt index 7349ea852c..b5c438c6b2 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt @@ -330,6 +330,35 @@ } } }, + "/WithOpenApi/IFromBody": { + "post": { + "tags": [ + "WithOpenApi" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationCustomExchangeRatesDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, "/XmlComments/Car/{id}": { "get": { "tags": [ @@ -445,6 +474,29 @@ }, "additionalProperties": false }, + "CurrenciesRate": { + "required": [ + "currencyFrom", + "currencyTo", + "rate" + ], + "type": "object", + "properties": { + "currencyFrom": { + "type": "string", + "nullable": true + }, + "currencyTo": { + "type": "string", + "nullable": true + }, + "rate": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, "DateTimeKind": { "enum": [ 0, @@ -465,6 +517,22 @@ "additionalProperties": false, "description": "Description for Schema" }, + "OrganizationCustomExchangeRatesDto": { + "required": [ + "currenciesRates" + ], + "type": "object", + "properties": { + "currenciesRates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CurrenciesRate" + }, + "nullable": true + } + }, + "additionalProperties": false + }, "Person": { "type": "object", "properties": { diff --git a/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs b/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs index 49fd09297f..061b15375e 100644 --- a/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs +++ b/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Mvc; namespace WebApi.EndPoints { @@ -51,6 +52,11 @@ public static IEndpointRouteBuilder MapWithOpenApiEndpoints(this IEndpointRouteB return $"{collection.Count} {string.Join(',', collection.Select(f => f.FileName))}"; }).WithOpenApi(); + group.MapPost("/IFromBody", (OrganizationCustomExchangeRatesDto dto) => + { + return $"{dto}"; + }).WithOpenApi(); + return app; } } @@ -61,4 +67,6 @@ record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) record class Person(string FirstName, string LastName); record class Address(string Street, string City, string State, string ZipCode); + sealed record OrganizationCustomExchangeRatesDto([property: JsonRequired] CurrenciesRate[] CurrenciesRates); + sealed record CurrenciesRate([property: JsonRequired] string currencyFrom, [property: JsonRequired] string currencyTo, [property: JsonRequired] double rate); } From 9bd77e57dd37a4ac71228bad6755c9fa9169fca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Thu, 18 Jul 2024 18:39:20 +0200 Subject: [PATCH 4/4] Make one of the properties not required --- ...lidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt | 3 +-- ...erifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt | 3 +-- test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt index b5c438c6b2..a024a3215d 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt @@ -477,8 +477,7 @@ "CurrenciesRate": { "required": [ "currencyFrom", - "currencyTo", - "rate" + "currencyTo" ], "type": "object", "properties": { diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt index b5c438c6b2..a024a3215d 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt @@ -477,8 +477,7 @@ "CurrenciesRate": { "required": [ "currencyFrom", - "currencyTo", - "rate" + "currencyTo" ], "type": "object", "properties": { diff --git a/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs b/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs index 061b15375e..f8c22e9244 100644 --- a/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs +++ b/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs @@ -68,5 +68,5 @@ record class Person(string FirstName, string LastName); record class Address(string Street, string City, string State, string ZipCode); sealed record OrganizationCustomExchangeRatesDto([property: JsonRequired] CurrenciesRate[] CurrenciesRates); - sealed record CurrenciesRate([property: JsonRequired] string currencyFrom, [property: JsonRequired] string currencyTo, [property: JsonRequired] double rate); + sealed record CurrenciesRate([property: JsonRequired] string CurrencyFrom, [property: JsonRequired] string CurrencyTo, double Rate); }