diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs index ffa7b6fe00d116..796bd86d16b8a7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs @@ -26,14 +26,17 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(valueTypeToConvert); } - JsonConverter converter = (JsonConverter)Activator.CreateInstance( + return CreateValueConverter(valueTypeToConvert, valueConverter); + } + + public static JsonConverter CreateValueConverter(Type valueTypeToConvert, JsonConverter valueConverter) + { + return (JsonConverter)Activator.CreateInstance( typeof(NullableConverter<>).MakeGenericType(valueTypeToConvert), BindingFlags.Instance | BindingFlags.Public, binder: null, args: new object[] { valueConverter }, culture: null)!; - - return converter; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs index c17282b56eabee..2de29b4f5358c4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs @@ -217,6 +217,13 @@ private JsonConverter GetConverterFromAttribute(JsonConverterAttribute converter Debug.Assert(converter != null); if (!converter.CanConvert(typeToConvert)) { + Type? underlyingType = Nullable.GetUnderlyingType(typeToConvert); + if (underlyingType != null && converter.CanConvert(underlyingType)) + { + // Allow nullable handling to forward to the underlying type's converter. + return NullableConverterFactory.CreateValueConverter(underlyingType, converter); + } + ThrowHelper.ThrowInvalidOperationException_SerializationConverterOnAttributeNotCompatible(classTypeAttributeIsOn, propertyInfo, typeToConvert); } diff --git a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.NullableTypes.cs b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.NullableTypes.cs new file mode 100644 index 00000000000000..ffc7c4569263d9 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.NullableTypes.cs @@ -0,0 +1,129 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static partial class CustomConverterTests + { + private class JsonTestStructConverter : JsonConverter + { + public override TestStruct Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new TestStruct + { + InnerValue = reader.GetInt32() + }; + } + + public override void Write(Utf8JsonWriter writer, TestStruct value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value.InnerValue); + } + } + + private class JsonTestStructThrowingConverter : JsonConverter + { + public override TestStruct Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotSupportedException(); + } + + public override void Write(Utf8JsonWriter writer, TestStruct value, JsonSerializerOptions options) + { + throw new NotSupportedException(); + } + } + + private struct TestStruct + { + public int InnerValue { get; set; } + } + + private class TestStructClass + { + [JsonConverter(typeof(JsonTestStructConverter))] + public TestStruct? MyStruct { get; set; } + } + + private class TestStructInvalidClass + { + // Note: JsonTestStructConverter does not convert int, this is for negative testing. + [JsonConverter(typeof(JsonTestStructConverter))] + public int? MyInt { get; set; } + } + + [Fact] + public static void NullableCustomValueTypeUsingOptions() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonTestStructConverter()); + + { + TestStruct myStruct = JsonSerializer.Deserialize("1", options); + Assert.Equal(1, myStruct.InnerValue); + } + + { + TestStruct? myStruct = JsonSerializer.Deserialize("null", options); + Assert.False(myStruct.HasValue); + } + + { + TestStruct? myStruct = JsonSerializer.Deserialize("1", options); + Assert.Equal(1, myStruct.Value.InnerValue); + } + } + + [Fact] + public static void NullableCustomValueTypeUsingAttributes() + { + { + TestStructClass myStructClass = JsonSerializer.Deserialize(@"{""MyStruct"":null}"); + Assert.False(myStructClass.MyStruct.HasValue); + } + + { + TestStructClass myStructClass = JsonSerializer.Deserialize(@"{""MyStruct"":1}"); + Assert.True(myStructClass.MyStruct.HasValue); + Assert.Equal(1, myStructClass.MyStruct.Value.InnerValue); + } + } + + [Fact] + public static void NullableCustomValueTypeChoosesAttributeOverOptions() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonTestStructThrowingConverter()); + + // Chooses JsonTestStructThrowingConverter on options, which will throw. + Assert.Throws(() => JsonSerializer.Deserialize("1", options)); + + // Chooses JsonTestStructConverter on attribute, which will not throw. + TestStructClass myStructClass = JsonSerializer.Deserialize(@"{""MyStruct"":null}", options); + Assert.False(myStructClass.MyStruct.HasValue); + } + + [Fact] + public static void NullableCustomValueTypeNegativeTest() + { + Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyInt"":null}")); + Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyInt"":1}")); + } + + [Fact] + public static void NullableStandardValueTypeTest() + { + { + int? myInt = JsonSerializer.Deserialize("null"); + Assert.False(myInt.HasValue); + } + + { + int? myInt = JsonSerializer.Deserialize("1"); + Assert.Equal(1, myInt.Value); + } + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj index d101e9fe03f8b7..61e78a9182db8a 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj @@ -50,6 +50,7 @@ +