From 2410717a54881751ae9804d3db20acab01afdc30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Fri, 7 Jun 2024 23:30:08 +0200 Subject: [PATCH] default null value on nullable types caused errors --- .../NewtonsoftDataContractResolver.cs | 49 +++++------ .../JsonSerializerDataContractResolver.cs | 81 ++++++++++--------- .../SchemaGenerator/SchemaGenerator.cs | 3 +- .../NewtonsoftSchemaGeneratorTests.cs | 6 +- .../JsonSerializerSchemaGeneratorTests.cs | 4 +- .../Fixtures/TypeWithDefaultAttributes.cs | 6 ++ 6 files changed, 80 insertions(+), 69 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs b/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs index 81875cc98c..08653d3028 100644 --- a/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs +++ b/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs @@ -22,14 +22,15 @@ public NewtonsoftDataContractResolver(JsonSerializerSettings serializerSettings) public DataContract GetDataContractForType(Type type) { - if (type.IsOneOf(typeof(object), typeof(JToken), typeof(JObject), typeof(JArray))) + var effectiveType = Nullable.GetUnderlyingType(type) ?? type; + if (effectiveType.IsOneOf(typeof(object), typeof(JToken), typeof(JObject), typeof(JArray))) { return DataContract.ForDynamic( - underlyingType: type, + underlyingType: effectiveType, jsonConverter: JsonConverterFunc); } - var jsonContract = _contractResolver.ResolveContract(type); + var jsonContract = _contractResolver.ResolveContract(effectiveType); if (jsonContract is JsonPrimitiveContract && !jsonContract.UnderlyingType.IsEnum) { @@ -134,7 +135,7 @@ public DataContract GetDataContractForType(Type type) } return DataContract.ForDynamic( - underlyingType: type, + underlyingType: effectiveType, jsonConverter: JsonConverterFunc); } @@ -199,26 +200,26 @@ private List GetDataPropertiesFor(JsonObjectContract jsonObjectCon private static readonly Dictionary> PrimitiveTypesAndFormats = new() { - [ typeof(bool) ] = Tuple.Create(DataType.Boolean, (string)null), - [ typeof(byte) ] = Tuple.Create(DataType.Integer, "int32"), - [ typeof(sbyte) ] = Tuple.Create(DataType.Integer, "int32"), - [ typeof(short) ] = Tuple.Create(DataType.Integer, "int32"), - [ typeof(ushort) ] = Tuple.Create(DataType.Integer, "int32"), - [ typeof(int) ] = Tuple.Create(DataType.Integer, "int32"), - [ typeof(uint) ] = Tuple.Create(DataType.Integer, "int32"), - [ typeof(long) ] = Tuple.Create(DataType.Integer, "int64"), - [ typeof(ulong) ] = Tuple.Create(DataType.Integer, "int64"), - [ typeof(float) ] = Tuple.Create(DataType.Number, "float"), - [ typeof(double) ] = Tuple.Create(DataType.Number, "double"), - [ typeof(decimal) ] = Tuple.Create(DataType.Number, "double"), - [ typeof(byte[]) ] = Tuple.Create(DataType.String, "byte"), - [ typeof(string) ] = Tuple.Create(DataType.String, (string)null), - [ typeof(char) ] = Tuple.Create(DataType.String, (string)null), - [ typeof(DateTime) ] = Tuple.Create(DataType.String, "date-time"), - [ typeof(DateTimeOffset) ] = Tuple.Create(DataType.String, "date-time"), - [ typeof(Guid) ] = Tuple.Create(DataType.String, "uuid"), - [ typeof(Uri) ] = Tuple.Create(DataType.String, "uri"), - [ typeof(TimeSpan) ] = Tuple.Create(DataType.String, "date-span"), + [typeof(bool)] = Tuple.Create(DataType.Boolean, (string)null), + [typeof(byte)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(sbyte)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(short)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(ushort)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(int)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(uint)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(long)] = Tuple.Create(DataType.Integer, "int64"), + [typeof(ulong)] = Tuple.Create(DataType.Integer, "int64"), + [typeof(float)] = Tuple.Create(DataType.Number, "float"), + [typeof(double)] = Tuple.Create(DataType.Number, "double"), + [typeof(decimal)] = Tuple.Create(DataType.Number, "double"), + [typeof(byte[])] = Tuple.Create(DataType.String, "byte"), + [typeof(string)] = Tuple.Create(DataType.String, (string)null), + [typeof(char)] = Tuple.Create(DataType.String, (string)null), + [typeof(DateTime)] = Tuple.Create(DataType.String, "date-time"), + [typeof(DateTimeOffset)] = Tuple.Create(DataType.String, "date-time"), + [typeof(Guid)] = Tuple.Create(DataType.String, "uuid"), + [typeof(Uri)] = Tuple.Create(DataType.String, "uri"), + [typeof(TimeSpan)] = Tuple.Create(DataType.String, "date-span"), #if NET6_0_OR_GREATER [ typeof(DateOnly) ] = Tuple.Create(DataType.String, "date"), [ typeof(TimeOnly) ] = Tuple.Create(DataType.String, "time"), diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs index 9b0b7fdc8b..fde8deaad9 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs @@ -20,25 +20,26 @@ public JsonSerializerDataContractResolver(JsonSerializerOptions serializerOption public DataContract GetDataContractForType(Type type) { - if (type.IsOneOf(typeof(object), typeof(JsonDocument), typeof(JsonElement))) + var effectiveType = Nullable.GetUnderlyingType(type) ?? type; + if (effectiveType.IsOneOf(typeof(object), typeof(JsonDocument), typeof(JsonElement))) { return DataContract.ForDynamic( - underlyingType: type, - jsonConverter: (value) => JsonConverterFunc(value, type)); + underlyingType: effectiveType, + jsonConverter: (value) => JsonConverterFunc(value, effectiveType)); } - if (PrimitiveTypesAndFormats.TryGetValue(type, out var primitiveTypeAndFormat)) + if (PrimitiveTypesAndFormats.TryGetValue(effectiveType, out var primitiveTypeAndFormat)) { return DataContract.ForPrimitive( - underlyingType: type, + underlyingType: effectiveType, dataType: primitiveTypeAndFormat.Item1, dataFormat: primitiveTypeAndFormat.Item2, jsonConverter: (value) => JsonConverterFunc(value, type)); } - if (type.IsEnum) + if (effectiveType.IsEnum) { - var enumValues = type.GetEnumValues(); + var enumValues = effectiveType.GetEnumValues(); // Test to determine if the serializer will treat as string var serializeAsString = @@ -51,18 +52,18 @@ public DataContract GetDataContractForType(Type type) var exampleType = serializeAsString ? typeof(string) : - type.GetEnumUnderlyingType(); + effectiveType.GetEnumUnderlyingType(); primitiveTypeAndFormat = PrimitiveTypesAndFormats[exampleType]; return DataContract.ForPrimitive( - underlyingType: type, + underlyingType: effectiveType, dataType: primitiveTypeAndFormat.Item1, dataFormat: primitiveTypeAndFormat.Item2, jsonConverter: (value) => JsonConverterFunc(value, type)); } - if (IsSupportedDictionary(type, out Type keyType, out Type valueType)) + if (IsSupportedDictionary(effectiveType, out Type keyType, out Type valueType)) { IEnumerable keys = null; @@ -79,25 +80,25 @@ public DataContract GetDataContractForType(Type type) } return DataContract.ForDictionary( - underlyingType: type, + underlyingType: effectiveType, valueType: valueType, keys: keys, - jsonConverter: (value) => JsonConverterFunc(value, type)); + jsonConverter: (value) => JsonConverterFunc(value, effectiveType)); } - if (IsSupportedCollection(type, out Type itemType)) + if (IsSupportedCollection(effectiveType, out Type itemType)) { return DataContract.ForArray( - underlyingType: type, + underlyingType: effectiveType, itemType: itemType, - jsonConverter: (value) => JsonConverterFunc(value, type)); + jsonConverter: (value) => JsonConverterFunc(value, effectiveType)); } return DataContract.ForObject( - underlyingType: type, + underlyingType: effectiveType, properties: GetDataPropertiesFor(type, out Type extensionDataType), extensionDataType: extensionDataType, - jsonConverter: (value) => JsonConverterFunc(value, type)); + jsonConverter: (value) => JsonConverterFunc(value, effectiveType)); } private string JsonConverterFunc(object value, Type type) @@ -244,30 +245,30 @@ private List GetDataPropertiesFor(Type objectType, out Type extens private static readonly Dictionary> PrimitiveTypesAndFormats = new() { - [ typeof(bool) ] = Tuple.Create(DataType.Boolean, (string)null), - [ typeof(byte) ] = Tuple.Create(DataType.Integer, "int32"), - [ typeof(sbyte) ] = Tuple.Create(DataType.Integer, "int32"), - [ typeof(short) ] = Tuple.Create(DataType.Integer, "int32"), - [ typeof(ushort) ] = Tuple.Create(DataType.Integer, "int32"), - [ typeof(int) ] = Tuple.Create(DataType.Integer, "int32"), - [ typeof(uint) ] = Tuple.Create(DataType.Integer, "int32"), - [ typeof(long) ] = Tuple.Create(DataType.Integer, "int64"), - [ typeof(ulong) ] = Tuple.Create(DataType.Integer, "int64"), - [ typeof(float) ] = Tuple.Create(DataType.Number, "float"), - [ typeof(double) ] = Tuple.Create(DataType.Number, "double"), - [ typeof(decimal) ] = Tuple.Create(DataType.Number, "double"), - [ typeof(byte[]) ] = Tuple.Create(DataType.String, "byte"), - [ typeof(string) ] = Tuple.Create(DataType.String, (string)null), - [ typeof(char) ] = Tuple.Create(DataType.String, (string)null), - [ typeof(DateTime) ] = Tuple.Create(DataType.String, "date-time"), - [ typeof(DateTimeOffset) ] = Tuple.Create(DataType.String, "date-time"), - [ typeof(TimeSpan) ] = Tuple.Create(DataType.String, "date-span"), - [ typeof(Guid) ] = Tuple.Create(DataType.String, "uuid"), - [ typeof(Uri) ] = Tuple.Create(DataType.String, "uri"), - [ typeof(Version) ] = Tuple.Create(DataType.String, (string)null), + [typeof(bool)] = Tuple.Create(DataType.Boolean, (string)null), + [typeof(byte)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(sbyte)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(short)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(ushort)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(int)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(uint)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(long)] = Tuple.Create(DataType.Integer, "int64"), + [typeof(ulong)] = Tuple.Create(DataType.Integer, "int64"), + [typeof(float)] = Tuple.Create(DataType.Number, "float"), + [typeof(double)] = Tuple.Create(DataType.Number, "double"), + [typeof(decimal)] = Tuple.Create(DataType.Number, "double"), + [typeof(byte[])] = Tuple.Create(DataType.String, "byte"), + [typeof(string)] = Tuple.Create(DataType.String, (string)null), + [typeof(char)] = Tuple.Create(DataType.String, (string)null), + [typeof(DateTime)] = Tuple.Create(DataType.String, "date-time"), + [typeof(DateTimeOffset)] = Tuple.Create(DataType.String, "date-time"), + [typeof(TimeSpan)] = Tuple.Create(DataType.String, "date-span"), + [typeof(Guid)] = Tuple.Create(DataType.String, "uuid"), + [typeof(Uri)] = Tuple.Create(DataType.String, "uri"), + [typeof(Version)] = Tuple.Create(DataType.String, (string)null), #if NET6_0_OR_GREATER - [ typeof(DateOnly) ] = Tuple.Create(DataType.String, "date"), - [ typeof(TimeOnly) ] = Tuple.Create(DataType.String, "time"), + [typeof(DateOnly)] = Tuple.Create(DataType.String, "date"), + [typeof(TimeOnly)] = Tuple.Create(DataType.String, "time"), #endif #if NET7_0_OR_GREATER [ typeof(Int128) ] = Tuple.Create(DataType.Integer, "int128"), diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs index 55047933b9..497cffd275 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs @@ -166,8 +166,7 @@ private OpenApiSchema GenerateSchemaForType(Type modelType, SchemaRepository sch private DataContract GetDataContractFor(Type modelType) { - var effectiveType = Nullable.GetUnderlyingType(modelType) ?? modelType; - return _serializerDataContractResolver.GetDataContractForType(effectiveType); + return _serializerDataContractResolver.GetDataContractForType(modelType); } private bool IsBaseTypeWithKnownTypesDefined(DataContract dataContract, out IEnumerable knownTypesDataContracts) diff --git a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs index 518e5db3d2..11d284ab6d 100644 --- a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs @@ -12,9 +12,9 @@ using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; -using Xunit; using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.TestSupport; +using Xunit; namespace Swashbuckle.AspNetCore.Newtonsoft.Test { @@ -273,6 +273,8 @@ public void GenerateSchema_SetsNullableFlag_IfPropertyIsReferenceOrNullableType( [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.StringWithDefault), "\"foobar\"")] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.IntArrayWithDefault), "[\n 1,\n 2,\n 3\n]")] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.StringArrayWithDefault), "[\n \"foo\",\n \"bar\"\n]")] + [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.NullableIntWithDefaultNullValue), "null")] + [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.NullableIntWithDefaultValue), "2147483647")] [UseInvariantCulture] public void GenerateSchema_SetsDefault_IfPropertyHasDefaultValueAttribute( Type declaringType, @@ -505,7 +507,7 @@ public void GenerateSchema_SupportsOption_UseAllOfForPolymorphism() Assert.NotNull(schema.OneOf[0].Reference); var baseSchema = schemaRepository.Schemas[schema.OneOf[0].Reference.Id]; Assert.Equal("object", baseSchema.Type); - Assert.Equal(new[] { "BaseProperty"}, baseSchema.Properties.Keys); + Assert.Equal(new[] { "BaseProperty" }, baseSchema.Properties.Keys); // The first sub type schema Assert.NotNull(schema.OneOf[1].Reference); var subType1Schema = schemaRepository.Schemas[schema.OneOf[1].Reference.Id]; diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs index d162753253..3771bfb46c 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs @@ -14,8 +14,8 @@ using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.TestSupport; using Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures; +using Swashbuckle.AspNetCore.TestSupport; using Xunit; namespace Swashbuckle.AspNetCore.SwaggerGen.Test @@ -296,6 +296,8 @@ public void GenerateSchema_SetsNullableFlag_IfPropertyIsReferenceOrNullableType( [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.StringWithDefault), "\"foobar\"")] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.IntArrayWithDefault), "[\n 1,\n 2,\n 3\n]")] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.StringArrayWithDefault), "[\n \"foo\",\n \"bar\"\n]")] + [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.NullableIntWithDefaultNullValue), "null")] + [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.NullableIntWithDefaultValue), "2147483647")] [UseInvariantCulture] public void GenerateSchema_SetsDefault_IfPropertyHasDefaultValueAttribute( Type declaringType, diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithDefaultAttributes.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithDefaultAttributes.cs index f1bdd58371..0b9d11d73c 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithDefaultAttributes.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithDefaultAttributes.cs @@ -30,5 +30,11 @@ public class TypeWithDefaultAttributes [DefaultValue(new[] { "foo", "bar" })] public string[] StringArrayWithDefault { get; set; } + + [DefaultValue(null)] + public int? NullableIntWithDefaultNullValue { get; set; } + + [DefaultValue(int.MaxValue)] + public int? NullableIntWithDefaultValue { get; set; } } }