From a58661ee6e62863d4a92b66364ff50c81b907cc3 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Fri, 28 Aug 2020 16:54:55 -0700 Subject: [PATCH 1/5] Honor custom number handling only when property/type is a number/collection of numbers --- .../src/Resources/Strings.resx | 2 +- .../Converters/Value/ByteConverter.cs | 5 +- .../Converters/Value/DecimalConverter.cs | 5 +- .../Converters/Value/DoubleConverter.cs | 5 +- .../Converters/Value/Int16Converter.cs | 5 +- .../Converters/Value/Int32Converter.cs | 5 +- .../Converters/Value/Int64Converter.cs | 5 +- .../Converters/Value/JsonElementConverter.cs | 5 ++ .../Converters/Value/NullableConverter.cs | 3 +- .../Converters/Value/ObjectConverter.cs | 8 ++ .../Converters/Value/SByteConverter.cs | 5 +- .../Converters/Value/SingleConverter.cs | 6 +- .../Converters/Value/UInt16Converter.cs | 5 +- .../Converters/Value/UInt32Converter.cs | 5 +- .../Converters/Value/UInt64Converter.cs | 5 +- .../Text/Json/Serialization/JsonConverter.cs | 2 +- .../Json/Serialization/JsonConverterOfT.cs | 6 +- .../Json/Serialization/JsonPropertyInfo.cs | 74 +++++++++++++++---- .../Serialization/NumberHandlingTests.cs | 33 +++++++-- 19 files changed, 115 insertions(+), 74 deletions(-) diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 4562c9a84eb458..a14cc263f1eb57 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -540,7 +540,7 @@ 'JsonNumberHandlingAttribute' cannot be placed on a property, field, or type that is handled by a custom converter. See usage(s) of converter '{0}' on type '{1}'. - When 'JsonNumberHandlingAttribute' is placed on a property or field, the property or field must be a number or a collection. See member '{0}' on type '{1}'. + When 'JsonNumberHandlingAttribute' is placed on a property or field, the property or field must be a number or a collection of numbers. See member '{0}' on type '{1}'. The converter '{0}' handles type '{1}' but is being asked to convert type '{2}'. Either create a separate converter for type '{2}' or change the converter's 'CanConvert' method to only return 'true' for a single type. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs index f02b3ac8d70d50..56892e9cb21eb9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs @@ -5,10 +5,7 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class ByteConverter : JsonConverter { - public ByteConverter() - { - IsInternalConverterForNumberType = true; - } + internal override bool IsInternalConverterForNumberType => true; public override byte Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs index 559079eec617a5..bb4ce57949b8b4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs @@ -5,10 +5,7 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class DecimalConverter : JsonConverter { - public DecimalConverter() - { - IsInternalConverterForNumberType = true; - } + internal override bool IsInternalConverterForNumberType => true; public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs index 7b929b10e33974..a897cd1cca5e50 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs @@ -5,10 +5,7 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class DoubleConverter : JsonConverter { - public DoubleConverter() - { - IsInternalConverterForNumberType = true; - } + internal override bool IsInternalConverterForNumberType => true; public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs index f62da456a3a484..dade217f1dc260 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs @@ -5,10 +5,7 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class Int16Converter : JsonConverter { - public Int16Converter() - { - IsInternalConverterForNumberType = true; - } + internal override bool IsInternalConverterForNumberType => true; public override short Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs index 85d7fb3c6aa90c..d32b835ff4c3cf 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs @@ -5,10 +5,7 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class Int32Converter : JsonConverter { - public Int32Converter() - { - IsInternalConverterForNumberType = true; - } + internal override bool IsInternalConverterForNumberType => true; public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs index 48725dccbeecca..50d22c83ebd677 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs @@ -5,10 +5,7 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class Int64Converter : JsonConverter { - public Int64Converter() - { - IsInternalConverterForNumberType = true; - } + internal override bool IsInternalConverterForNumberType => true; public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs index a3a0d1ede13728..084ae45e0869d2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs @@ -17,5 +17,10 @@ public override void Write(Utf8JsonWriter writer, JsonElement value, JsonSeriali { value.WriteTo(writer); } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, JsonElement value, JsonNumberHandling handling) + { + value.WriteTo(writer); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs index 44e4270a26165b..952dad4df3de94 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs @@ -11,10 +11,11 @@ internal class NullableConverter : JsonConverter where T : struct // an instance is created only once for each JsonSerializerOptions instance. private readonly JsonConverter _converter; + internal override bool IsInternalConverterForNumberType => true; + public NullableConverter(JsonConverter converter) { _converter = converter; - IsInternalConverterForNumberType = converter.IsInternalConverterForNumberType; } public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs index a4a1d348b52a35..5bab8daa422faa 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs @@ -37,5 +37,13 @@ private JsonConverter GetRuntimeConverter(Type runtimeType, JsonSerializerOption return runtimeConverter; } + + internal override object ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) + { + using (JsonDocument document = JsonDocument.ParseValue(ref reader)) + { + return document.RootElement.Clone(); + } + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs index 0896bf97527ef5..4a13d4e8857f42 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs @@ -5,10 +5,7 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class SByteConverter : JsonConverter { - public SByteConverter() - { - IsInternalConverterForNumberType = true; - } + internal override bool IsInternalConverterForNumberType => true; public override sbyte Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs index 9b7e6fad328c63..471efd696c16cb 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs @@ -5,11 +5,7 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class SingleConverter : JsonConverter { - - public SingleConverter() - { - IsInternalConverterForNumberType = true; - } + internal override bool IsInternalConverterForNumberType => true; public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs index 249d2a059ffa1d..12c0ed86c3c2e3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs @@ -5,10 +5,7 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class UInt16Converter : JsonConverter { - public UInt16Converter() - { - IsInternalConverterForNumberType = true; - } + internal override bool IsInternalConverterForNumberType => true; public override ushort Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs index 2c6ea5e07d6b46..09d3e7762da161 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs @@ -5,10 +5,7 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class UInt32Converter : JsonConverter { - public UInt32Converter() - { - IsInternalConverterForNumberType = true; - } + internal override bool IsInternalConverterForNumberType => true; public override uint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs index 3d94f296de13fe..79ba635b4a0eb5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs @@ -5,10 +5,7 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class UInt64Converter : JsonConverter { - public UInt64Converter() - { - IsInternalConverterForNumberType = true; - } + internal override bool IsInternalConverterForNumberType => true; public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs index f8a000ecfeeacc..3c0d3789df22ba 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs @@ -52,7 +52,7 @@ internal JsonConverter() { } /// /// Whether the converter is built-in and handles a number type. /// - internal bool IsInternalConverterForNumberType; + internal virtual bool IsInternalConverterForNumberType { get; } /// /// Loosely-typed ReadCore() that forwards to strongly-typed ReadCore(). diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs index 9ce34a858bfe1f..8a6ec11b6b1aff 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs @@ -163,7 +163,7 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali // For performance, only perform validation on internal converters on debug builds. if (IsInternalConverter) { - if (IsInternalConverterForNumberType && state.Current.NumberHandling != null) + if (state.Current.NumberHandling != null) { value = ReadNumberWithCustomHandling(ref reader, state.Current.NumberHandling.Value); } @@ -179,7 +179,7 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali int originalPropertyDepth = reader.CurrentDepth; long originalPropertyBytesConsumed = reader.BytesConsumed; - if (IsInternalConverterForNumberType && state.Current.NumberHandling != null) + if (state.Current.NumberHandling != null) { value = ReadNumberWithCustomHandling(ref reader, state.Current.NumberHandling.Value); } @@ -356,7 +356,7 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions int originalPropertyDepth = writer.CurrentDepth; - if (IsInternalConverterForNumberType && state.Current.NumberHandling != null) + if (state.Current.NumberHandling != null) { WriteNumberWithCustomHandling(writer, value, state.Current.NumberHandling.Value); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs index 10b92e26fac964..e8e5bc47d5b8a4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs @@ -184,6 +184,9 @@ private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition, bool private void DetermineNumberHandling(JsonNumberHandling? parentTypeNumberHandling) { + bool propertyIsNumberOrNumberCollection = ConverterBase.IsInternalConverterForNumberType || TypeIsCollectionOfNumbers(); + bool numberHandlingIsApplicable = ConverterBase.IsInternalConverter && propertyIsNumberOrNumberCollection; + if (IsForClassInfo) { if (parentTypeNumberHandling != null && !ConverterBase.IsInternalConverter) @@ -191,13 +194,19 @@ private void DetermineNumberHandling(JsonNumberHandling? parentTypeNumberHandlin ThrowHelper.ThrowInvalidOperationException_NumberHandlingOnPropertyInvalid(this); } - // Priority 1: Get handling from the type (parent type in this case is the type itself). - NumberHandling = parentTypeNumberHandling; - - // Priority 2: Get handling from JsonSerializerOptions instance. - if (!NumberHandling.HasValue && Options.NumberHandling != JsonNumberHandling.Strict) + if (numberHandlingIsApplicable) { - NumberHandling = Options.NumberHandling; + // This logic is to honor JsonNumberHandlingAttribute placed on + // custom collections e.g. public class MyNumberList : List. + + // Priority 1: Get handling from the type (parent type in this case is the type itself). + NumberHandling = parentTypeNumberHandling; + + // Priority 2: Get handling from JsonSerializerOptions instance. + if (!NumberHandling.HasValue && Options.NumberHandling != JsonNumberHandling.Strict) + { + NumberHandling = Options.NumberHandling; + } } } else @@ -209,9 +218,7 @@ private void DetermineNumberHandling(JsonNumberHandling? parentTypeNumberHandlin { JsonNumberHandlingAttribute? attribute = GetAttribute(MemberInfo); - if (attribute != null && - !ConverterBase.IsInternalConverterForNumberType && - ((ClassType.Enumerable | ClassType.Dictionary) & ClassType) == 0) + if (attribute != null && !numberHandlingIsApplicable) { ThrowHelper.ThrowInvalidOperationException_NumberHandlingOnPropertyInvalid(this); } @@ -219,17 +226,52 @@ private void DetermineNumberHandling(JsonNumberHandling? parentTypeNumberHandlin handling = attribute?.Handling; } - // Priority 2: Get handling from attribute on parent class type. - handling ??= parentTypeNumberHandling; - - // Priority 3: Get handling from JsonSerializerOptions instance. - if (!handling.HasValue && Options.NumberHandling != JsonNumberHandling.Strict) + if (numberHandlingIsApplicable) { - handling = Options.NumberHandling; + // Priority 2: Get handling from attribute on parent class type. + handling ??= parentTypeNumberHandling; + + // Priority 3: Get handling from JsonSerializerOptions instance. + if (!handling.HasValue && Options.NumberHandling != JsonNumberHandling.Strict) + { + handling = Options.NumberHandling; + } + + NumberHandling = handling; } + } + } + + private bool TypeIsCollectionOfNumbers() + { + if (((ClassType.Enumerable | ClassType.Dictionary) & ClassType) == 0) + { + return false; + } - NumberHandling = handling; + Type? elementType = ConverterBase.ElementType; + Debug.Assert(elementType != null); + + elementType = Nullable.GetUnderlyingType(elementType) ?? elementType; + + if (elementType == typeof(byte) || + elementType == typeof(decimal) || + elementType == typeof(double) || + elementType == typeof(short) || + elementType == typeof(int) || + elementType == typeof(long) || + elementType == typeof(sbyte) || + elementType == typeof(float) || + elementType == typeof(ushort) || + elementType == typeof(uint) || + elementType == typeof(ulong) || + elementType == JsonClassInfo.ObjectType + ) + { + return true; } + + return false; } public static TAttribute? GetAttribute(MemberInfo memberInfo) where TAttribute : Attribute diff --git a/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs index 60acb54094d6f9..502a704e05474a 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs @@ -1102,26 +1102,46 @@ public class ClassWithSimpleCollectionProperty } [Fact] - public static void NestedCollectionElementTypeHandling_Overrides_ParentPropertyHandling() + public static void NestedCollectionElementTypeHandling_Overrides_GlobalOption() { // Strict policy on the collection element type overrides read-as-string on the collection property string json = @"{""MyList"":[{""Float"":""1""}]}"; - Assert.Throws(() => JsonSerializer.Deserialize(json)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionReadAndWriteFromStr)); // Strict policy on the collection element type overrides write-as-string on the collection property var obj = new ClassWithComplexListProperty { MyList = new List { new ClassWith_StrictAttribute { Float = 1 } } }; - Assert.Equal(@"{""MyList"":[{""Float"":1}]}", JsonSerializer.Serialize(obj)); + Assert.Equal(@"{""MyList"":[{""Float"":1}]}", JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr)); } public class ClassWithComplexListProperty + { + public List MyList { get; set; } + } + + [Fact] + public static void NumberHandlingAttribute_NotAllowedOn_CollectionOfNonNumbers() + { + Assert.Throws(() => JsonSerializer.Deserialize("")); + Assert.Throws(() => JsonSerializer.Serialize(new ClassWith_AttributeOnComplexListProperty())); + Assert.Throws(() => JsonSerializer.Deserialize("")); + Assert.Throws(() => JsonSerializer.Serialize(new ClassWith_AttributeOnComplexDictionaryProperty())); + } + + public class ClassWith_AttributeOnComplexListProperty { [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] public List MyList { get; set; } } + public class ClassWith_AttributeOnComplexDictionaryProperty + { + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public Dictionary MyDictionary { get; set; } + } + [Fact] public static void MemberAttributeAppliesToDictionary_SimpleElements() { @@ -1136,23 +1156,22 @@ public class ClassWithSimpleDictionaryProperty } [Fact] - public static void NestedDictionaryElementTypeHandling_Overrides_ParentPropertyHandling() + public static void NestedDictionaryElementTypeHandling_Overrides_GlobalOption() { // Strict policy on the dictionary element type overrides read-as-string on the collection property. string json = @"{""MyDictionary"":{""Key"":{""Float"":""1""}}}"; - Assert.Throws(() => JsonSerializer.Deserialize(json)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionReadFromStr)); // Strict policy on the collection element type overrides write-as-string on the collection property var obj = new ClassWithComplexDictionaryProperty { MyDictionary = new Dictionary { ["Key"] = new ClassWith_StrictAttribute { Float = 1 } } }; - Assert.Equal(@"{""MyDictionary"":{""Key"":{""Float"":1}}}", JsonSerializer.Serialize(obj)); + Assert.Equal(@"{""MyDictionary"":{""Key"":{""Float"":1}}}", JsonSerializer.Serialize(obj, s_optionReadFromStr)); } public class ClassWithComplexDictionaryProperty { - [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] public Dictionary MyDictionary { get; set; } } From 33f24be7f36dca2746a75a437393d7d886767a36 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Thu, 3 Sep 2020 13:13:14 -0700 Subject: [PATCH 2/5] Verify typeof(object) + custom number handling behavior --- .../Converters/Value/ObjectConverter.cs | 2 + .../Json/Serialization/JsonPropertyInfo.cs | 13 ++- .../Serialization/NumberHandlingTests.cs | 105 +++++++++++++++++- 3 files changed, 113 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs index 5bab8daa422faa..0e30711af99706 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs @@ -5,6 +5,8 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class ObjectConverter : JsonConverter { + internal override bool IsInternalConverterForNumberType => true; + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { using (JsonDocument document = JsonDocument.ParseValue(ref reader)) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs index e8e5bc47d5b8a4..e280d5901713d7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs @@ -184,8 +184,9 @@ private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition, bool private void DetermineNumberHandling(JsonNumberHandling? parentTypeNumberHandling) { - bool propertyIsNumberOrNumberCollection = ConverterBase.IsInternalConverterForNumberType || TypeIsCollectionOfNumbers(); - bool numberHandlingIsApplicable = ConverterBase.IsInternalConverter && propertyIsNumberOrNumberCollection; + bool numberHandlingIsApplicable = + ConverterBase.IsInternalConverterForNumberType || + TypeIsCollectionOfNumbersWithInternalConverter(); if (IsForClassInfo) { @@ -242,9 +243,10 @@ private void DetermineNumberHandling(JsonNumberHandling? parentTypeNumberHandlin } } - private bool TypeIsCollectionOfNumbers() + private bool TypeIsCollectionOfNumbersWithInternalConverter() { - if (((ClassType.Enumerable | ClassType.Dictionary) & ClassType) == 0) + if (!ConverterBase.IsInternalConverter || + ((ClassType.Enumerable | ClassType.Dictionary) & ClassType) == 0) { return false; } @@ -265,8 +267,7 @@ private bool TypeIsCollectionOfNumbers() elementType == typeof(ushort) || elementType == typeof(uint) || elementType == typeof(ulong) || - elementType == JsonClassInfo.ObjectType - ) + elementType == JsonClassInfo.ObjectType) { return true; } diff --git a/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs index 502a704e05474a..42cef9aa690392 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs @@ -126,7 +126,7 @@ private static void PerformAsRootTypeSerialization(T number, string jsonWithN } [Fact] - public static void Number_AsBoxedRootType() + public static void Number_AsBoxed_RootType() { string numberAsString = @"""2"""; @@ -146,6 +146,109 @@ public static void Number_AsBoxedRootType() Assert.Equal(2, (float?)JsonSerializer.Deserialize(numberAsString, typeof(float?), s_optionReadAndWriteFromStr)); } + [Fact] + public static void Number_AsBoxed_Property() + { + int @int = 2; + float? nullableFloat = 2; + + string expected = @"{""MyInt"":""2"",""MyNullableFloat"":""2""}"; + + var obj = new Class_With_BoxedNumbers + { + MyInt = @int, + MyNullableFloat = nullableFloat + }; + + string serialized = JsonSerializer.Serialize(obj); + JsonTestHelper.AssertJsonEqual(expected, serialized); + + obj = JsonSerializer.Deserialize(serialized); + + JsonElement el = Assert.IsType(obj.MyInt); + Assert.Equal(JsonValueKind.String, el.ValueKind); + Assert.Equal("2", el.GetString()); + + el = Assert.IsType(obj.MyNullableFloat); + Assert.Equal(JsonValueKind.String, el.ValueKind); + Assert.Equal("2", el.GetString()); + } + + public class Class_With_BoxedNumbers + { + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public object MyInt { get; set; } + + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public object MyNullableFloat { get; set; } + } + + [Fact] + public static void Number_AsBoxed_CollectionRootType_Element() + { + int @int = 2; + float? nullableFloat = 2; + + string expected = @"[""2""]"; + + var obj = new List { @int }; + string serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr); + Assert.Equal(expected, serialized); + + obj = JsonSerializer.Deserialize>(serialized, s_optionReadAndWriteFromStr); + + JsonElement el = Assert.IsType(obj[0]); + Assert.Equal(JsonValueKind.String, el.ValueKind); + Assert.Equal("2", el.GetString()); + + IList obj2 = new object[] { nullableFloat }; + serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr); + Assert.Equal(expected, serialized); + + obj2 = JsonSerializer.Deserialize(serialized, s_optionReadAndWriteFromStr); + + el = Assert.IsType(obj2[0]); + Assert.Equal(JsonValueKind.String, el.ValueKind); + Assert.Equal("2", el.GetString()); + } + + [Fact] + public static void Number_AsBoxed_CollectionProperty_Element() + { + int @int = 2; + float? nullableFloat = 2; + + string expected = @"{""MyInts"":[""2""],""MyNullableFloats"":[""2""]}"; + + var obj = new Class_With_ListsOfBoxedNumbers + { + MyInts = new List { @int }, + MyNullableFloats = new object[] { nullableFloat } + }; + + string serialized = JsonSerializer.Serialize(obj); + JsonTestHelper.AssertJsonEqual(expected, serialized); + + obj = JsonSerializer.Deserialize(serialized); + + JsonElement el = Assert.IsType(obj.MyInts[0]); + Assert.Equal(JsonValueKind.String, el.ValueKind); + Assert.Equal("2", el.GetString()); + + el = Assert.IsType(obj.MyNullableFloats[0]); + Assert.Equal(JsonValueKind.String, el.ValueKind); + Assert.Equal("2", el.GetString()); + } + + public class Class_With_ListsOfBoxedNumbers + { + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public List MyInts { get; set; } + + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public IList MyNullableFloats { get; set; } + } + [Fact] public static void Number_AsCollectionElement_RoundTrip() { From 62eb63dc4d2606d2155411c69bc4ea0fec640560 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Thu, 3 Sep 2020 14:17:13 -0700 Subject: [PATCH 3/5] Simplify setting of per-property number handling --- .../Json/Serialization/JsonPropertyInfo.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs index e280d5901713d7..f469de0b20e356 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs @@ -212,23 +212,19 @@ private void DetermineNumberHandling(JsonNumberHandling? parentTypeNumberHandlin } else { - JsonNumberHandling? handling = null; + Debug.Assert(MemberInfo != null); - // Priority 1: Get handling from attribute on property or field. - if (MemberInfo != null) + JsonNumberHandlingAttribute? attribute = GetAttribute(MemberInfo); + if (attribute != null && !numberHandlingIsApplicable) { - JsonNumberHandlingAttribute? attribute = GetAttribute(MemberInfo); - - if (attribute != null && !numberHandlingIsApplicable) - { - ThrowHelper.ThrowInvalidOperationException_NumberHandlingOnPropertyInvalid(this); - } - - handling = attribute?.Handling; + ThrowHelper.ThrowInvalidOperationException_NumberHandlingOnPropertyInvalid(this); } if (numberHandlingIsApplicable) { + // Priority 1: Get handling from attribute on property or field. + JsonNumberHandling? handling = attribute?.Handling; + // Priority 2: Get handling from attribute on parent class type. handling ??= parentTypeNumberHandling; From a6e17ee5febfe8a73daa55fedfd078c62b0cdb34 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Thu, 3 Sep 2020 14:21:21 -0700 Subject: [PATCH 4/5] Clean up number-handling-applicable check --- .../src/System/Text/Json/Serialization/JsonPropertyInfo.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs index f469de0b20e356..4d7d9f40f2c816 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs @@ -184,9 +184,7 @@ private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition, bool private void DetermineNumberHandling(JsonNumberHandling? parentTypeNumberHandling) { - bool numberHandlingIsApplicable = - ConverterBase.IsInternalConverterForNumberType || - TypeIsCollectionOfNumbersWithInternalConverter(); + bool numberHandlingIsApplicable = ConverterBase.IsInternalConverterForNumberType || TypeIsCollectionOfNumbersWithInternalConverter(); if (IsForClassInfo) { From 2c8adaad6e60ef4dbc0d63f8d3c270d781f4bcef Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Fri, 4 Sep 2020 13:43:49 -0700 Subject: [PATCH 5/5] Fix issue with number-handling + boxed non-number types --- .../Converters/Value/ByteConverter.cs | 5 +- .../Converters/Value/DecimalConverter.cs | 5 +- .../Converters/Value/DoubleConverter.cs | 5 +- .../Converters/Value/Int16Converter.cs | 5 +- .../Converters/Value/Int32Converter.cs | 5 +- .../Converters/Value/Int64Converter.cs | 5 +- .../Converters/Value/JsonElementConverter.cs | 5 - .../Converters/Value/NullableConverter.cs | 3 +- .../Converters/Value/ObjectConverter.cs | 5 +- .../Converters/Value/SByteConverter.cs | 5 +- .../Converters/Value/SingleConverter.cs | 6 +- .../Converters/Value/UInt16Converter.cs | 5 +- .../Converters/Value/UInt32Converter.cs | 5 +- .../Converters/Value/UInt64Converter.cs | 5 +- .../Text/Json/Serialization/JsonConverter.cs | 2 +- .../Json/Serialization/JsonConverterOfT.cs | 2 +- .../Serialization/NumberHandlingTests.cs | 121 +++++++++++++++++- 17 files changed, 166 insertions(+), 28 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs index 56892e9cb21eb9..f02b3ac8d70d50 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs @@ -5,7 +5,10 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class ByteConverter : JsonConverter { - internal override bool IsInternalConverterForNumberType => true; + public ByteConverter() + { + IsInternalConverterForNumberType = true; + } public override byte Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs index bb4ce57949b8b4..559079eec617a5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs @@ -5,7 +5,10 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class DecimalConverter : JsonConverter { - internal override bool IsInternalConverterForNumberType => true; + public DecimalConverter() + { + IsInternalConverterForNumberType = true; + } public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs index a897cd1cca5e50..7b929b10e33974 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs @@ -5,7 +5,10 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class DoubleConverter : JsonConverter { - internal override bool IsInternalConverterForNumberType => true; + public DoubleConverter() + { + IsInternalConverterForNumberType = true; + } public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs index dade217f1dc260..f62da456a3a484 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs @@ -5,7 +5,10 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class Int16Converter : JsonConverter { - internal override bool IsInternalConverterForNumberType => true; + public Int16Converter() + { + IsInternalConverterForNumberType = true; + } public override short Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs index d32b835ff4c3cf..85d7fb3c6aa90c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs @@ -5,7 +5,10 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class Int32Converter : JsonConverter { - internal override bool IsInternalConverterForNumberType => true; + public Int32Converter() + { + IsInternalConverterForNumberType = true; + } public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs index 50d22c83ebd677..48725dccbeecca 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs @@ -5,7 +5,10 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class Int64Converter : JsonConverter { - internal override bool IsInternalConverterForNumberType => true; + public Int64Converter() + { + IsInternalConverterForNumberType = true; + } public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs index 084ae45e0869d2..a3a0d1ede13728 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs @@ -17,10 +17,5 @@ public override void Write(Utf8JsonWriter writer, JsonElement value, JsonSeriali { value.WriteTo(writer); } - - internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, JsonElement value, JsonNumberHandling handling) - { - value.WriteTo(writer); - } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs index 952dad4df3de94..44e4270a26165b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs @@ -11,11 +11,10 @@ internal class NullableConverter : JsonConverter where T : struct // an instance is created only once for each JsonSerializerOptions instance. private readonly JsonConverter _converter; - internal override bool IsInternalConverterForNumberType => true; - public NullableConverter(JsonConverter converter) { _converter = converter; + IsInternalConverterForNumberType = converter.IsInternalConverterForNumberType; } public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs index 0e30711af99706..a9feb74b5c6c1a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs @@ -5,7 +5,10 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class ObjectConverter : JsonConverter { - internal override bool IsInternalConverterForNumberType => true; + public ObjectConverter() + { + IsInternalConverterForNumberType = true; + } public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs index 4a13d4e8857f42..0896bf97527ef5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs @@ -5,7 +5,10 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class SByteConverter : JsonConverter { - internal override bool IsInternalConverterForNumberType => true; + public SByteConverter() + { + IsInternalConverterForNumberType = true; + } public override sbyte Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs index 471efd696c16cb..9b7e6fad328c63 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs @@ -5,7 +5,11 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class SingleConverter : JsonConverter { - internal override bool IsInternalConverterForNumberType => true; + + public SingleConverter() + { + IsInternalConverterForNumberType = true; + } public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs index 12c0ed86c3c2e3..249d2a059ffa1d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs @@ -5,7 +5,10 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class UInt16Converter : JsonConverter { - internal override bool IsInternalConverterForNumberType => true; + public UInt16Converter() + { + IsInternalConverterForNumberType = true; + } public override ushort Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs index 09d3e7762da161..2c6ea5e07d6b46 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs @@ -5,7 +5,10 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class UInt32Converter : JsonConverter { - internal override bool IsInternalConverterForNumberType => true; + public UInt32Converter() + { + IsInternalConverterForNumberType = true; + } public override uint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs index 79ba635b4a0eb5..3d94f296de13fe 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs @@ -5,7 +5,10 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class UInt64Converter : JsonConverter { - internal override bool IsInternalConverterForNumberType => true; + public UInt64Converter() + { + IsInternalConverterForNumberType = true; + } public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs index 3c0d3789df22ba..f8a000ecfeeacc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs @@ -52,7 +52,7 @@ internal JsonConverter() { } /// /// Whether the converter is built-in and handles a number type. /// - internal virtual bool IsInternalConverterForNumberType { get; } + internal bool IsInternalConverterForNumberType; /// /// Loosely-typed ReadCore() that forwards to strongly-typed ReadCore(). diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs index 8a6ec11b6b1aff..38bd9cb480f8b5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs @@ -356,7 +356,7 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions int originalPropertyDepth = writer.CurrentDepth; - if (state.Current.NumberHandling != null) + if (state.Current.NumberHandling != null && IsInternalConverterForNumberType) { WriteNumberWithCustomHandling(writer, value, state.Current.NumberHandling.Value); } diff --git a/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs index 42cef9aa690392..f306e3a3287484 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs @@ -149,10 +149,10 @@ public static void Number_AsBoxed_RootType() [Fact] public static void Number_AsBoxed_Property() { - int @int = 2; + int @int = 1; float? nullableFloat = 2; - string expected = @"{""MyInt"":""2"",""MyNullableFloat"":""2""}"; + string expected = @"{""MyInt"":""1"",""MyNullableFloat"":""2""}"; var obj = new Class_With_BoxedNumbers { @@ -167,7 +167,7 @@ public static void Number_AsBoxed_Property() JsonElement el = Assert.IsType(obj.MyInt); Assert.Equal(JsonValueKind.String, el.ValueKind); - Assert.Equal("2", el.GetString()); + Assert.Equal("1", el.GetString()); el = Assert.IsType(obj.MyNullableFloat); Assert.Equal(JsonValueKind.String, el.ValueKind); @@ -186,10 +186,10 @@ public class Class_With_BoxedNumbers [Fact] public static void Number_AsBoxed_CollectionRootType_Element() { - int @int = 2; + int @int = 1; float? nullableFloat = 2; - string expected = @"[""2""]"; + string expected = @"[""1""]"; var obj = new List { @int }; string serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr); @@ -199,10 +199,12 @@ public static void Number_AsBoxed_CollectionRootType_Element() JsonElement el = Assert.IsType(obj[0]); Assert.Equal(JsonValueKind.String, el.ValueKind); - Assert.Equal("2", el.GetString()); + Assert.Equal("1", el.GetString()); + + expected = @"[""2""]"; IList obj2 = new object[] { nullableFloat }; - serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr); + serialized = JsonSerializer.Serialize(obj2, s_optionReadAndWriteFromStr); Assert.Equal(expected, serialized); obj2 = JsonSerializer.Deserialize(serialized, s_optionReadAndWriteFromStr); @@ -249,6 +251,111 @@ public class Class_With_ListsOfBoxedNumbers public IList MyNullableFloats { get; set; } } + [Fact] + public static void NonNumber_AsBoxed_Property() + { + DateTime dateTime = DateTime.Now; + Guid? nullableGuid = Guid.NewGuid(); + + string expected = @$"{{""MyDateTime"":{JsonSerializer.Serialize(dateTime)},""MyNullableGuid"":{JsonSerializer.Serialize(nullableGuid)}}}"; + + var obj = new Class_With_BoxedNonNumbers + { + MyDateTime = dateTime, + MyNullableGuid = nullableGuid + }; + + string serialized = JsonSerializer.Serialize(obj); + JsonTestHelper.AssertJsonEqual(expected, serialized); + + obj = JsonSerializer.Deserialize(serialized); + + JsonElement el = Assert.IsType(obj.MyDateTime); + Assert.Equal(JsonValueKind.String, el.ValueKind); + Assert.Equal(dateTime, el.GetDateTime()); + + el = Assert.IsType(obj.MyNullableGuid); + Assert.Equal(JsonValueKind.String, el.ValueKind); + Assert.Equal(nullableGuid.Value, el.GetGuid()); + } + + public class Class_With_BoxedNonNumbers + { + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public object MyDateTime { get; set; } + + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public object MyNullableGuid { get; set; } + } + + [Fact] + public static void NonNumber_AsBoxed_CollectionRootType_Element() + { + DateTime dateTime = DateTime.Now; + Guid? nullableGuid = Guid.NewGuid(); + + string expected = @$"[{JsonSerializer.Serialize(dateTime)}]"; + + var obj = new List { dateTime }; + string serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr); + Assert.Equal(expected, serialized); + + obj = JsonSerializer.Deserialize>(serialized, s_optionReadAndWriteFromStr); + + JsonElement el = Assert.IsType(obj[0]); + Assert.Equal(JsonValueKind.String, el.ValueKind); + Assert.Equal(dateTime, el.GetDateTime()); + + expected = @$"[{JsonSerializer.Serialize(nullableGuid)}]"; + + IList obj2 = new object[] { nullableGuid }; + serialized = JsonSerializer.Serialize(obj2, s_optionReadAndWriteFromStr); + Assert.Equal(expected, serialized); + + obj2 = JsonSerializer.Deserialize(serialized, s_optionReadAndWriteFromStr); + + el = Assert.IsType(obj2[0]); + Assert.Equal(JsonValueKind.String, el.ValueKind); + Assert.Equal(nullableGuid.Value, el.GetGuid()); + } + + [Fact] + public static void NonNumber_AsBoxed_CollectionProperty_Element() + { + DateTime dateTime = DateTime.Now; + Guid? nullableGuid = Guid.NewGuid(); + + string expected = @$"{{""MyDateTimes"":[{JsonSerializer.Serialize(dateTime)}],""MyNullableGuids"":[{JsonSerializer.Serialize(nullableGuid)}]}}"; + + var obj = new Class_With_ListsOfBoxedNonNumbers + { + MyDateTimes = new List { dateTime }, + MyNullableGuids = new object[] { nullableGuid } + }; + + string serialized = JsonSerializer.Serialize(obj); + JsonTestHelper.AssertJsonEqual(expected, serialized); + + obj = JsonSerializer.Deserialize(serialized); + + JsonElement el = Assert.IsType(obj.MyDateTimes[0]); + Assert.Equal(JsonValueKind.String, el.ValueKind); + Assert.Equal(dateTime, el.GetDateTime()); + + el = Assert.IsType(obj.MyNullableGuids[0]); + Assert.Equal(JsonValueKind.String, el.ValueKind); + Assert.Equal(nullableGuid, el.GetGuid()); + } + + public class Class_With_ListsOfBoxedNonNumbers + { + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public List MyDateTimes { get; set; } + + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public IList MyNullableGuids { get; set; } + } + [Fact] public static void Number_AsCollectionElement_RoundTrip() {