diff --git a/src/DotSwashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/ISerializerDataContractResolver.cs b/src/DotSwashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/ISerializerDataContractResolver.cs index 45d6b52275..250fc1300f 100644 --- a/src/DotSwashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/ISerializerDataContractResolver.cs +++ b/src/DotSwashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/ISerializerDataContractResolver.cs @@ -150,8 +150,8 @@ public DataProperty( } public string Name { get; } - public bool IsRequired { get; } - public bool IsNullable { get; } + public bool IsRequired { get; internal set; } + public bool IsNullable { get; internal set; } public bool IsReadOnly { get; } public bool IsWriteOnly { get; } public Type MemberType { get; } diff --git a/src/DotSwashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs b/src/DotSwashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs index 2301fe5376..05947caf56 100644 --- a/src/DotSwashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs +++ b/src/DotSwashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs @@ -70,9 +70,22 @@ private OpenApiSchema GenerateSchemaForMember( { var requiredAttribute = customAttributes.OfType().FirstOrDefault(); var hasRequiredMemberAttribute = customAttributes.OfType().Any(); - schema.Nullable = _generatorOptions.SupportNonNullableReferenceTypes - ? dataProperty.IsNullable && requiredAttribute == null && !hasRequiredMemberAttribute && !memberInfo.IsNonNullableReferenceType() - : dataProperty.IsNullable && requiredAttribute == null && !hasRequiredMemberAttribute; + + if (_generatorOptions.SupportNonNullableReferenceTypes) + { + schema.Nullable = dataProperty.IsNullable && requiredAttribute == null && + !hasRequiredMemberAttribute && !memberInfo.IsNonNullableReferenceType(); + if (!schema.Nullable) + { + dataProperty.IsNullable = false; + dataProperty.IsRequired = true; + } + } + else + { + schema.Nullable = dataProperty.IsNullable && requiredAttribute == null && + !hasRequiredMemberAttribute; + } schema.ReadOnly = dataProperty.IsReadOnly; schema.WriteOnly = dataProperty.IsWriteOnly; diff --git a/test/DotSwashbuckle.AspNetCore.SwaggerGen.Test/DotSwashbuckle.AspNetCore.SwaggerGen.Test.csproj b/test/DotSwashbuckle.AspNetCore.SwaggerGen.Test/DotSwashbuckle.AspNetCore.SwaggerGen.Test.csproj index a6a31c7b26..f087e96628 100644 --- a/test/DotSwashbuckle.AspNetCore.SwaggerGen.Test/DotSwashbuckle.AspNetCore.SwaggerGen.Test.csproj +++ b/test/DotSwashbuckle.AspNetCore.SwaggerGen.Test/DotSwashbuckle.AspNetCore.SwaggerGen.Test.csproj @@ -10,6 +10,7 @@ true true true + enable diff --git a/test/DotSwashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/RecordsWithNullableAndRequiredParams.cs b/test/DotSwashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/RecordsWithNullableAndRequiredParams.cs new file mode 100644 index 0000000000..bbcd1428c8 --- /dev/null +++ b/test/DotSwashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/RecordsWithNullableAndRequiredParams.cs @@ -0,0 +1,67 @@ +namespace DotSwashbuckle.AspNetCore.SwaggerGen.Test.Fixtures +{ + public sealed class TestClass + { + public TestClass(string requiredString, string optionalString) + { + RequiredString = requiredString; + OptionalString = optionalString; + } + + public string RequiredString { get; } + public string? OptionalString { get; } + } + + public sealed record TestRecordPrimary(string RequiredString, string? OptionalString); + public sealed record TestRecordPrimaryInverse(string? OptionalString, string RequiredString); + + public sealed record TestValueRecordPrimary(int RequiredString, int? OptionalString); + public sealed record TestValueRecordPrimaryInverse(int? OptionalString, int RequiredString); + + public sealed record TestRecord + { + public TestRecord(string requiredString, string? optionalString) + { + RequiredString = requiredString; + OptionalString = optionalString; + } + + public string RequiredString { get; } + + public string? OptionalString { get; } + } + + public sealed record TestRecordInitOptional + { + public TestRecordInitOptional(string? optionalString) + { + OptionalString = optionalString; + } + + public string RequiredString { get; set; } = string.Empty; + + public string? OptionalString { get; } + } + + public sealed record TestRecordInitRequired + { + public TestRecordInitRequired(string requiredString) + { + RequiredString = requiredString; + } + + public string RequiredString { get; } + + public string? OptionalString { get; set; } + } + + public sealed record TestRecordMixed(string RequiredString) + { + public string? OptionalString { get; } + } + + public sealed record TestRecordMixedSwapped(string? OptionalString) + { + public string RequiredString { get; set; } = string.Empty; + } +} diff --git a/test/DotSwashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs b/test/DotSwashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs index b31fff4a74..c622453717 100644 --- a/test/DotSwashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs +++ b/test/DotSwashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs @@ -6,6 +6,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Net; +using DotSwashbuckle.AspNetCore.SwaggerGen.Test.Fixtures; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; @@ -81,6 +82,47 @@ public void GenerateSchema_GeneratesPrimitiveSchema_IfPrimitiveOrNullablePrimiti Assert.Equal(expectedFormat, schema.Format); } + [Theory] + [InlineData(typeof(TestRecordMixedSwapped), nameof(TestRecordMixedSwapped.RequiredString), false, true)] + [InlineData(typeof(TestRecordMixedSwapped), nameof(TestRecordMixedSwapped.OptionalString), true, false)] + [InlineData(typeof(TestClass), nameof(TestClass.RequiredString), false, true)] + [InlineData(typeof(TestClass), nameof(TestClass.OptionalString), true, false)] + [InlineData(typeof(TestRecord), nameof(TestRecord.RequiredString), false, true)] + [InlineData(typeof(TestRecord), nameof(TestRecord.OptionalString), true, false)] + [InlineData(typeof(TestRecordMixed), nameof(TestRecordMixed.RequiredString), false, true)] + [InlineData(typeof(TestRecordMixed), nameof(TestRecordMixed.OptionalString), true, false)] + [InlineData(typeof(TestRecordInitOptional), nameof(TestRecordInitOptional.RequiredString), false, true)] + [InlineData(typeof(TestRecordInitOptional), nameof(TestRecordInitOptional.OptionalString), true, false)] + [InlineData(typeof(TestRecordInitRequired), nameof(TestRecordInitRequired.RequiredString), false, true)] + [InlineData(typeof(TestRecordInitRequired), nameof(TestRecordInitRequired.OptionalString), true, false)] + [InlineData(typeof(TestRecordPrimary), nameof(TestRecordPrimary.RequiredString), false, true)] + [InlineData(typeof(TestRecordPrimary), nameof(TestRecordPrimary.OptionalString), true, false)] + [InlineData(typeof(TestRecordPrimaryInverse), nameof(TestRecordPrimaryInverse.RequiredString), false, true)] + [InlineData(typeof(TestRecordPrimaryInverse), nameof(TestRecordPrimaryInverse.OptionalString), true, false)] + [InlineData(typeof(TestValueRecordPrimary), nameof(TestValueRecordPrimary.RequiredString), false, true)] + [InlineData(typeof(TestValueRecordPrimary), nameof(TestValueRecordPrimary.OptionalString), true, false)] + [InlineData(typeof(TestValueRecordPrimaryInverse), nameof(TestValueRecordPrimaryInverse.RequiredString), false, true)] + [InlineData(typeof(TestValueRecordPrimaryInverse), nameof(TestValueRecordPrimaryInverse.OptionalString), true, false)] + public void TestNullable_And_Required_When_SupportNonNullableReferenceTypes_Enabled( + Type type, + string propertyName, + bool expectedNullable, + bool expectedRequired + ) + { + var schemaRepository = new SchemaRepository(); + + var referenceSchema = Subject( + configureGenerator: c => c.SupportNonNullableReferenceTypes = true + ).GenerateSchema(type, schemaRepository); + + Assert.NotNull(referenceSchema.Reference); + var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; + schema.Properties.TryGetValue(propertyName, out var propertySchema); + Assert.Equal(expectedNullable, propertySchema.Nullable); + Assert.Equal(expectedRequired, schema.Required.Contains(propertyName)); + } + [Theory] [InlineData(typeof(IntEnum), "integer", "int32", "2", "4", "8")] [InlineData(typeof(LongEnum), "integer", "int64", "2", "4", "8")]