From 612f9dcab68ef5d5b9725bc92046030734322150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Wed, 20 Nov 2024 19:05:52 +0100 Subject: [PATCH 1/3] Support DescriptionAttribute and ReadOnlyAttribute --- .../SchemaGenerator/OpenApiSchemaExtensions.cs | 18 ++++++++++++++++++ ...or_WebApi_swaggerRequestUri=v1.verified.txt | 5 +++++ ...Test.TypesAreRenderedCorrectly.verified.txt | 5 +++++ .../NewtonsoftSchemaGeneratorTests.cs | 10 ++++++---- .../JsonSerializerSchemaGeneratorTests.cs | 4 +++- .../Fixtures/TypeWithValidationAttributes.cs | 9 ++++++++- ...eWithValidationAttributesViaMetadataType.cs | 13 ++++++++++++- .../WebApi/EndPoints/OpenApiEndpoints.cs | 7 ++++--- 8 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs index c6bf6bf059..9aec763cf3 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Routing.Constraints; @@ -60,6 +61,13 @@ public static void ApplyValidationAttributes(this OpenApiSchema schema, IEnumera else if (attribute is StringLengthAttribute stringLengthAttribute) ApplyStringLengthAttribute(schema, stringLengthAttribute); + + else if (attribute is ReadOnlyAttribute readOnlyAttribute) + ApplyReadOnlyAttribute(schema, readOnlyAttribute); + + else if (attribute is DescriptionAttribute descriptionAttribute) + ApplyDescriptionAttribute(schema, descriptionAttribute); + } } @@ -230,6 +238,16 @@ private static void ApplyStringLengthAttribute(OpenApiSchema schema, StringLengt schema.MaxLength = stringLengthAttribute.MaximumLength; } + private static void ApplyReadOnlyAttribute(OpenApiSchema schema, ReadOnlyAttribute readOnlyAttribute) + { + schema.ReadOnly = readOnlyAttribute.IsReadOnly; + } + + private static void ApplyDescriptionAttribute(OpenApiSchema schema, DescriptionAttribute descriptionAttribute) + { + schema.Description = descriptionAttribute.Description; + } + private static void ApplyLengthRouteConstraint(OpenApiSchema schema, LengthRouteConstraint lengthRouteConstraint) { schema.MinLength = lengthRouteConstraint.MinLength; 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 fe08716caa..84b7b56421 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 @@ -972,6 +972,7 @@ "properties": { "currencyFrom": { "type": "string", + "description": "Currency From", "nullable": true }, "currencyTo": { @@ -1017,6 +1018,10 @@ "$ref": "#/components/schemas/CurrenciesRate" }, "nullable": true + }, + "isUpdated": { + "type": "boolean", + "readOnly": true } }, "additionalProperties": false diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt index fe08716caa..84b7b56421 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt @@ -972,6 +972,7 @@ "properties": { "currencyFrom": { "type": "string", + "description": "Currency From", "nullable": true }, "currencyTo": { @@ -1017,6 +1018,10 @@ "$ref": "#/components/schemas/CurrenciesRate" }, "nullable": true + }, + "isUpdated": { + "type": "boolean", + "readOnly": true } }, "additionalProperties": false diff --git a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs index 922c1bc856..4fcc339fb7 100644 --- a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs @@ -7,16 +7,16 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.NewtonsoftJson; -using Microsoft.OpenApi.Models; using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using Swashbuckle.AspNetCore.SwaggerGen; +using Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures; using Swashbuckle.AspNetCore.TestSupport; using Xunit; -using Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures; namespace Swashbuckle.AspNetCore.Newtonsoft.Test { @@ -340,7 +340,9 @@ public void GenerateSchema_SetsValidationProperties_IfComplexTypeHasValidationAt Assert.False(schema.Properties["StringWithRequired"].Nullable); Assert.False(schema.Properties["StringWithRequiredAllowEmptyTrue"].Nullable); Assert.Null(schema.Properties["StringWithRequiredAllowEmptyTrue"].MinLength); - Assert.Equal(new[] { "StringWithRequired", "StringWithRequiredAllowEmptyTrue" }, schema.Required.ToArray()); + Assert.Equal(["StringWithRequired", "StringWithRequiredAllowEmptyTrue"], schema.Required); + Assert.Equal("Description", schema.Properties[nameof(TypeWithValidationAttributes.StringWithDescription)].Description); + Assert.True(schema.Properties[nameof(TypeWithValidationAttributes.StringWithReadOnly)].ReadOnly); } [Fact] @@ -788,7 +790,7 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonRequired() var referenceSchema = Subject().GenerateSchema(typeof(JsonRequiredAnnotatedType), schemaRepository); var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; - Assert.Equal(new[] { "StringWithConflictingRequired", "StringWithJsonRequired"}, schema.Required.ToArray()); + Assert.Equal(new[] { "StringWithConflictingRequired", "StringWithJsonRequired" }, schema.Required.ToArray()); Assert.False(schema.Properties["StringWithJsonRequired"].Nullable); } diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs index 2e08909d1a..c7a126fed0 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs @@ -364,7 +364,9 @@ public void GenerateSchema_SetsValidationProperties_IfComplexTypeHasValidationAt Assert.False(schema.Properties["StringWithRequired"].Nullable); Assert.False(schema.Properties["StringWithRequiredAllowEmptyTrue"].Nullable); Assert.Null(schema.Properties["StringWithRequiredAllowEmptyTrue"].MinLength); - Assert.Equal(new[] { "StringWithRequired", "StringWithRequiredAllowEmptyTrue" }, schema.Required.ToArray()); + Assert.Equal(["StringWithRequired", "StringWithRequiredAllowEmptyTrue"], schema.Required); + Assert.Equal("Description", schema.Properties[nameof(TypeWithValidationAttributes.StringWithDescription)].Description); + Assert.True(schema.Properties[nameof(TypeWithValidationAttributes.StringWithReadOnly)].ReadOnly); } [Fact] diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs index ff23792668..8eac3ef444 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; namespace Swashbuckle.AspNetCore.TestSupport { @@ -43,5 +44,11 @@ public class TypeWithValidationAttributes [Required(AllowEmptyStrings = true)] public string StringWithRequiredAllowEmptyTrue { get; set; } + + [Description("Description")] + public string StringWithDescription { get; set; } + + [ReadOnly(true)] + public string StringWithReadOnly { get; set; } } } diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs index 2cda5471da..6dcdf41bbe 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc; namespace Swashbuckle.AspNetCore.TestSupport @@ -33,6 +34,10 @@ public class TypeWithValidationAttributesViaMetadataType public string StringWithRequired { get; set; } public string StringWithRequiredAllowEmptyTrue { get; set; } + + public string StringWithDescription { get; set; } + + public string StringWithReadOnly { get; set; } } public class MetadataType @@ -76,5 +81,11 @@ public class MetadataType [Required(AllowEmptyStrings = true)] public string StringWithRequiredAllowEmptyTrue { get; set; } + + [Description("Description")] + public string StringWithDescription { get; set; } + + [ReadOnly(true)] + public string StringWithReadOnly { get; set; } } } diff --git a/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs b/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs index a3406f7327..a693a02343 100644 --- a/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs +++ b/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.ComponentModel; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; namespace WebApi.EndPoints @@ -95,8 +96,8 @@ 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, double Rate); + sealed record OrganizationCustomExchangeRatesDto([property: JsonRequired] CurrenciesRate[] CurrenciesRates, [property: ReadOnly(true)] bool IsUpdated); + sealed record CurrenciesRate([property: JsonRequired, Description("Currency From")] string CurrencyFrom, [property: JsonRequired] string CurrencyTo, double Rate); record TypeWithTryParse(string Name) { public static bool TryParse(string value, out TypeWithTryParse? result) From ac032ce76c58618f08a4d4771ba98ad65b76e799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Fri, 22 Nov 2024 04:29:49 +0100 Subject: [PATCH 2/3] PR suggestions --- .../SchemaGenerator/OpenApiSchemaExtensions.cs | 2 +- .../SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs index 9aec763cf3..e570bb28ab 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs @@ -245,7 +245,7 @@ private static void ApplyReadOnlyAttribute(OpenApiSchema schema, ReadOnlyAttribu private static void ApplyDescriptionAttribute(OpenApiSchema schema, DescriptionAttribute descriptionAttribute) { - schema.Description = descriptionAttribute.Description; + schema.Description ??= descriptionAttribute.Description; } private static void ApplyLengthRouteConstraint(OpenApiSchema schema, LengthRouteConstraint lengthRouteConstraint) diff --git a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs index 4fcc339fb7..685e633c3e 100644 --- a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs @@ -790,7 +790,7 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonRequired() var referenceSchema = Subject().GenerateSchema(typeof(JsonRequiredAnnotatedType), schemaRepository); var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; - Assert.Equal(new[] { "StringWithConflictingRequired", "StringWithJsonRequired" }, schema.Required.ToArray()); + Assert.Equal(["StringWithConflictingRequired", "StringWithJsonRequired"], schema.Required); Assert.False(schema.Properties["StringWithJsonRequired"].Nullable); } From 325cfaae35f6b7241d747e7928581433f3011e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Fri, 22 Nov 2024 04:46:31 +0100 Subject: [PATCH 3/3] Use of more collectionExpressions --- .../NewtonsoftSchemaGeneratorTests.cs | 37 ++++++++----------- .../JsonSerializerSchemaGeneratorTests.cs | 12 +++--- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs index 685e633c3e..c726decdd0 100644 --- a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs @@ -738,7 +738,7 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonIgnore() var referenceSchema = Subject().GenerateSchema(typeof(JsonIgnoreAnnotatedType), schemaRepository); var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; - Assert.Equal(new[] { /* "StringWithJsonIgnore" */ "StringWithNoAnnotation" }, schema.Properties.Keys.ToArray()); + Assert.Equal(["StringWithNoAnnotation"], schema.Properties.Keys); } [Fact] @@ -750,8 +750,7 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonProperty() var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; Assert.Equal( - new[] - { + [ "string-with-json-property-name", "IntWithRequiredDefault", "StringWithRequiredDefault", @@ -760,17 +759,16 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonProperty() "StringWithRequiredAllowNull", "StringWithRequiredAlwaysButConflictingDataMember", "StringWithRequiredDefaultButConflictingDataMember" - }, - schema.Properties.Keys.ToArray() + ], + schema.Properties.Keys ); Assert.Equal( - new[] - { + [ "StringWithRequiredAllowNull", "StringWithRequiredAlways", "StringWithRequiredAlwaysButConflictingDataMember" - }, - schema.Required.ToArray() + ], + schema.Required ); Assert.True(schema.Properties["string-with-json-property-name"].Nullable); Assert.False(schema.Properties["IntWithRequiredDefault"].Nullable); @@ -803,14 +801,13 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonObject() var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; Assert.Equal( - new[] - { + [ "StringWithDataMemberRequiredFalse", "StringWithNoAnnotation", "StringWithRequiredAllowNull", "StringWithRequiredUnspecified" - }, - schema.Required.ToArray() + ], + schema.Required ); Assert.False(schema.Properties["StringWithNoAnnotation"].Nullable); Assert.False(schema.Properties["StringWithRequiredUnspecified"].Nullable); @@ -863,24 +860,22 @@ public void GenerateSchema_HonorsDataMemberAttribute() Assert.True(schema.Properties["NonRequiredWithCustomNameFromDataMember"].Nullable); Assert.Equal( - new[] - { + [ "StringWithDataMemberRequired", "StringWithDataMemberNonRequired", "RequiredWithCustomNameFromDataMember", "NonRequiredWithCustomNameFromDataMember" - }, - schema.Properties.Keys.ToArray() + ], + schema.Properties.Keys ); Assert.Equal( - new[] - { + [ "RequiredWithCustomNameFromDataMember", "StringWithDataMemberRequired" - }, - schema.Required.ToArray() + ], + schema.Required ); } diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs index c7a126fed0..2959018a2d 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs @@ -422,7 +422,7 @@ public void GenerateSchema_SetsRequired_IfPropertyHasRequiredKeywordAndValidatio var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; Assert.Equal(1, schema.Properties["RequiredProperty"].MinLength); Assert.True(schema.Properties["RequiredProperty"].Nullable); - Assert.Equal(new[] { "RequiredProperty" }, schema.Required.ToArray()); + Assert.Equal(["RequiredProperty"], schema.Required); } #nullable enable @@ -1155,14 +1155,14 @@ public void GenerateSchema_HonorsSerializerAttributes_JsonIgnore() var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; string[] expectedKeys = - { + [ nameof(JsonIgnoreAnnotatedType.StringWithJsonIgnoreConditionNever), nameof(JsonIgnoreAnnotatedType.StringWithJsonIgnoreConditionWhenWritingDefault), nameof(JsonIgnoreAnnotatedType.StringWithJsonIgnoreConditionWhenWritingNull), nameof(JsonIgnoreAnnotatedType.StringWithNoAnnotation) - }; + ]; - Assert.Equal(expectedKeys, schema.Properties.Keys.ToArray()); + Assert.Equal(expectedKeys, schema.Properties.Keys); } [Fact] @@ -1173,7 +1173,7 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonPropertyName() var referenceSchema = Subject().GenerateSchema(typeof(JsonPropertyNameAnnotatedType), schemaRepository); var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; - Assert.Equal(new[] { "string-with-json-property-name" }, schema.Properties.Keys.ToArray()); + Assert.Equal(["string-with-json-property-name"], schema.Properties.Keys); } #if NET7_0_OR_GREATER @@ -1185,7 +1185,7 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonRequired() var referenceSchema = Subject().GenerateSchema(typeof(JsonRequiredAnnotatedType), schemaRepository); var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; - Assert.Equal(new[] { "StringWithJsonRequired" }, schema.Required.ToArray()); + Assert.Equal(["StringWithJsonRequired"], schema.Required); Assert.True(schema.Properties["StringWithJsonRequired"].Nullable); } #endif