diff --git a/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs b/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs index 88e62d5913..ec3ff32c07 100644 --- a/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs +++ b/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs @@ -22,11 +22,13 @@ public DataContract GetDataContractForType(Type type) jsonConverter: JsonConverterFunc); } - var jsonContract = _contractResolver.ResolveContract(effectiveType); + var jsonContract = _contractResolver.ResolveContract(type); - if (jsonContract is JsonPrimitiveContract && !jsonContract.UnderlyingType.IsEnum) + var effectiveUnderlyingType = Nullable.GetUnderlyingType(jsonContract.UnderlyingType) ?? jsonContract.UnderlyingType; + + if (jsonContract is JsonPrimitiveContract && !effectiveUnderlyingType.IsEnum) { - if (!PrimitiveTypesAndFormats.TryGetValue(jsonContract.UnderlyingType, out var primitiveTypeAndFormat)) + if (!PrimitiveTypesAndFormats.TryGetValue(effectiveUnderlyingType, out var primitiveTypeAndFormat)) { primitiveTypeAndFormat = Tuple.Create(DataType.String, (string)null); } @@ -38,9 +40,9 @@ public DataContract GetDataContractForType(Type type) jsonConverter: JsonConverterFunc); } - if (jsonContract is JsonPrimitiveContract && jsonContract.UnderlyingType.IsEnum) + if (jsonContract is JsonPrimitiveContract && effectiveUnderlyingType.IsEnum) { - var enumValues = jsonContract.UnderlyingType.GetEnumValues(); + var enumValues = effectiveUnderlyingType.GetEnumValues(); // Test to determine if the serializer will treat as string var serializeAsString = (enumValues.Length > 0) && @@ -52,7 +54,7 @@ public DataContract GetDataContractForType(Type type) var primitiveTypeAndFormat = serializeAsString ? PrimitiveTypesAndFormats[typeof(string)] - : PrimitiveTypesAndFormats[jsonContract.UnderlyingType.GetEnumUnderlyingType()]; + : PrimitiveTypesAndFormats[effectiveUnderlyingType.GetEnumUnderlyingType()]; return DataContract.ForPrimitive( underlyingType: jsonContract.UnderlyingType, diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs index 2cfb2140c0..549c7c5f50 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs @@ -49,7 +49,7 @@ public DataContract GetDataContractForType(Type type) primitiveTypeAndFormat = PrimitiveTypesAndFormats[exampleType]; return DataContract.ForPrimitive( - underlyingType: effectiveType, + underlyingType: type, dataType: primitiveTypeAndFormat.Item1, dataFormat: primitiveTypeAndFormat.Item2, jsonConverter: (value) => JsonConverterFunc(value, type)); diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs index 1135f91e4e..ed013d2d4a 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs @@ -54,6 +54,18 @@ private OpenApiSchema GenerateSchemaForMember( MemberInfo memberInfo, DataProperty dataProperty = null) { + if (dataProperty != null) + { + var customAttributes = memberInfo.GetInlineAndMetadataAttributes(); + + var requiredAttribute = customAttributes.OfType().FirstOrDefault(); + + if (!IsNullable(customAttributes, requiredAttribute, dataProperty, memberInfo)) + { + modelType = Nullable.GetUnderlyingType(modelType) ?? modelType; + } + } + var dataContract = GetDataContractFor(modelType); var schema = _generatorOptions.UseOneOfForPolymorphism && IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts) @@ -75,11 +87,7 @@ private OpenApiSchema GenerateSchemaForMember( { var requiredAttribute = customAttributes.OfType().FirstOrDefault(); - var nullable = _generatorOptions.SupportNonNullableReferenceTypes - ? dataProperty.IsNullable && requiredAttribute == null && !memberInfo.IsNonNullableReferenceType() - : dataProperty.IsNullable && requiredAttribute == null; - - schema.Nullable = nullable; + schema.Nullable = IsNullable(customAttributes, requiredAttribute, dataProperty, memberInfo); schema.ReadOnly = dataProperty.IsReadOnly; schema.WriteOnly = dataProperty.IsWriteOnly; @@ -129,6 +137,13 @@ private OpenApiSchema GenerateSchemaForMember( return schema; } + private bool IsNullable(IEnumerable customAttributes, RequiredAttribute requiredAttribute, DataProperty dataProperty, MemberInfo memberInfo) + { + return _generatorOptions.SupportNonNullableReferenceTypes + ? dataProperty.IsNullable && requiredAttribute == null && !memberInfo.IsNonNullableReferenceType() + : dataProperty.IsNullable && requiredAttribute == null; + } + private OpenApiSchema GenerateSchemaForParameter( Type modelType, SchemaRepository schemaRepository, @@ -264,7 +279,7 @@ private OpenApiSchema GenerateConcreteSchema(DataContract dataContract, SchemaRe case DataType.String: { schemaFactory = () => CreatePrimitiveSchema(dataContract); - returnAsReference = dataContract.UnderlyingType.IsEnum && !_generatorOptions.UseInlineDefinitionsForEnums; + returnAsReference = (Nullable.GetUnderlyingType(dataContract.UnderlyingType) ?? dataContract.UnderlyingType).IsEnum && !_generatorOptions.UseInlineDefinitionsForEnums; break; } @@ -330,10 +345,19 @@ private static OpenApiSchema CreatePrimitiveSchema(DataContract dataContract) } #pragma warning restore CS0618 // Type or member is obsolete - if (dataContract.UnderlyingType.IsEnum) + var underlyingType = Nullable.GetUnderlyingType(dataContract.UnderlyingType) ?? dataContract.UnderlyingType; + + if (underlyingType.IsEnum) { - schema.Enum = [.. dataContract.UnderlyingType.GetEnumValues() - .Cast() + var enumValues = underlyingType.GetEnumValues().Cast(); + + if (dataContract.UnderlyingType != underlyingType) + { + schema.Nullable = true; + enumValues = enumValues.Append(null); + } + + schema.Enum = [.. enumValues .Select(value => dataContract.JsonConverter(value)) .Distinct() .Select(JsonModelFactory.CreateFromJson)]; @@ -440,9 +464,11 @@ private OpenApiSchema CreateObjectSchema(DataContract dataContract, SchemaReposi continue; } + var memberType = dataProperty.IsNullable ? dataProperty.MemberType : (Nullable.GetUnderlyingType(dataProperty.MemberType) ?? dataProperty.MemberType); + schema.Properties[dataProperty.Name] = (dataProperty.MemberInfo != null) - ? GenerateSchemaForMember(dataProperty.MemberType, schemaRepository, dataProperty.MemberInfo, dataProperty) - : GenerateSchemaForType(dataProperty.MemberType, schemaRepository); + ? GenerateSchemaForMember(memberType, schemaRepository, dataProperty.MemberInfo, dataProperty) + : GenerateSchemaForType(memberType, schemaRepository); var markNonNullableTypeAsRequired = _generatorOptions.NonNullableReferenceTypesAsRequired && diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index 7633673b48..033c572f73 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -598,7 +598,7 @@ apiParameter.Type is not null && var schema = (type != null) ? GenerateSchema( - type, + isRequired ? (Nullable.GetUnderlyingType(type) ?? type) : type, schemaRepository, apiParameter.PropertyInfo(), apiParameter.ParameterInfo(), diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.DotNet8_0.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.DotNet8_0.verified.txt index bceac63b86..2bc25cf159 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.DotNet8_0.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.DotNet8_0.verified.txt @@ -1491,7 +1491,7 @@ "$ref": "#/components/schemas/ProductStatus" }, "status2": { - "$ref": "#/components/schemas/ProductStatus" + "$ref": "#/components/schemas/ProductStatusNullable" } }, "additionalProperties": false, @@ -1511,6 +1511,17 @@ "type": "integer", "format": "int32" }, + "ProductStatusNullable": { + "enum": [ + 0, + 1, + 2, + null + ], + "type": "integer", + "format": "int32", + "nullable": true + }, "Promotion": { "type": "object", "properties": { diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.DotNet9_0.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.DotNet9_0.verified.txt index bceac63b86..2bc25cf159 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.DotNet9_0.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.DotNet9_0.verified.txt @@ -1491,7 +1491,7 @@ "$ref": "#/components/schemas/ProductStatus" }, "status2": { - "$ref": "#/components/schemas/ProductStatus" + "$ref": "#/components/schemas/ProductStatusNullable" } }, "additionalProperties": false, @@ -1511,6 +1511,17 @@ "type": "integer", "format": "int32" }, + "ProductStatusNullable": { + "enum": [ + 0, + 1, + 2, + null + ], + "type": "integer", + "format": "int32", + "nullable": true + }, "Promotion": { "type": "object", "properties": { diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=MvcWithNullable.Program_swaggerRequestUri=v1.DotNet8_0.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=MvcWithNullable.Program_swaggerRequestUri=v1.DotNet8_0.verified.txt index e3f4dcbc21..2ae438746b 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=MvcWithNullable.Program_swaggerRequestUri=v1.DotNet8_0.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=MvcWithNullable.Program_swaggerRequestUri=v1.DotNet8_0.verified.txt @@ -14,6 +14,33 @@ { "name": "logLevel", "in": "query", + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/LogLevelNullable" + } + ], + "default": 4 + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/RequiredEnum": { + "get": { + "tags": [ + "RequiredEnum" + ], + "parameters": [ + { + "name": "logLevel", + "in": "query", + "required": true, "schema": { "allOf": [ { @@ -46,6 +73,21 @@ ], "type": "integer", "format": "int32" + }, + "LogLevelNullable": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + null + ], + "type": "integer", + "format": "int32", + "nullable": true } } } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=MvcWithNullable.Program_swaggerRequestUri=v1.DotNet9_0.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=MvcWithNullable.Program_swaggerRequestUri=v1.DotNet9_0.verified.txt index e3f4dcbc21..2ae438746b 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=MvcWithNullable.Program_swaggerRequestUri=v1.DotNet9_0.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=MvcWithNullable.Program_swaggerRequestUri=v1.DotNet9_0.verified.txt @@ -14,6 +14,33 @@ { "name": "logLevel", "in": "query", + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/LogLevelNullable" + } + ], + "default": 4 + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/RequiredEnum": { + "get": { + "tags": [ + "RequiredEnum" + ], + "parameters": [ + { + "name": "logLevel", + "in": "query", + "required": true, "schema": { "allOf": [ { @@ -46,6 +73,21 @@ ], "type": "integer", "format": "int32" + }, + "LogLevelNullable": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + null + ], + "type": "integer", + "format": "int32", + "nullable": true } } } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=WebApi.Program_swaggerRequestUri=v1.DotNet8_0.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=WebApi.Program_swaggerRequestUri=v1.DotNet8_0.verified.txt index d437eea1cf..4f186dc60d 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=WebApi.Program_swaggerRequestUri=v1.DotNet8_0.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=WebApi.Program_swaggerRequestUri=v1.DotNet8_0.verified.txt @@ -379,7 +379,7 @@ "name": "paramNine", "in": "query", "schema": { - "$ref": "#/components/schemas/DateTimeKind" + "$ref": "#/components/schemas/DateTimeKindNullable" } }, { @@ -946,7 +946,7 @@ "format": "time" }, "paramNine": { - "$ref": "#/components/schemas/DateTimeKind" + "$ref": "#/components/schemas/DateTimeKindNullable" }, "paramTen": { "$ref": "#/components/schemas/DateTimeKind" @@ -995,6 +995,17 @@ "type": "integer", "format": "int32" }, + "DateTimeKindNullable": { + "enum": [ + 0, + 1, + 2, + null + ], + "type": "integer", + "format": "int32", + "nullable": true + }, "Fruit": { "type": "object", "properties": { diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=WebApi.Program_swaggerRequestUri=v1.DotNet9_0.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=WebApi.Program_swaggerRequestUri=v1.DotNet9_0.verified.txt index d437eea1cf..4f186dc60d 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=WebApi.Program_swaggerRequestUri=v1.DotNet9_0.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.Swagger_IsValidJson_No_Startup_entryPointType=WebApi.Program_swaggerRequestUri=v1.DotNet9_0.verified.txt @@ -379,7 +379,7 @@ "name": "paramNine", "in": "query", "schema": { - "$ref": "#/components/schemas/DateTimeKind" + "$ref": "#/components/schemas/DateTimeKindNullable" } }, { @@ -946,7 +946,7 @@ "format": "time" }, "paramNine": { - "$ref": "#/components/schemas/DateTimeKind" + "$ref": "#/components/schemas/DateTimeKindNullable" }, "paramTen": { "$ref": "#/components/schemas/DateTimeKind" @@ -995,6 +995,17 @@ "type": "integer", "format": "int32" }, + "DateTimeKindNullable": { + "enum": [ + 0, + 1, + 2, + null + ], + "type": "integer", + "format": "int32", + "nullable": true + }, "Fruit": { "type": "object", "properties": { diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.TypesAreRenderedCorrectly.DotNet8_0.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.TypesAreRenderedCorrectly.DotNet8_0.verified.txt index d437eea1cf..4f186dc60d 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.TypesAreRenderedCorrectly.DotNet8_0.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.TypesAreRenderedCorrectly.DotNet8_0.verified.txt @@ -379,7 +379,7 @@ "name": "paramNine", "in": "query", "schema": { - "$ref": "#/components/schemas/DateTimeKind" + "$ref": "#/components/schemas/DateTimeKindNullable" } }, { @@ -946,7 +946,7 @@ "format": "time" }, "paramNine": { - "$ref": "#/components/schemas/DateTimeKind" + "$ref": "#/components/schemas/DateTimeKindNullable" }, "paramTen": { "$ref": "#/components/schemas/DateTimeKind" @@ -995,6 +995,17 @@ "type": "integer", "format": "int32" }, + "DateTimeKindNullable": { + "enum": [ + 0, + 1, + 2, + null + ], + "type": "integer", + "format": "int32", + "nullable": true + }, "Fruit": { "type": "object", "properties": { diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.TypesAreRenderedCorrectly.DotNet9_0.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.TypesAreRenderedCorrectly.DotNet9_0.verified.txt index d437eea1cf..4f186dc60d 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.TypesAreRenderedCorrectly.DotNet9_0.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/snapshots/VerifyTests.TypesAreRenderedCorrectly.DotNet9_0.verified.txt @@ -379,7 +379,7 @@ "name": "paramNine", "in": "query", "schema": { - "$ref": "#/components/schemas/DateTimeKind" + "$ref": "#/components/schemas/DateTimeKindNullable" } }, { @@ -946,7 +946,7 @@ "format": "time" }, "paramNine": { - "$ref": "#/components/schemas/DateTimeKind" + "$ref": "#/components/schemas/DateTimeKindNullable" }, "paramTen": { "$ref": "#/components/schemas/DateTimeKind" @@ -995,6 +995,17 @@ "type": "integer", "format": "int32" }, + "DateTimeKindNullable": { + "enum": [ + 0, + 1, + 2, + null + ], + "type": "integer", + "format": "int32", + "nullable": true + }, "Fruit": { "type": "object", "properties": { diff --git a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/Fixtures/JsonPropertyAnnotatedType.cs b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/Fixtures/JsonPropertyAnnotatedType.cs index 57d48e146b..57698af60c 100644 --- a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/Fixtures/JsonPropertyAnnotatedType.cs +++ b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/Fixtures/JsonPropertyAnnotatedType.cs @@ -1,5 +1,6 @@ using System.Runtime.Serialization; using Newtonsoft.Json; +using Swashbuckle.AspNetCore.TestSupport; namespace Swashbuckle.AspNetCore.Newtonsoft.Test; @@ -31,4 +32,16 @@ public class JsonPropertyAnnotatedType [DataMember(IsRequired = true)] //As the support for DataMember has been implemented later, JsonProperty.Required should take precedence [JsonProperty(Required = Required.Default)] public string StringWithRequiredDefaultButConflictingDataMember { get; set; } + + [JsonProperty(Required = Required.Default)] + public IntEnum? IntEnumWithRequiredDefault { get; set; } + + [JsonProperty(Required = Required.DisallowNull)] + public IntEnum? IntEnumWithRequiredDisallowNull { get; set; } + + [JsonProperty(Required = Required.Always)] + public IntEnum? IntEnumWithRequiredAlways { get; set; } + + [JsonProperty(Required = Required.AllowNull)] + public IntEnum? IntEnumWithRequiredAllowNull { get; set; } } diff --git a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/Fixtures/JsonRequiredAnnotatedType.cs b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/Fixtures/JsonRequiredAnnotatedType.cs index 615630df04..d49f76e3b4 100644 --- a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/Fixtures/JsonRequiredAnnotatedType.cs +++ b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/Fixtures/JsonRequiredAnnotatedType.cs @@ -1,5 +1,6 @@ using System.Runtime.Serialization; using Newtonsoft.Json; +using Swashbuckle.AspNetCore.TestSupport; namespace Swashbuckle.AspNetCore.Newtonsoft.Test; @@ -12,4 +13,10 @@ public class JsonRequiredAnnotatedType [DataMember(IsRequired = false)] //As the support for DataMember has been implemented later, JsonRequired should take precedence [JsonRequired] public string StringWithConflictingRequired { get; set; } + + [JsonRequired] + public IntEnum IntEnumWithRequired { get; set; } + + [JsonRequired] + public IntEnum? NullableIntEnumWithRequired { get; set; } } diff --git a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs index 2d9cd1b1b4..15db76790b 100644 --- a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs @@ -88,8 +88,8 @@ public void GenerateSchema_GeneratesPrimitiveSchema_IfPrimitiveOrNullablePrimiti { { typeof(IntEnum), JsonSchemaTypes.Integer, "int32", 3 }, { typeof(LongEnum), JsonSchemaTypes.Integer, "int64", 3 }, - { typeof(IntEnum?), JsonSchemaTypes.Integer, "int32", 3 }, - { typeof(LongEnum?), JsonSchemaTypes.Integer, "int64", 3 }, + { typeof(IntEnum?), JsonSchemaTypes.Integer, "int32", 4 }, + { typeof(LongEnum?), JsonSchemaTypes.Integer, "int64", 4 }, }; [Theory] @@ -287,6 +287,30 @@ public void GenerateSchema_SetsNullableFlag_IfPropertyIsReferenceOrNullableType( Assert.Equal(expectedNullable, schema.Properties[propertyName].Nullable); } + [Fact] + public void GenerateSchema_DoesNotSetNullableFlag_IfReferencedEnum() + { + var schemaRepository = new SchemaRepository(); + + var referenceSchema = Subject().GenerateSchema(typeof(TypeWithNullableProperties), schemaRepository); + + var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; + const string propertyName = nameof(TypeWithNullableProperties.NullableIntEnumProperty); + Assert.False(schema.Properties[propertyName].Nullable); + Assert.Equal("IntEnumNullable", schema.Properties[propertyName].Reference.Id); + } + + [Fact] + public void GenerateSchema_SetNullableFlag_IfInlineEnum() + { + var schemaRepository = new SchemaRepository(); + + var referenceSchema = Subject(o => o.UseInlineDefinitionsForEnums = true).GenerateSchema(typeof(TypeWithNullableProperties), schemaRepository); + + var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; + Assert.True(schema.Properties[nameof(TypeWithNullableProperties.NullableIntEnumProperty)].Nullable); + } + [Theory] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.BoolWithDefault), "true")] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.IntWithDefault), "2147483647")] @@ -365,9 +389,11 @@ 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(["StringWithRequired", "StringWithRequiredAllowEmptyTrue"], schema.Required); + Assert.Equal(["NullableIntEnumWithRequired", "StringWithRequired", "StringWithRequiredAllowEmptyTrue"], schema.Required); Assert.Equal("Description", schema.Properties[nameof(TypeWithValidationAttributes.StringWithDescription)].Description); Assert.True(schema.Properties[nameof(TypeWithValidationAttributes.StringWithReadOnly)].ReadOnly); + Assert.False(schema.Properties[nameof(TypeWithValidationAttributes.NullableIntEnumWithRequired)].Nullable); + Assert.Equal(nameof(IntEnum), schema.Properties[nameof(TypeWithValidationAttributes.NullableIntEnumWithRequired)].Reference.Id); } [Fact] @@ -794,12 +820,18 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonProperty() "StringWithRequiredAlways", "StringWithRequiredAllowNull", "StringWithRequiredAlwaysButConflictingDataMember", - "StringWithRequiredDefaultButConflictingDataMember" + "StringWithRequiredDefaultButConflictingDataMember", + "IntEnumWithRequiredDefault", + "IntEnumWithRequiredDisallowNull", + "IntEnumWithRequiredAlways", + "IntEnumWithRequiredAllowNull" ], schema.Properties.Keys ); Assert.Equal( [ + "IntEnumWithRequiredAllowNull", + "IntEnumWithRequiredAlways", "StringWithRequiredAllowNull", "StringWithRequiredAlways", "StringWithRequiredAlwaysButConflictingDataMember" @@ -814,6 +846,14 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonProperty() Assert.True(schema.Properties["StringWithRequiredAllowNull"].Nullable); Assert.False(schema.Properties["StringWithRequiredAlwaysButConflictingDataMember"].Nullable); Assert.True(schema.Properties["StringWithRequiredDefaultButConflictingDataMember"].Nullable); + Assert.False(schema.Properties["IntEnumWithRequiredDefault"].Nullable); + Assert.False(schema.Properties["IntEnumWithRequiredAllowNull"].Nullable); + Assert.False(schema.Properties["IntEnumWithRequiredAlways"].Nullable); + Assert.False(schema.Properties["IntEnumWithRequiredDisallowNull"].Nullable); + Assert.Equal("IntEnumNullable", schema.Properties["IntEnumWithRequiredDefault"].Reference.Id); + Assert.Equal("IntEnumNullable", schema.Properties["IntEnumWithRequiredAllowNull"].Reference.Id); + Assert.Equal(nameof(IntEnum), schema.Properties["IntEnumWithRequiredAlways"].Reference.Id); + Assert.Equal(nameof(IntEnum), schema.Properties["IntEnumWithRequiredDisallowNull"].Reference.Id); } [Fact] @@ -824,8 +864,12 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonRequired() var referenceSchema = Subject().GenerateSchema(typeof(JsonRequiredAnnotatedType), schemaRepository); var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; - Assert.Equal(["StringWithConflictingRequired", "StringWithJsonRequired"], schema.Required); + Assert.Equal(["IntEnumWithRequired", "NullableIntEnumWithRequired", "StringWithConflictingRequired", "StringWithJsonRequired"], schema.Required); Assert.False(schema.Properties["StringWithJsonRequired"].Nullable); + Assert.False(schema.Properties["IntEnumWithRequired"].Nullable); + Assert.Equal(nameof(IntEnum), schema.Properties["IntEnumWithRequired"].Reference.Id); + Assert.True(schemaRepository.TryLookupByType(typeof(IntEnum), out _)); + Assert.False(schemaRepository.TryLookupByType(typeof(IntEnum?), out _)); } [Fact] diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs index c424c7d484..0ef8c2145d 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs @@ -89,13 +89,14 @@ public void GenerateSchema_GeneratesPrimitiveSchema_IfPrimitiveOrNullablePrimiti } [Theory] - [InlineData(typeof(IntEnum), "int32", "2", "4", "8")] - [InlineData(typeof(LongEnum), "int64", "2", "4", "8")] - [InlineData(typeof(IntEnum?), "int32", "2", "4", "8")] - [InlineData(typeof(LongEnum?), "int64", "2", "4", "8")] + [InlineData(typeof(IntEnum), "int32", false, "2", "4", "8")] + [InlineData(typeof(LongEnum), "int64", false, "2", "4", "8")] + [InlineData(typeof(IntEnum?), "int32", true, "2", "4", "8", "null")] + [InlineData(typeof(LongEnum?), "int64", true, "2", "4", "8", "null")] public void GenerateSchema_GeneratesReferencedEnumSchema_IfEnumOrNullableEnumType( Type type, string expectedFormat, + bool expectedNullable, params string[] expectedEnumAsJson) { var schemaRepository = new SchemaRepository(); @@ -106,6 +107,7 @@ public void GenerateSchema_GeneratesReferencedEnumSchema_IfEnumOrNullableEnumTyp var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; Assert.Equal(JsonSchemaTypes.Integer, schema.Type); Assert.Equal(expectedFormat, schema.Format); + Assert.Equal(expectedNullable, schema.Nullable); Assert.NotNull(schema.Enum); Assert.Equal(expectedEnumAsJson, schema.Enum.Select(openApiAny => openApiAny.ToJson())); } @@ -387,7 +389,7 @@ 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(["StringWithRequired", "StringWithRequiredAllowEmptyTrue"], schema.Required); + Assert.Equal(["NullableIntEnumWithRequired", "StringWithRequired", "StringWithRequiredAllowEmptyTrue"], schema.Required); Assert.Equal("Description", schema.Properties[nameof(TypeWithValidationAttributes.StringWithDescription)].Description); Assert.True(schema.Properties[nameof(TypeWithValidationAttributes.StringWithReadOnly)].ReadOnly); } diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithNullableProperties.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithNullableProperties.cs index 5f9f01c086..c5d07574d5 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithNullableProperties.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithNullableProperties.cs @@ -7,4 +7,6 @@ public class TypeWithNullableProperties public string StringProperty { get; set; } public int? NullableIntProperty { get; set; } + + public IntEnum? NullableIntEnumProperty { get; set; } } diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs index efc18a80a9..ffc7d799d8 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs @@ -50,4 +50,7 @@ public class TypeWithValidationAttributes [ReadOnly(true)] public string StringWithReadOnly { get; set; } + + [Required] + public IntEnum? NullableIntEnumWithRequired { get; set; } } diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs index 5795032aab..37f6057cb4 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs @@ -38,6 +38,8 @@ public class TypeWithValidationAttributesViaMetadataType public string StringWithDescription { get; set; } public string StringWithReadOnly { get; set; } + + public IntEnum? NullableIntEnumWithRequired { get; set; } } public class MetadataType @@ -87,4 +89,7 @@ public class MetadataType [ReadOnly(true)] public string StringWithReadOnly { get; set; } + + [Required] + public IntEnum? NullableIntEnumWithRequired { get; set; } } diff --git a/test/WebSites/MvcWithNullable/Program.cs b/test/WebSites/MvcWithNullable/Program.cs index eaf33739b4..fd0db6a237 100644 --- a/test/WebSites/MvcWithNullable/Program.cs +++ b/test/WebSites/MvcWithNullable/Program.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(args); @@ -31,6 +32,14 @@ public class EnumController : ControllerBase public IActionResult Get(LogLevel? logLevel = LogLevel.Error) => Ok(new { logLevel }); } +[ApiController] +[Route("api/[controller]")] +public class RequiredEnumController : ControllerBase +{ + [HttpGet] + public IActionResult Get([Required] LogLevel? logLevel = LogLevel.Error) => Ok(new { logLevel }); +} + namespace MvcWithNullable { ///