Skip to content

Commit

Permalink
Fix default null value on nullable types (domaindrivendev#2941)
Browse files Browse the repository at this point in the history
Co-authored-by: Javier García de la Noceda Argüelles <[email protected]>
  • Loading branch information
jgarciadelanoceda and Javier García de la Noceda Argüelles authored Jun 8, 2024
1 parent 824253d commit 39f8fc6
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -134,7 +135,7 @@ public DataContract GetDataContractForType(Type type)
}

return DataContract.ForDynamic(
underlyingType: type,
underlyingType: effectiveType,
jsonConverter: JsonConverterFunc);
}

Expand Down Expand Up @@ -199,26 +200,26 @@ private List<DataProperty> GetDataPropertiesFor(JsonObjectContract jsonObjectCon

private static readonly Dictionary<Type, Tuple<DataType, string>> 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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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<string> keys = null;

Expand All @@ -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)
Expand Down Expand Up @@ -244,30 +245,30 @@ private List<DataProperty> GetDataPropertiesFor(Type objectType, out Type extens

private static readonly Dictionary<Type, Tuple<DataType, string>> 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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DataContract> knownTypesDataContracts)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
}

0 comments on commit 39f8fc6

Please sign in to comment.