diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs index de2ac497fc..435d6ed594 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs @@ -67,13 +67,25 @@ private OpenApiSchema GenerateSchemaForMember( if (dataProperty != null) { var requiredAttribute = customAttributes.OfType().FirstOrDefault(); + + schema.ReadOnly = dataProperty.IsReadOnly; + schema.WriteOnly = dataProperty.IsWriteOnly; + +#if NET7_0_OR_GREATER + var hasRequiredMemberAttribute = customAttributes.OfType().Any(); + + schema.Nullable = _generatorOptions.SupportNonNullableReferenceTypes + ? dataProperty.IsNullable && requiredAttribute == null && !hasRequiredMemberAttribute && !memberInfo.IsNonNullableReferenceType() + : dataProperty.IsNullable && requiredAttribute == null && !hasRequiredMemberAttribute; + + schema.MinLength = modelType == typeof(string) && (hasRequiredMemberAttribute || requiredAttribute is { AllowEmptyStrings: false }) ? 1 : null; +#else schema.Nullable = _generatorOptions.SupportNonNullableReferenceTypes ? dataProperty.IsNullable && requiredAttribute==null && !memberInfo.IsNonNullableReferenceType() : dataProperty.IsNullable && requiredAttribute==null; - schema.ReadOnly = dataProperty.IsReadOnly; - schema.WriteOnly = dataProperty.IsWriteOnly; schema.MinLength = modelType == typeof(string) && requiredAttribute is { AllowEmptyStrings: false } ? 1 : null; +#endif } var defaultValueAttribute = customAttributes.OfType().FirstOrDefault(); @@ -392,7 +404,13 @@ private OpenApiSchema CreateObjectSchema(DataContract dataContract, SchemaReposi ? GenerateSchemaForMember(dataProperty.MemberType, schemaRepository, dataProperty.MemberInfo, dataProperty) : GenerateSchemaForType(dataProperty.MemberType, schemaRepository); - if ((dataProperty.IsRequired || customAttributes.OfType().Any()) + if (( + dataProperty.IsRequired + || customAttributes.OfType().Any() +#if NET7_0_OR_GREATER + || customAttributes.OfType().Any() +#endif + ) && !schema.Required.Contains(dataProperty.Name)) { schema.Required.Add(dataProperty.Name); diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs index d8442ac9cb..021e2c495e 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs @@ -15,7 +15,10 @@ public static class ApiParameterDescriptionExtensions private static readonly Type[] RequiredAttributeTypes = new[] { typeof(BindRequiredAttribute), - typeof(RequiredAttribute) + typeof(RequiredAttribute), +#if NET7_0_OR_GREATER + typeof(System.Runtime.CompilerServices.RequiredMemberAttribute) +#endif }; public static bool IsRequiredParameter(this ApiParameterDescription apiParameter) @@ -109,4 +112,4 @@ internal static bool IsFromForm(this ApiParameterDescription apiParameter) || (elementType != null && typeof(IFormFile).IsAssignableFrom(elementType)); } } -} \ No newline at end of file +} diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs index 4bf3fdcada..fa2786cd8a 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs @@ -52,6 +52,16 @@ public void ActionWithIntParameterWithRequiredAttribute([Required]int param) public void ActionWithObjectParameter(XmlAnnotatedType param) { } +#if NET7_0_OR_GREATER + public class TypeWithRequiredProperty + { + public required string RequiredProperty { get; set; } + } + + public void ActionWithRequiredMember(TypeWithRequiredProperty param) + { } +#endif + [Consumes("application/someMediaType")] public void ActionWithConsumesAttribute(string param) { } @@ -67,4 +77,4 @@ public int ActionWithProducesAttribute() throw new NotImplementedException(); } } -} \ No newline at end of file +} diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs index 30da476cec..a37041c470 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs @@ -1,20 +1,21 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Dynamic; using System.Linq; +using System.Net; using System.Text.Json; using System.Text.Json.Serialization; -using System.Net; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; -using Xunit; using Swashbuckle.AspNetCore.TestSupport; -using Microsoft.OpenApi.Any; +using Xunit; namespace Swashbuckle.AspNetCore.SwaggerGen.Test { @@ -343,6 +344,43 @@ public void GenerateSchema_SetsReadOnlyAndWriteOnlyFlags_IfPropertyIsRestricted( Assert.True(schema.Properties["WriteOnlyProperty"].WriteOnly); } +#if NET7_0_OR_GREATER + public class TypeWithRequiredProperty + { + public required string RequiredProperty { get; set; } + } + + public class TypeWithRequiredPropertyAndValidationAttribute + { + [MinLength(1)] + public required string RequiredProperty { get; set; } + } + + [Fact] + public void GenerateSchema_SetsRequired_IfPropertyHasRequiredKeyword() + { + var schemaRepository = new SchemaRepository(); + + var referenceSchema = Subject().GenerateSchema(typeof(TypeWithRequiredProperty), schemaRepository); + + var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; + Assert.Equal(new[] { "RequiredProperty" }, schema.Required.ToArray()); + } + + [Fact] + public void GenerateSchema_SetsRequired_IfPropertyHasRequiredKeywordAndValidationAttribute() + { + var schemaRepository = new SchemaRepository(); + + var referenceSchema = Subject().GenerateSchema(typeof(TypeWithRequiredPropertyAndValidationAttribute), schemaRepository); + + var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; + Assert.Equal(1, schema.Properties["RequiredProperty"].MinLength); + Assert.False(schema.Properties["RequiredProperty"].Nullable); + Assert.Equal(new[] { "RequiredProperty" }, schema.Required.ToArray()); + } +#endif + [Theory] [InlineData(typeof(TypeWithParameterizedConstructor), nameof(TypeWithParameterizedConstructor.Id), false)] [InlineData(typeof(TypeWithParameterlessAndParameterizedConstructor), nameof(TypeWithParameterlessAndParameterizedConstructor.Id), true)] diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs index f384f09502..37966f70c6 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs @@ -514,6 +514,38 @@ public void GetSwagger_SetsParameterRequired_IfActionParameterHasRequiredOrBindR Assert.Equal(expectedRequired, parameter.Required); } +#if NET7_0_OR_GREATER + [Fact] + public void GetSwagger_SetsParameterRequired_IfActionParameterHasRequiredMember() + { + var subject = Subject( + apiDescriptions: new[] + { + ApiDescriptionFactory.Create( + methodInfo: typeof(FakeController).GetMethod(nameof(FakeController.ActionWithRequiredMember)), + groupName: "v1", + httpMethod: "POST", + relativePath: "resource", + parameterDescriptions: new [] + { + new ApiParameterDescription + { + Name = "param", + Source = BindingSource.Query, + ModelMetadata = ModelMetadataFactory.CreateForProperty(typeof(FakeController.TypeWithRequiredProperty), "RequiredProperty") + } + }) + } + ); + + var document = subject.GetSwagger("v1"); + + var operation = document.Paths["/resource"].Operations[OperationType.Post]; + var parameter = Assert.Single(operation.Parameters); + Assert.True(parameter.Required); + } +#endif + [Theory] [InlineData(false)] [InlineData(true)] diff --git a/test/Swashbuckle.AspNetCore.TestSupport/ApiExplorer/ApiDescriptionFactory.cs b/test/Swashbuckle.AspNetCore.TestSupport/ApiExplorer/ApiDescriptionFactory.cs index 8538b08c90..91c198814e 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/ApiExplorer/ApiDescriptionFactory.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/ApiExplorer/ApiDescriptionFactory.cs @@ -46,7 +46,7 @@ public static ApiDescription Create( ControllerParameterDescriptor; #endif - if (parameterDescriptorWithParameterInfo != null) + if (parameterDescriptorWithParameterInfo != null && parameter.ModelMetadata == null) { parameter.ModelMetadata = ModelMetadataFactory.CreateForParameter(parameterDescriptorWithParameterInfo.ParameterInfo); }