From ee7858274036a1f3ea1d76121fd431f7e5021245 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Sat, 11 Jul 2020 21:28:47 -0700 Subject: [PATCH] Add JsonNumberHandling & support for (de)serializing numbers from/to string --- .../System.Text.Json/ref/System.Text.Json.cs | 15 + .../src/Resources/Strings.resx | 6 + .../src/System.Text.Json.csproj | 2 + .../src/System/Text/Json/JsonConstants.cs | 4 + .../Text/Json/Reader/JsonReaderHelper.cs | 77 + .../Text/Json/Reader/Utf8JsonReader.TryGet.cs | 93 +- .../Attributes/JsonNumberHandlingAttribute.cs | 30 + .../Converters/Collection/ArrayConverter.cs | 2 +- .../Collection/DictionaryDefaultConverter.cs | 20 +- .../DictionaryOfTKeyTValueConverter.cs | 8 +- .../Collection/IDictionaryConverter.cs | 2 +- .../IDictionaryOfTKeyTValueConverter.cs | 2 +- .../Collection/IEnumerableDefaultConverter.cs | 12 +- .../Converters/Collection/IListConverter.cs | 45 +- ...ReadOnlyDictionaryOfTKeyTValueConverter.cs | 2 +- ...mmutableDictionaryOfTKeyTValueConverter.cs | 2 +- .../Converters/Collection/ListOfTConverter.cs | 2 +- .../Object/KeyValuePairConverter.cs | 1 + .../Object/ObjectDefaultConverter.cs | 2 + ...ctWithParameterizedConstructorConverter.cs | 3 + .../Converters/Value/ByteConverter.cs | 27 + .../Converters/Value/DecimalConverter.cs | 28 + .../Converters/Value/DoubleConverter.cs | 38 + .../Converters/Value/Int16Converter.cs | 31 +- .../Converters/Value/Int32Converter.cs | 29 + .../Converters/Value/Int64Converter.cs | 28 + .../Converters/Value/NullableConverter.cs | 30 + .../Converters/Value/SByteConverter.cs | 28 + .../Converters/Value/SingleConverter.cs | 39 + .../Converters/Value/StringConverter.cs | 2 - .../Converters/Value/UInt16Converter.cs | 29 + .../Converters/Value/UInt32Converter.cs | 29 + .../Converters/Value/UInt64Converter.cs | 28 + .../Json/Serialization/JsonClassInfo.Cache.cs | 16 +- .../Text/Json/Serialization/JsonClassInfo.cs | 17 +- .../Text/Json/Serialization/JsonConverter.cs | 12 +- .../Json/Serialization/JsonConverterOfT.cs | 41 +- .../Json/Serialization/JsonNumberHandling.cs | 32 + .../Json/Serialization/JsonParameterInfo.cs | 3 + .../Json/Serialization/JsonPropertyInfo.cs | 68 +- .../Json/Serialization/JsonPropertyInfoOfT.cs | 12 +- .../JsonSerializer.Read.HandlePropertyName.cs | 1 + .../JsonSerializer.Read.Helpers.cs | 7 + .../JsonSerializerOptions.Converters.cs | 2 +- .../Serialization/JsonSerializerOptions.cs | 23 + .../Text/Json/Serialization/ReadStack.cs | 15 +- .../Text/Json/Serialization/ReadStackFrame.cs | 5 + .../Text/Json/Serialization/WriteStack.cs | 11 +- .../Json/Serialization/WriteStackFrame.cs | 3 + .../Text/Json/ThrowHelper.Serialization.cs | 22 + .../Utf8JsonWriter.WriteValues.Decimal.cs | 8 + .../Utf8JsonWriter.WriteValues.Double.cs | 28 + .../Utf8JsonWriter.WriteValues.Float.cs | 28 + ...Utf8JsonWriter.WriteValues.SignedNumber.cs | 8 + .../Utf8JsonWriter.WriteValues.String.cs | 14 + ...f8JsonWriter.WriteValues.UnsignedNumber.cs | 8 + .../tests/JsonNumberTestData.cs | 29 +- .../Serialization/NumberHandlingTests.cs | 1414 +++++++++++++++++ .../tests/Serialization/Object.ReadTests.cs | 4 +- .../tests/Serialization/OptionsTests.cs | 5 + .../tests/Serialization/Stream.Collections.cs | 38 +- .../TestClasses.NonGenericCollections.cs | 10 + .../Serialization/TestClasses/TestClasses.cs | 66 + .../tests/System.Text.Json.Tests.csproj | 3 +- 64 files changed, 2502 insertions(+), 147 deletions(-) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonNumberHandlingAttribute.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberHandling.cs create mode 100644 src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 0ab83e8b526561..0de194ebde5bca 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -244,6 +244,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults defaults) { public bool IgnoreReadOnlyFields { get { throw null; } set { } } public bool IncludeFields { get { throw null; } set { } } public int MaxDepth { get { throw null; } set { } } + public System.Text.Json.Serialization.JsonNumberHandling NumberHandling { get { throw null; } set { } } public bool PropertyNameCaseInsensitive { get { throw null; } set { } } public System.Text.Json.JsonNamingPolicy? PropertyNamingPolicy { get { throw null; } set { } } public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } } @@ -496,6 +497,14 @@ public enum JsonIgnoreCondition WhenWritingDefault = 2, WhenWritingNull = 3, } + [System.FlagsAttribute] + public enum JsonNumberHandling + { + Strict = 0, + AllowReadingFromString = 1, + WriteAsString = 2, + AllowNamedFloatingPointLiterals = 4, + } public abstract partial class JsonAttribute : System.Attribute { protected JsonAttribute() { } @@ -533,6 +542,12 @@ protected internal JsonConverter() { } public abstract T Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options); public abstract void Write(System.Text.Json.Utf8JsonWriter writer, T value, System.Text.Json.JsonSerializerOptions options); } + [System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Struct | System.AttributeTargets.Property | System.AttributeTargets.Field, AllowMultiple = false)] + public sealed partial class JsonNumberHandlingAttribute : System.Text.Json.Serialization.JsonAttribute + { + public JsonNumberHandlingAttribute(System.Text.Json.Serialization.JsonNumberHandling handling) { } + public System.Text.Json.Serialization.JsonNumberHandling Handling { get { throw null; } } + } [System.AttributeUsageAttribute(System.AttributeTargets.Constructor, AllowMultiple = false)] public sealed partial class JsonConstructorAttribute : System.Text.Json.Serialization.JsonAttribute { diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 41cf9de418c483..d5db3cfc6fb1b0 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -536,4 +536,10 @@ The ignore condition 'JsonIgnoreCondition.WhenWritingNull' is not valid on value-type member '{0}' on type '{1}'. Consider using 'JsonIgnoreCondition.WhenWritingDefault'. + + '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}'. + \ No newline at end of file diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index e381a1b381d464..e787defd06be02 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -60,6 +60,7 @@ + @@ -135,6 +136,7 @@ + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs index 2bfc41c6bd912c..2e1bb4e16cfe17 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs @@ -37,6 +37,10 @@ internal static class JsonConstants public static ReadOnlySpan FalseValue => new byte[] { (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e' }; public static ReadOnlySpan NullValue => new byte[] { (byte)'n', (byte)'u', (byte)'l', (byte)'l' }; + public static ReadOnlySpan NaNValue => new byte[] { (byte)'N', (byte)'a', (byte)'N' }; + public static ReadOnlySpan PositiveInfinityValue => new byte[] { (byte)'I', (byte)'n', (byte)'f', (byte)'i', (byte)'n', (byte)'i', (byte)'t', (byte)'y' }; + public static ReadOnlySpan NegativeInfinityValue => new byte[] { (byte)'-', (byte)'I', (byte)'n', (byte)'f', (byte)'i', (byte)'n', (byte)'i', (byte)'t', (byte)'y' }; + // Used to search for the end of a number public static ReadOnlySpan Delimiters => new byte[] { ListSeparator, CloseBrace, CloseBracket, Space, LineFeed, CarriageReturn, Tab, Slash }; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs index fcd7f0d3fb8da7..dbea5c0fea0c10 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs @@ -321,5 +321,82 @@ public static bool TryGetEscapedGuid(ReadOnlySpan source, out Guid value) value = default; return false; } + + public static char GetFloatingPointStandardParseFormat(ReadOnlySpan span) + { + // Assume that 'e/E' is closer to the end. + int startIndex = span.Length - 1; + for (int i = startIndex; i >= 0; i--) + { + byte token = span[i]; + if (token == 'E' || token == 'e') + { + return JsonConstants.ScientificNotationFormat; + } + } + return default; + } + + public static bool TryGetFloatingPointConstant(ReadOnlySpan span, out float value) + { + if (span.Length == 3) + { + if (span.SequenceEqual(JsonConstants.NaNValue)) + { + value = float.NaN; + return true; + } + } + else if (span.Length == 8) + { + if (span.SequenceEqual(JsonConstants.PositiveInfinityValue)) + { + value = float.PositiveInfinity; + return true; + } + } + else if (span.Length == 9) + { + if (span.SequenceEqual(JsonConstants.NegativeInfinityValue)) + { + value = float.NegativeInfinity; + return true; + } + } + + value = 0; + return false; + } + + public static bool TryGetFloatingPointConstant(ReadOnlySpan span, out double value) + { + if (span.Length == 3) + { + if (span.SequenceEqual(JsonConstants.NaNValue)) + { + value = double.NaN; + return true; + } + } + else if (span.Length == 8) + { + if (span.SequenceEqual(JsonConstants.PositiveInfinityValue)) + { + value = double.PositiveInfinity; + return true; + } + } + else if (span.Length == 9) + { + if (span.SequenceEqual(JsonConstants.NegativeInfinityValue)) + { + value = double.NegativeInfinity; + return true; + } + } + + value = 0; + return false; + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs index dbc52d94b79d57..d08cbcbc28dd06 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs @@ -416,11 +416,38 @@ public float GetSingle() internal float GetSingleWithQuotes() { ReadOnlySpan span = GetUnescapedSpan(); - if (!TryGetSingleCore(out float value, span)) + + if (JsonReaderHelper.TryGetFloatingPointConstant(span, out float value)) { - throw ThrowHelper.GetFormatException(NumericType.Single); + return value; } - return value; + + char numberFormat = JsonReaderHelper.GetFloatingPointStandardParseFormat(span); + if (Utf8Parser.TryParse(span, out value, out int bytesConsumed, numberFormat) + && span.Length == bytesConsumed) + { + // NETCOREAPP implementation of the TryParse method above permits case-insenstive variants of the + // float constants "NaN", "Infinity", "-Infinity". This differs from the NETFRAMEWORK implementation. + // The following logic reconciles the two implementations to enforce consistent behavior. + if (!float.IsNaN(value) && !float.IsPositiveInfinity(value) && !float.IsNegativeInfinity(value)) + { + return value; + } + } + + throw ThrowHelper.GetFormatException(NumericType.Single); + } + + internal float GetSingleFloatingPointConstant() + { + ReadOnlySpan span = GetUnescapedSpan(); + + if (JsonReaderHelper.TryGetFloatingPointConstant(span, out float value)) + { + return value; + } + + throw ThrowHelper.GetFormatException(NumericType.Single); } /// @@ -449,11 +476,38 @@ public double GetDouble() internal double GetDoubleWithQuotes() { ReadOnlySpan span = GetUnescapedSpan(); - if (!TryGetDoubleCore(out double value, span)) + + if (JsonReaderHelper.TryGetFloatingPointConstant(span, out double value)) { - throw ThrowHelper.GetFormatException(NumericType.Double); + return value; } - return value; + + char numberFormat = JsonReaderHelper.GetFloatingPointStandardParseFormat(span); + if (Utf8Parser.TryParse(span, out value, out int bytesConsumed, numberFormat) + && span.Length == bytesConsumed) + { + // NETCOREAPP implmentation of the TryParse method above permits case-insenstive variants of the + // float constants "NaN", "Infinity", "-Infinity". This differs from the NETFRAMEWORK implementation. + // The following logic reconciles the two implementations to enforce consistent behavior. + if (!double.IsNaN(value) && !double.IsPositiveInfinity(value) && !double.IsNegativeInfinity(value)) + { + return value; + } + } + + throw ThrowHelper.GetFormatException(NumericType.Double); + } + + internal double GetDoubleFloatingPointConstant() + { + ReadOnlySpan span = GetUnescapedSpan(); + + if (JsonReaderHelper.TryGetFloatingPointConstant(span, out double value)) + { + return value; + } + + throw ThrowHelper.GetFormatException(NumericType.Double); } /// @@ -482,11 +536,15 @@ public decimal GetDecimal() internal decimal GetDecimalWithQuotes() { ReadOnlySpan span = GetUnescapedSpan(); - if (!TryGetDecimalCore(out decimal value, span)) + + char numberFormat = JsonReaderHelper.GetFloatingPointStandardParseFormat(span); + if (Utf8Parser.TryParse(span, out decimal value, out int bytesConsumed, numberFormat) + && span.Length == bytesConsumed) { - throw ThrowHelper.GetFormatException(NumericType.Decimal); + return value; } - return value; + + throw ThrowHelper.GetFormatException(NumericType.Decimal); } /// @@ -919,13 +977,8 @@ public bool TryGetSingle(out float value) throw ThrowHelper.GetInvalidOperationException_ExpectedNumber(TokenType); } - ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; - return TryGetSingleCore(out value, span); - } + ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal bool TryGetSingleCore(out float value, ReadOnlySpan span) - { if (Utf8Parser.TryParse(span, out float tmp, out int bytesConsumed, _numberFormat) && span.Length == bytesConsumed) { @@ -955,12 +1008,7 @@ public bool TryGetDouble(out double value) } ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; - return TryGetDoubleCore(out value, span); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal bool TryGetDoubleCore(out double value, ReadOnlySpan span) - { if (Utf8Parser.TryParse(span, out double tmp, out int bytesConsumed, _numberFormat) && span.Length == bytesConsumed) { @@ -990,12 +1038,7 @@ public bool TryGetDecimal(out decimal value) } ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; - return TryGetDecimalCore(out value, span); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal bool TryGetDecimalCore(out decimal value, ReadOnlySpan span) - { if (Utf8Parser.TryParse(span, out decimal tmp, out int bytesConsumed, _numberFormat) && span.Length == bytesConsumed) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonNumberHandlingAttribute.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonNumberHandlingAttribute.cs new file mode 100644 index 00000000000000..581d4aacd766e4 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonNumberHandlingAttribute.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization +{ + /// + /// When placed on a type, property, or field, indicates what + /// settings should be used when serializing or deserialing numbers. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] + public sealed class JsonNumberHandlingAttribute : JsonAttribute + { + /// + /// Indicates what settings should be used when serializing or deserialing numbers. + /// + public JsonNumberHandling Handling { get; } + + /// + /// Initializes a new instance of . + /// + public JsonNumberHandlingAttribute(JsonNumberHandling handling) + { + if (!JsonSerializer.IsValidNumberHandlingValue(handling)) + { + throw new ArgumentOutOfRangeException(nameof(handling)); + } + Handling = handling; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs index 4337037817f050..9147243ef95be1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs @@ -38,7 +38,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, int index = state.Current.EnumeratorIndex; JsonConverter elementConverter = GetElementConverter(ref state); - if (elementConverter.CanUseDirectReadOrWrite) + if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { // Fast path that avoids validation and extra indirection. for (; index < array.Length; index++) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs index cd20a06206db83..36d3acd1606f20 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs @@ -29,7 +29,9 @@ protected virtual void ConvertCollection(ref ReadStack state, JsonSerializerOpti /// protected virtual void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state) { } - internal override Type ElementType => typeof(TValue); + private static Type s_valueType = typeof(TValue); + + internal override Type ElementType => s_valueType; protected Type KeyType = typeof(TKey); // For string keys we don't use a key converter @@ -39,9 +41,9 @@ protected virtual void CreateCollection(ref Utf8JsonReader reader, ref ReadStack protected JsonConverter? _keyConverter; protected JsonConverter? _valueConverter; - protected static JsonConverter GetValueConverter(JsonClassInfo classInfo) + protected static JsonConverter GetValueConverter(JsonClassInfo elementClassInfo) { - JsonConverter converter = (JsonConverter)classInfo.ElementClassInfo!.PropertyInfoForClassInfo.ConverterBase; + JsonConverter converter = (JsonConverter)elementClassInfo.PropertyInfoForClassInfo.ConverterBase; Debug.Assert(converter != null); // It should not be possible to have a null converter at this point. return converter; @@ -57,6 +59,8 @@ internal sealed override bool OnTryRead( ref ReadStack state, [MaybeNullWhen(false)] out TCollection value) { + JsonClassInfo elementClassInfo = state.Current.JsonClassInfo.ElementClassInfo!; + if (state.UseFastPath) { // Fast path that avoids maintaining state variables and dealing with preserved references. @@ -68,8 +72,8 @@ internal sealed override bool OnTryRead( CreateCollection(ref reader, ref state); - JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo); - if (valueConverter.CanUseDirectReadOrWrite) + JsonConverter valueConverter = _valueConverter ??= GetValueConverter(elementClassInfo); + if (valueConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { // Process all elements. while (true) @@ -89,7 +93,7 @@ internal sealed override bool OnTryRead( // Read the value and add. reader.ReadWithVerify(); - TValue element = valueConverter.Read(ref reader, typeof(TValue), options); + TValue element = valueConverter.Read(ref reader, s_valueType, options); Add(key, element!, options, ref state); } } @@ -114,7 +118,7 @@ internal sealed override bool OnTryRead( reader.ReadWithVerify(); // Get the value from the converter and add it. - valueConverter.TryRead(ref reader, typeof(TValue), options, ref state, out TValue element); + valueConverter.TryRead(ref reader, s_valueType, options, ref state, out TValue element); Add(key, element!, options, ref state); } } @@ -172,7 +176,7 @@ internal sealed override bool OnTryRead( } // Process all elements. - JsonConverter elementConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo); + JsonConverter elementConverter = _valueConverter ??= GetValueConverter(elementClassInfo); while (true) { if (state.Current.PropertyState == StackFramePropertyState.None) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs index 10818b7d31eeee..c89e1f9be07ee8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs @@ -49,16 +49,18 @@ protected internal override bool OnWriteResume( enumerator = (Dictionary.Enumerator)state.Current.CollectionEnumerator; } + JsonClassInfo elementClassInfo = state.Current.JsonClassInfo.ElementClassInfo!; + JsonConverter keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options); - JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo); - if (!state.SupportContinuation && valueConverter.CanUseDirectReadOrWrite) + JsonConverter valueConverter = _valueConverter ??= GetValueConverter(elementClassInfo); + + if (!state.SupportContinuation && valueConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { // Fast path that avoids validation and extra indirection. do { TKey key = enumerator.Current.Key; keyConverter.WriteWithQuotes(writer, key, options, ref state); - valueConverter.Write(writer, enumerator.Current.Value, options); } while (enumerator.MoveNext()); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs index d1055df9ec5817..5bfef5de0b1eb7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs @@ -71,7 +71,7 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio enumerator = (IDictionaryEnumerator)state.Current.CollectionEnumerator; } - JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo); + JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo.ElementClassInfo!); do { if (ShouldFlush(writer, ref state)) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfTKeyTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfTKeyTValueConverter.cs index 4fb795ce71a8f4..44c2069f6fd2e4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfTKeyTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfTKeyTValueConverter.cs @@ -71,7 +71,7 @@ protected internal override bool OnWriteResume( } JsonConverter keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options); - JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo); + JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo.ElementClassInfo!); do { if (ShouldFlush(writer, ref state)) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs index dbe43be8bdc808..250ba4c14713f3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs @@ -16,9 +16,9 @@ internal abstract class IEnumerableDefaultConverter protected abstract void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options); protected virtual void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) { } - protected static JsonConverter GetElementConverter(ref ReadStack state) + protected static JsonConverter GetElementConverter(JsonClassInfo elementClassInfo) { - JsonConverter converter = (JsonConverter)state.Current.JsonClassInfo.ElementClassInfo!.PropertyInfoForClassInfo.ConverterBase; + JsonConverter converter = (JsonConverter)elementClassInfo.PropertyInfoForClassInfo.ConverterBase; Debug.Assert(converter != null); // It should not be possible to have a null converter at this point. return converter; @@ -39,6 +39,8 @@ internal override bool OnTryRead( ref ReadStack state, [MaybeNullWhen(false)] out TCollection value) { + JsonClassInfo elementClassInfo = state.Current.JsonClassInfo.ElementClassInfo!; + if (state.UseFastPath) { // Fast path that avoids maintaining state variables and dealing with preserved references. @@ -50,8 +52,8 @@ internal override bool OnTryRead( CreateCollection(ref reader, ref state, options); - JsonConverter elementConverter = GetElementConverter(ref state); - if (elementConverter.CanUseDirectReadOrWrite) + JsonConverter elementConverter = GetElementConverter(elementClassInfo); + if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { // Fast path that avoids validation and extra indirection. while (true) @@ -154,7 +156,7 @@ internal override bool OnTryRead( if (state.Current.ObjectState < StackFrameObjectState.ReadElements) { - JsonConverter elementConverter = GetElementConverter(ref state); + JsonConverter elementConverter = GetElementConverter(elementClassInfo); // Process all elements. while (true) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListConverter.cs index 3f6516cdfbb2d0..606ddfa7ca3d78 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListConverter.cs @@ -49,36 +49,39 @@ protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStac protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) { - IEnumerator enumerator; - if (state.Current.CollectionEnumerator == null) + IList list = value; + + // Using an index is 2x faster than using an enumerator. + int index = state.Current.EnumeratorIndex; + JsonConverter elementConverter = GetElementConverter(ref state); + + if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { - enumerator = value.GetEnumerator(); - if (!enumerator.MoveNext()) + // Fast path that avoids validation and extra indirection. + for (; index < list.Count; index++) { - return true; + elementConverter.Write(writer, list[index], options); } } else { - enumerator = state.Current.CollectionEnumerator; - } - - JsonConverter converter = GetElementConverter(ref state); - do - { - if (ShouldFlush(writer, ref state)) + for (; index < list.Count; index++) { - state.Current.CollectionEnumerator = enumerator; - return false; - } + object? element = list[index]; + if (!elementConverter.TryWrite(writer, element, options, ref state)) + { + state.Current.EnumeratorIndex = index; + return false; + } - object? element = enumerator.Current; - if (!converter.TryWrite(writer, element, options, ref state)) - { - state.Current.CollectionEnumerator = enumerator; - return false; + if (ShouldFlush(writer, ref state)) + { + state.Current.EnumeratorIndex = ++index; + return false; + } } - } while (enumerator.MoveNext()); + } + return true; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfTKeyTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfTKeyTValueConverter.cs index 125f37534c1cc8..28c3c245b1c23b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfTKeyTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfTKeyTValueConverter.cs @@ -42,7 +42,7 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio } JsonConverter keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options); - JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo); + JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo.ElementClassInfo!); do { if (ShouldFlush(writer, ref state)) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfTKeyTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfTKeyTValueConverter.cs index 9f5e5d0506474c..ac9544d0528591 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfTKeyTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfTKeyTValueConverter.cs @@ -53,7 +53,7 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio } JsonConverter keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options); - JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo); + JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo.ElementClassInfo!); do { if (ShouldFlush(writer, ref state)) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ListOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ListOfTConverter.cs index 5004bc1ded4c6e..52362693749eb7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ListOfTConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ListOfTConverter.cs @@ -33,7 +33,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, int index = state.Current.EnumeratorIndex; JsonConverter elementConverter = GetElementConverter(ref state); - if (elementConverter.CanUseDirectReadOrWrite) + if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { // Fast path that avoids validation and extra indirection. for (; index < list.Count; index++) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/KeyValuePairConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/KeyValuePairConverter.cs index bee101fbfc1812..9f2de6a1897eaa 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/KeyValuePairConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/KeyValuePairConverter.cs @@ -86,6 +86,7 @@ protected override bool TryLookupConstructorParameter( Debug.Assert(jsonParameterInfo != null); argState.ParameterIndex++; argState.JsonParameterInfo = jsonParameterInfo; + state.Current.NumberHandling = jsonParameterInfo.NumberHandling; return true; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs index 25bbcb0dc103a5..c9f8f519529116 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs @@ -265,6 +265,7 @@ internal sealed override bool OnTryWrite( // Remember the current property for JsonPath support if an exception is thrown. state.Current.DeclaredJsonPropertyInfo = jsonPropertyInfo; + state.Current.NumberHandling = jsonPropertyInfo.NumberHandling; if (jsonPropertyInfo.ShouldSerialize) { @@ -321,6 +322,7 @@ internal sealed override bool OnTryWrite( { JsonPropertyInfo jsonPropertyInfo = propertyCacheArray![state.Current.EnumeratorIndex]; state.Current.DeclaredJsonPropertyInfo = jsonPropertyInfo; + state.Current.NumberHandling = jsonPropertyInfo.NumberHandling; if (jsonPropertyInfo.ShouldSerialize) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs index 120b687bdabc0d..f001c86198ed53 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs @@ -467,6 +467,9 @@ protected virtual bool TryLookupConstructorParameter( state.Current.JsonPropertyName = utf8PropertyName; state.Current.CtorArgumentState.JsonParameterInfo = jsonParameterInfo; + + state.Current.NumberHandling = jsonParameterInfo?.NumberHandling; + return jsonParameterInfo != null; } 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 774341c8e39ae6..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,6 +5,11 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class ByteConverter : JsonConverter { + public ByteConverter() + { + IsInternalConverterForNumberType = true; + } + public override byte Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return reader.GetByte(); @@ -24,5 +29,27 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, byte value, JsonSe { writer.WritePropertyName(value); } + + internal override byte ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) + { + if (reader.TokenType == JsonTokenType.String && (JsonNumberHandling.AllowReadingFromString & handling) != 0) + { + return reader.GetByteWithQuotes(); + } + + return reader.GetByte(); + } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, byte value, JsonNumberHandling handling) + { + if ((JsonNumberHandling.WriteAsString & handling) != 0) + { + writer.WriteNumberValueAsString(value); + } + else + { + writer.WriteNumberValue(value); + } + } } } 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 c78316a88f95fd..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,6 +5,11 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class DecimalConverter : JsonConverter { + public DecimalConverter() + { + IsInternalConverterForNumberType = true; + } + public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return reader.GetDecimal(); @@ -24,5 +29,28 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, decimal value, Jso { writer.WritePropertyName(value); } + + internal override decimal ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) + { + if (reader.TokenType == JsonTokenType.String && + (JsonNumberHandling.AllowReadingFromString & handling) != 0) + { + return reader.GetDecimalWithQuotes(); + } + + return reader.GetDecimal(); + } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, decimal value, JsonNumberHandling handling) + { + if ((JsonNumberHandling.WriteAsString & handling) != 0) + { + writer.WriteNumberValueAsString(value); + } + else + { + writer.WriteNumberValue(value); + } + } } } 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 641ad8e9991e8f..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,6 +5,11 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class DoubleConverter : JsonConverter { + public DoubleConverter() + { + IsInternalConverterForNumberType = true; + } + public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return reader.GetDouble(); @@ -24,5 +29,38 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, double value, Json { writer.WritePropertyName(value); } + + internal override double ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) + { + if (reader.TokenType == JsonTokenType.String) + { + if ((JsonNumberHandling.AllowReadingFromString & handling) != 0) + { + return reader.GetDoubleWithQuotes(); + } + else if ((JsonNumberHandling.AllowNamedFloatingPointLiterals & handling) != 0) + { + return reader.GetDoubleFloatingPointConstant(); + } + } + + return reader.GetDouble(); + } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, double value, JsonNumberHandling handling) + { + if ((JsonNumberHandling.WriteAsString & handling) != 0) + { + writer.WriteNumberValueAsString(value); + } + else if ((JsonNumberHandling.AllowNamedFloatingPointLiterals & handling) != 0) + { + writer.WriteFloatingPointConstant(value); + } + else + { + writer.WriteNumberValue(value); + } + } } } 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 c01ee00032cc55..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 @@ -1,12 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; - namespace System.Text.Json.Serialization.Converters { internal sealed class Int16Converter : JsonConverter { + public Int16Converter() + { + IsInternalConverterForNumberType = true; + } + public override short Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return reader.GetInt16(); @@ -27,5 +30,29 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, short value, JsonS { writer.WritePropertyName(value); } + + internal override short ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) + { + if (reader.TokenType == JsonTokenType.String && + (JsonNumberHandling.AllowReadingFromString & handling) != 0) + { + return reader.GetInt16WithQuotes(); + } + + return reader.GetInt16(); + } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, short value, JsonNumberHandling handling) + { + if ((JsonNumberHandling.WriteAsString & handling) != 0) + { + writer.WriteNumberValueAsString(value); + } + else + { + // For performance, lift up the writer implementation. + writer.WriteNumberValue((long)value); + } + } } } 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 5fc881a6b2e4d1..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,6 +5,11 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class Int32Converter : JsonConverter { + public Int32Converter() + { + IsInternalConverterForNumberType = true; + } + public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return reader.GetInt32(); @@ -25,5 +30,29 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, int value, JsonSer { writer.WritePropertyName(value); } + + internal override int ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) + { + if (reader.TokenType == JsonTokenType.String && + (JsonNumberHandling.AllowReadingFromString & handling) != 0) + { + return reader.GetInt32WithQuotes(); + } + + return reader.GetInt32(); + } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, int value, JsonNumberHandling handling) + { + if ((JsonNumberHandling.WriteAsString & handling) != 0) + { + writer.WriteNumberValueAsString(value); + } + else + { + // For performance, lift up the writer implementation. + writer.WriteNumberValue((long)value); + } + } } } 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 4aec3ff4e48564..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,6 +5,11 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class Int64Converter : JsonConverter { + public Int64Converter() + { + IsInternalConverterForNumberType = true; + } + public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return reader.GetInt64(); @@ -24,5 +29,28 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, long value, JsonSe { writer.WritePropertyName(value); } + + internal override long ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) + { + if (reader.TokenType == JsonTokenType.String && + (JsonNumberHandling.AllowReadingFromString & handling) != 0) + { + return reader.GetInt64WithQuotes(); + } + + return reader.GetInt64(); + } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, long value, JsonNumberHandling handling) + { + if ((JsonNumberHandling.WriteAsString & handling) != 0) + { + writer.WriteNumberValueAsString(value); + } + else + { + writer.WriteNumberValue(value); + } + } } } 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 e2c0ddcc43e01e..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 @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; + namespace System.Text.Json.Serialization.Converters { internal class NullableConverter : JsonConverter where T : struct @@ -12,6 +14,7 @@ internal class NullableConverter : JsonConverter where T : struct public NullableConverter(JsonConverter converter) { _converter = converter; + IsInternalConverterForNumberType = converter.IsInternalConverterForNumberType; } public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -40,5 +43,32 @@ public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOption _converter.Write(writer, value.Value, options); } } + + internal override T? ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling numberHandling) + { + // We do not check _converter.HandleNull, as the underlying struct cannot be null. + // A custom converter for some type T? can handle null. + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + T value = _converter.ReadNumberWithCustomHandling(ref reader, numberHandling); + return value; + } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, T? value, JsonNumberHandling handling) + { + if (!value.HasValue) + { + // We do not check _converter.HandleNull, as the underlying struct cannot be null. + // A custom converter for some type T? can handle null. + writer.WriteNullValue(); + } + else + { + _converter.WriteNumberWithCustomHandling(writer, value.Value, handling); + } + } } } 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 49ba6699b45301..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,6 +5,11 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class SByteConverter : JsonConverter { + public SByteConverter() + { + IsInternalConverterForNumberType = true; + } + public override sbyte Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return reader.GetSByte(); @@ -24,5 +29,28 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, sbyte value, JsonS { writer.WritePropertyName(value); } + + internal override sbyte ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) + { + if (reader.TokenType == JsonTokenType.String && + (JsonNumberHandling.AllowReadingFromString & handling) != 0) + { + return reader.GetSByteWithQuotes(); + } + + return reader.GetSByte(); + } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, sbyte value, JsonNumberHandling handling) + { + if ((JsonNumberHandling.WriteAsString & handling) != 0) + { + writer.WriteNumberValueAsString(value); + } + else + { + writer.WriteNumberValue(value); + } + } } } 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 e2f7d76761e003..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,6 +5,12 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class SingleConverter : JsonConverter { + + public SingleConverter() + { + IsInternalConverterForNumberType = true; + } + public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return reader.GetSingle(); @@ -24,5 +30,38 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, float value, JsonS { writer.WritePropertyName(value); } + + internal override float ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) + { + if (reader.TokenType == JsonTokenType.String) + { + if ((JsonNumberHandling.AllowReadingFromString & handling) != 0) + { + return reader.GetSingleWithQuotes(); + } + else if ((JsonNumberHandling.AllowNamedFloatingPointLiterals & handling) != 0) + { + return reader.GetSingleFloatingPointConstant(); + } + } + + return reader.GetSingle(); + } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, float value, JsonNumberHandling handling) + { + if ((JsonNumberHandling.WriteAsString & handling) != 0) + { + writer.WriteNumberValueAsString(value); + } + else if ((JsonNumberHandling.AllowNamedFloatingPointLiterals & handling) != 0) + { + writer.WriteFloatingPointConstant(value); + } + else + { + writer.WriteNumberValue(value); + } + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/StringConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/StringConverter.cs index f03d9ac1cba129..08972856b698c6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/StringConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/StringConverter.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; - namespace System.Text.Json.Serialization.Converters { internal sealed class StringConverter : JsonConverter 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 db2af0a5f2e3ba..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,6 +5,11 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class UInt16Converter : JsonConverter { + public UInt16Converter() + { + IsInternalConverterForNumberType = true; + } + public override ushort Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return reader.GetUInt16(); @@ -25,5 +30,29 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, ushort value, Json { writer.WritePropertyName(value); } + + internal override ushort ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) + { + if (reader.TokenType == JsonTokenType.String && + (JsonNumberHandling.AllowReadingFromString & handling) != 0) + { + return reader.GetUInt16WithQuotes(); + } + + return reader.GetUInt16(); + } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, ushort value, JsonNumberHandling handling) + { + if ((JsonNumberHandling.WriteAsString & handling) != 0) + { + writer.WriteNumberValueAsString(value); + } + else + { + // For performance, lift up the writer implementation. + writer.WriteNumberValue((long)value); + } + } } } 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 131e6feb1b0544..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,6 +5,11 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class UInt32Converter : JsonConverter { + public UInt32Converter() + { + IsInternalConverterForNumberType = true; + } + public override uint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return reader.GetUInt32(); @@ -25,5 +30,29 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, uint value, JsonSe { writer.WritePropertyName(value); } + + internal override uint ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) + { + if (reader.TokenType == JsonTokenType.String && + (JsonNumberHandling.AllowReadingFromString & handling) != 0) + { + return reader.GetUInt32WithQuotes(); + } + + return reader.GetUInt32(); + } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, uint value, JsonNumberHandling handling) + { + if ((JsonNumberHandling.WriteAsString & handling) != 0) + { + writer.WriteNumberValueAsString(value); + } + else + { + // For performance, lift up the writer implementation. + writer.WriteNumberValue((ulong)value); + } + } } } 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 95f9025c39812d..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,6 +5,11 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class UInt64Converter : JsonConverter { + public UInt64Converter() + { + IsInternalConverterForNumberType = true; + } + public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return reader.GetUInt64(); @@ -24,5 +29,28 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, ulong value, JsonS { writer.WritePropertyName(value); } + + internal override ulong ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) + { + if (reader.TokenType == JsonTokenType.String && + (JsonNumberHandling.AllowReadingFromString & handling) != 0) + { + return reader.GetUInt64WithQuotes(); + } + + return reader.GetUInt64(); + } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, ulong value, JsonNumberHandling handling) + { + if ((JsonNumberHandling.WriteAsString & handling) != 0) + { + writer.WriteNumberValueAsString(value); + } + else + { + writer.WriteNumberValue(value); + } + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.Cache.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.Cache.cs index 785abae8a60793..7b01b16fb47725 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.Cache.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.Cache.cs @@ -52,10 +52,14 @@ internal sealed partial class JsonClassInfo // Use an array (instead of List) for highest performance. private volatile PropertyRef[]? _propertyRefsSorted; - public static JsonPropertyInfo AddProperty(MemberInfo memberInfo, Type memberType, Type parentClassType, JsonSerializerOptions options) + public static JsonPropertyInfo AddProperty( + MemberInfo memberInfo, + Type memberType, + Type parentClassType, + JsonNumberHandling? parentTypeNumberHandling, + JsonSerializerOptions options) { JsonIgnoreCondition? ignoreCondition = JsonPropertyInfo.GetAttribute(memberInfo)?.Condition; - if (ignoreCondition == JsonIgnoreCondition.Always) { return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(memberInfo, options); @@ -75,6 +79,7 @@ public static JsonPropertyInfo AddProperty(MemberInfo memberInfo, Type memberTyp parentClassType, converter, options, + parentTypeNumberHandling, ignoreCondition); } @@ -85,6 +90,7 @@ internal static JsonPropertyInfo CreateProperty( Type parentClassType, JsonConverter converter, JsonSerializerOptions options, + JsonNumberHandling? parentTypeNumberHandling = null, JsonIgnoreCondition? ignoreCondition = null) { // Create the JsonPropertyInfo instance. @@ -98,6 +104,7 @@ internal static JsonPropertyInfo CreateProperty( memberInfo, converter, ignoreCondition, + parentTypeNumberHandling, options); return jsonPropertyInfo; @@ -113,13 +120,16 @@ internal static JsonPropertyInfo CreatePropertyInfoForClassInfo( JsonConverter converter, JsonSerializerOptions options) { + JsonNumberHandling? numberHandling = GetNumberHandlingForType(declaredPropertyType); + JsonPropertyInfo jsonPropertyInfo = CreateProperty( declaredPropertyType: declaredPropertyType, runtimePropertyType: runtimePropertyType, memberInfo: null, // Not a real property so this is null. parentClassType: JsonClassInfo.ObjectType, // a dummy value (not used) converter: converter, - options); + options, + parentTypeNumberHandling: numberHandling); Debug.Assert(jsonPropertyInfo.IsForClassInfo); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs index 2ab746903ec803..28b49558108de6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs @@ -89,6 +89,8 @@ public JsonClassInfo(Type type, JsonSerializerOptions options) Options); ClassType = converter.ClassType; + JsonNumberHandling? typeNumberHandling = GetNumberHandlingForType(Type); + PropertyInfoForClassInfo = CreatePropertyInfoForClassInfo(Type, runtimeType, converter, Options); switch (ClassType) @@ -124,7 +126,7 @@ public JsonClassInfo(Type type, JsonSerializerOptions options) if (propertyInfo.GetMethod?.IsPublic == true || propertyInfo.SetMethod?.IsPublic == true) { - CacheMember(currentType, propertyInfo.PropertyType, propertyInfo, cache, ref ignoredMembers); + CacheMember(currentType, propertyInfo.PropertyType, propertyInfo, typeNumberHandling, cache, ref ignoredMembers); } else { @@ -150,7 +152,7 @@ public JsonClassInfo(Type type, JsonSerializerOptions options) { if (hasJsonInclude || Options.IncludeFields) { - CacheMember(currentType, fieldInfo.FieldType, fieldInfo, cache, ref ignoredMembers); + CacheMember(currentType, fieldInfo.FieldType, fieldInfo, typeNumberHandling, cache, ref ignoredMembers); } } else @@ -225,10 +227,11 @@ private void CacheMember( Type declaringType, Type memberType, MemberInfo memberInfo, + JsonNumberHandling? typeNumberHandling, Dictionary cache, ref Dictionary? ignoredMembers) { - JsonPropertyInfo jsonPropertyInfo = AddProperty(memberInfo, memberType, declaringType, Options); + JsonPropertyInfo jsonPropertyInfo = AddProperty(memberInfo, memberType, declaringType, typeNumberHandling, Options); Debug.Assert(jsonPropertyInfo.NameAsString != null); string memberName = memberInfo.Name; @@ -570,5 +573,13 @@ private static bool IsByRefLike(Type type) return false; #endif } + + private static JsonNumberHandling? GetNumberHandlingForType(Type type) + { + var numberHandlingAttribute = + (JsonNumberHandlingAttribute?)JsonSerializerOptions.GetAttributeThatCanHaveMultiple(type, typeof(JsonNumberHandlingAttribute)); + + return numberHandlingAttribute?.Handling; + } } } 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 b2c34e14599a1d..4526db14a36a23 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 @@ -44,6 +44,16 @@ internal JsonConverter() { } /// internal bool IsValueType { get; set; } + /// + /// Whether the converter is built-in. + /// + internal bool IsInternalConverter { get; set; } + + /// + /// Whether the converter is built-in and handles a number type. + /// + internal bool IsInternalConverterForNumberType; + /// /// Loosely-typed ReadCore() that forwards to strongly-typed ReadCore(). /// @@ -76,7 +86,7 @@ internal bool ShouldFlush(Utf8JsonWriter writer, ref WriteStack state) internal abstract void WriteWithQuotesAsObject(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state); // Whether a type (ClassType.Object) is deserialized using a parameterized constructor. - internal virtual bool ConstructorIsParameterized => false; + internal virtual bool ConstructorIsParameterized { get; } internal ConstructorInfo? ConstructorInfo { get; set; } 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 52dba1df4bb4dc..dfd81ffd871b27 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 @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; namespace System.Text.Json.Serialization { @@ -69,11 +68,6 @@ internal override sealed JsonParameterInfo CreateJsonParameterInfo() /// internal bool CanBeNull { get; } - /// - /// Is the converter built-in. - /// - internal bool IsInternalConverter { get; set; } - // This non-generic API is sealed as it just forwards to the generic version. internal sealed override bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state) { @@ -131,7 +125,14 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali // For performance, only perform validation on internal converters on debug builds. if (IsInternalConverter) { - value = Read(ref reader, typeToConvert, options); + if (IsInternalConverterForNumberType && state.Current.NumberHandling != null) + { + value = ReadNumberWithCustomHandling(ref reader, state.Current.NumberHandling.Value); + } + else + { + value = Read(ref reader, typeToConvert, options); + } } else #endif @@ -140,7 +141,15 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali int originalPropertyDepth = reader.CurrentDepth; long originalPropertyBytesConsumed = reader.BytesConsumed; - value = Read(ref reader, typeToConvert, options); + if (IsInternalConverterForNumberType && state.Current.NumberHandling != null) + { + value = ReadNumberWithCustomHandling(ref reader, state.Current.NumberHandling.Value); + } + else + { + value = Read(ref reader, typeToConvert, options); + } + VerifyRead( originalPropertyTokenType, originalPropertyDepth, @@ -309,7 +318,15 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions int originalPropertyDepth = writer.CurrentDepth; - Write(writer, value, options); + if (IsInternalConverterForNumberType && state.Current.NumberHandling != null) + { + WriteNumberWithCustomHandling(writer, value, state.Current.NumberHandling.Value); + } + else + { + Write(writer, value, options); + } + VerifyWrite(originalPropertyDepth, writer); return true; } @@ -452,5 +469,11 @@ internal virtual void WriteWithQuotes(Utf8JsonWriter writer, [DisallowNull] T va internal sealed override void WriteWithQuotesAsObject(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state) => WriteWithQuotes(writer, (T)value, options, ref state); + + internal virtual T ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) + => throw new InvalidOperationException(); + + internal virtual void WriteNumberWithCustomHandling(Utf8JsonWriter writer, T value, JsonNumberHandling handling) + => throw new InvalidOperationException(); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberHandling.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberHandling.cs new file mode 100644 index 00000000000000..09be0bfc94c81e --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberHandling.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization +{ + /// + /// Determines how handles numbers when serializing and deserializing. + /// + [Flags] + public enum JsonNumberHandling + { + /// + /// Numbers will only be read from tokens and will only be written as JSON numbers (without quotes). + /// + Strict = 0x0, + /// + /// Numbers can be read from tokens. + /// Does not prevent numbers from being read from token. + /// + AllowReadingFromString = 0x1, + /// + /// Numbers will be written as JSON strings (with quotes), not as JSON numbers. + /// + WriteAsString = 0x2, + /// + /// The "NaN", "Infinity", and "-Infinity" tokens can be read as floating-point constants, + /// and the , , and + /// values will be written as their corresponding JSON string representations. + /// + AllowNamedFloatingPointLiterals = 0x4 + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfo.cs index 0ccdbeeafda1ef..4687107f7f82be 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfo.cs @@ -27,6 +27,8 @@ internal abstract class JsonParameterInfo // The name of the parameter as UTF-8 bytes. public byte[] NameAsUtf8Bytes { get; private set; } = null!; + public JsonNumberHandling? NumberHandling { get; private set; } + // The zero-based position of the parameter in the formal parameter list. public int Position { get; private set; } @@ -63,6 +65,7 @@ public virtual void Initialize( ShouldDeserialize = true; ConverterBase = matchingProperty.ConverterBase; IgnoreDefaultValuesOnRead = matchingProperty.IgnoreDefaultValuesOnRead; + NumberHandling = matchingProperty.NumberHandling; } // Create a parameter that is ignored at run-time. It uses the same type (typeof(sbyte)) to help 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 38adc919f9ed04..10b92e26fac964 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 @@ -50,6 +50,14 @@ public static JsonPropertyInfo CreateIgnoredPropertyPlaceholder(MemberInfo membe public Type DeclaredPropertyType { get; private set; } = null!; + public virtual void GetPolicies(JsonIgnoreCondition? ignoreCondition, JsonNumberHandling? parentTypeNumberHandling, bool defaultValueIsNull) + { + DetermineSerializationCapabilities(ignoreCondition); + DeterminePropertyName(); + DetermineIgnoreCondition(ignoreCondition, defaultValueIsNull); + DetermineNumberHandling(parentTypeNumberHandling); + } + private void DeterminePropertyName() { if (MemberInfo == null) @@ -174,6 +182,56 @@ private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition, bool #pragma warning restore CS0618 // IgnoreNullValues is obsolete } + private void DetermineNumberHandling(JsonNumberHandling? parentTypeNumberHandling) + { + if (IsForClassInfo) + { + if (parentTypeNumberHandling != null && !ConverterBase.IsInternalConverter) + { + 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) + { + NumberHandling = Options.NumberHandling; + } + } + else + { + JsonNumberHandling? handling = null; + + // Priority 1: Get handling from attribute on property or field. + if (MemberInfo != null) + { + JsonNumberHandlingAttribute? attribute = GetAttribute(MemberInfo); + + if (attribute != null && + !ConverterBase.IsInternalConverterForNumberType && + ((ClassType.Enumerable | ClassType.Dictionary) & ClassType) == 0) + { + ThrowHelper.ThrowInvalidOperationException_NumberHandlingOnPropertyInvalid(this); + } + + 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) + { + handling = Options.NumberHandling; + } + + NumberHandling = handling; + } + } + public static TAttribute? GetAttribute(MemberInfo memberInfo) where TAttribute : Attribute { return (TAttribute?)memberInfo.GetCustomAttribute(typeof(TAttribute), inherit: false); @@ -182,13 +240,6 @@ private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition, bool public abstract bool GetMemberAndWriteJson(object obj, ref WriteStack state, Utf8JsonWriter writer); public abstract bool GetMemberAndWriteJsonExtensionData(object obj, ref WriteStack state, Utf8JsonWriter writer); - public virtual void GetPolicies(JsonIgnoreCondition? ignoreCondition, bool defaultValueIsNull) - { - DetermineSerializationCapabilities(ignoreCondition); - DeterminePropertyName(); - DetermineIgnoreCondition(ignoreCondition, defaultValueIsNull); - } - public abstract object? GetValueAsObject(object obj); public bool HasGetter { get; set; } @@ -202,6 +253,7 @@ public virtual void Initialize( MemberInfo? memberInfo, JsonConverter converter, JsonIgnoreCondition? ignoreCondition, + JsonNumberHandling? parentTypeNumberHandling, JsonSerializerOptions options) { Debug.Assert(converter != null); @@ -344,5 +396,7 @@ public JsonClassInfo RuntimeClassInfo public bool ShouldSerialize { get; private set; } public bool ShouldDeserialize { get; private set; } public bool IsIgnored { get; private set; } + + public JsonNumberHandling? NumberHandling { get; private set; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs index 83c5ee116372de..85962cdb0bf515 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs @@ -28,6 +28,7 @@ public override void Initialize( MemberInfo? memberInfo, JsonConverter converter, JsonIgnoreCondition? ignoreCondition, + JsonNumberHandling? parentTypeNumberHandling, JsonSerializerOptions options) { base.Initialize( @@ -38,6 +39,7 @@ public override void Initialize( memberInfo, converter, ignoreCondition, + parentTypeNumberHandling, options); switch (memberInfo) @@ -89,7 +91,7 @@ public override void Initialize( } } - GetPolicies(ignoreCondition, defaultValueIsNull: Converter.CanBeNull); + GetPolicies(ignoreCondition, parentTypeNumberHandling, defaultValueIsNull: Converter.CanBeNull); } public override JsonConverter ConverterBase @@ -209,13 +211,13 @@ public override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref U success = true; } - else if (Converter.CanUseDirectReadOrWrite) + else if (Converter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { if (!isNullToken || !IgnoreDefaultValuesOnRead || !Converter.CanBeNull) { // Optimize for internal converters by avoiding the extra call to TryRead. - T fastvalue = Converter.Read(ref reader, RuntimePropertyType!, Options); - Set!(obj, fastvalue!); + T fastValue = Converter.Read(ref reader, RuntimePropertyType!, Options); + Set!(obj, fastValue!); } success = true; @@ -253,7 +255,7 @@ public override bool ReadJsonAsObject(ref ReadStack state, ref Utf8JsonReader re else { // Optimize for internal converters by avoiding the extra call to TryRead. - if (Converter.CanUseDirectReadOrWrite) + if (Converter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { value = Converter.Read(ref reader, RuntimePropertyType!, Options); success = true; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs index 04bb332121f9fa..17a0669b2a4ab2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs @@ -55,6 +55,7 @@ internal static JsonPropertyInfo LookupProperty( } state.Current.JsonPropertyInfo = jsonPropertyInfo; + state.Current.NumberHandling = jsonPropertyInfo.NumberHandling; return jsonPropertyInfo; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs index 4e7946872454c4..8b01c199e6e470 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text.Json.Serialization; namespace System.Text.Json @@ -35,5 +36,11 @@ private static TValue ReadCore(JsonConverter jsonConverter, ref Utf8Json Debug.Assert(value == null || value is TValue); return (TValue)value!; } + + internal static bool IsValidNumberHandlingValue(JsonNumberHandling handling) + { + int handlingValue = (int)handling; + return handlingValue >= 0 && handlingValue <= 7; + } } } 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 0796280c369dcd..860b49315a404d 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 @@ -325,7 +325,7 @@ private JsonConverter GetConverterFromAttribute(JsonConverterAttribute converter return GetAttributeThatCanHaveMultiple(attributeType, classType, memberInfo, attributes); } - private static Attribute? GetAttributeThatCanHaveMultiple(Type classType, Type attributeType) + internal static Attribute? GetAttributeThatCanHaveMultiple(Type classType, Type attributeType) { object[] attributes = classType.GetCustomAttributes(attributeType, inherit: false); return GetAttributeThatCanHaveMultiple(attributeType, classType, null, attributes); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index e26eb9e0176aab..4217f61a61fc84 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -33,6 +33,7 @@ public sealed partial class JsonSerializerOptions private ReferenceHandler? _referenceHandler; private JavaScriptEncoder? _encoder; private JsonIgnoreCondition _defaultIgnoreCondition; + private JsonNumberHandling _numberHandling; private int _defaultBufferSize = BufferSizeDefault; private int _maxDepth; @@ -74,6 +75,7 @@ public JsonSerializerOptions(JsonSerializerOptions options) _referenceHandler = options._referenceHandler; _encoder = options._encoder; _defaultIgnoreCondition = options._defaultIgnoreCondition; + _numberHandling = options._numberHandling; _defaultBufferSize = options._defaultBufferSize; _maxDepth = options._maxDepth; @@ -262,6 +264,27 @@ public JsonIgnoreCondition DefaultIgnoreCondition } } + /// + /// Specifies how number types should be handled when serializing or deserializing. + /// + /// + /// Thrown if this property is set after serialization or deserialization has occurred. + /// + public JsonNumberHandling NumberHandling + { + get => _numberHandling; + set + { + VerifyMutable(); + + if (!JsonSerializer.IsValidNumberHandlingValue(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _numberHandling = value; + } + } + /// /// Determines whether read-only properties are ignored during serialization. /// A property is read-only if it contains a public getter but not a public setter. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs index c3b1ba7c38a74e..ac03e3b6adba67 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs @@ -87,6 +87,8 @@ public void Initialize(Type type, JsonSerializerOptions options, bool supportCon // The initial JsonPropertyInfo will be used to obtain the converter. Current.JsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo; + Current.NumberHandling = Current.JsonPropertyInfo.NumberHandling; + bool preserveReferences = options.ReferenceHandler != null; if (preserveReferences) { @@ -109,6 +111,8 @@ public void Push() else { JsonClassInfo jsonClassInfo; + JsonNumberHandling? numberHandling = Current.NumberHandling; + if (Current.JsonClassInfo.ClassType == ClassType.Object) { if (Current.JsonPropertyInfo != null) @@ -120,13 +124,14 @@ public void Push() jsonClassInfo = Current.CtorArgumentState!.JsonParameterInfo!.RuntimeClassInfo; } } - else if ((Current.JsonClassInfo.ClassType & (ClassType.Value | ClassType.NewValue)) != 0) + else if (((ClassType.Value | ClassType.NewValue) & Current.JsonClassInfo.ClassType) != 0) { // Although ClassType.Value doesn't push, a custom custom converter may re-enter serialization. jsonClassInfo = Current.JsonPropertyInfo!.RuntimeClassInfo; } else { + Debug.Assert(((ClassType.Enumerable | ClassType.Dictionary) & Current.JsonClassInfo.ClassType) != 0); jsonClassInfo = Current.JsonClassInfo.ElementClassInfo!; } @@ -135,6 +140,8 @@ public void Push() Current.JsonClassInfo = jsonClassInfo; Current.JsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo; + // Allow number handling on property to win over handling on type. + Current.NumberHandling = numberHandling ?? Current.JsonPropertyInfo.NumberHandling; } } else if (_continuationCount == 1) @@ -159,7 +166,7 @@ public void Push() } } - SetConstrutorArgumentState(); + SetConstructorArgumentState(); } public void Pop(bool success) @@ -210,7 +217,7 @@ public void Pop(bool success) Current = _previous[--_count -1]; } - SetConstrutorArgumentState(); + SetConstructorArgumentState(); } // Return a JSONPath using simple dot-notation when possible. When special characters are present, bracket-notation is used: @@ -328,7 +335,7 @@ static void AppendPropertyName(StringBuilder sb, string? propertyName) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetConstrutorArgumentState() + private void SetConstructorArgumentState() { if (Current.JsonClassInfo.ParameterCount > 0) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs index 650c1c27c8c14a..75dba157be5eeb 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Text.Json.Serialization; namespace System.Text.Json { @@ -46,6 +47,9 @@ internal struct ReadStackFrame public int CtorArgumentStateIndex; public ArgumentState? CtorArgumentState; + // Whether to use custom number handling. + public JsonNumberHandling? NumberHandling; + public void EndConstructorParameter() { CtorArgumentState!.JsonParameterInfo = null; @@ -62,6 +66,7 @@ public void EndProperty() MetadataId = null; // No need to clear these since they are overwritten each time: + // NumberHandling // UseExtensionProperty } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs index ec90b0c75e0218..e3378711eb1eb1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs @@ -68,12 +68,10 @@ private void AddCurrent() public JsonConverter Initialize(Type type, JsonSerializerOptions options, bool supportContinuation) { JsonClassInfo jsonClassInfo = options.GetOrAddClassForRootType(type); - Current.JsonClassInfo = jsonClassInfo; - if ((jsonClassInfo.ClassType & (ClassType.Enumerable | ClassType.Dictionary)) == 0) - { - Current.DeclaredJsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo; - } + Current.JsonClassInfo = jsonClassInfo; + Current.DeclaredJsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo; + Current.NumberHandling = Current.DeclaredJsonPropertyInfo.NumberHandling; if (options.ReferenceHandler != null) { @@ -97,12 +95,15 @@ public void Push() else { JsonClassInfo jsonClassInfo = Current.GetPolymorphicJsonPropertyInfo().RuntimeClassInfo; + JsonNumberHandling? numberHandling = Current.NumberHandling; AddCurrent(); Current.Reset(); Current.JsonClassInfo = jsonClassInfo; Current.DeclaredJsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo; + // Allow number handling on property to win over handling on type. + Current.NumberHandling = numberHandling ?? Current.DeclaredJsonPropertyInfo.NumberHandling; } } else if (_continuationCount == 1) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs index 9689568809b24d..d5a671dd4794f3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs @@ -68,6 +68,9 @@ internal struct WriteStackFrame /// public JsonPropertyInfo? PolymorphicJsonPropertyInfo; + // Whether to use custom number handling. + public JsonNumberHandling? NumberHandling; + public void EndDictionaryElement() { PropertyState = StackFramePropertyState.None; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index 2194ab03fab145..58ea337c53bb8f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -224,6 +224,28 @@ public static void ThrowInvalidOperationException_IgnoreConditionOnValueTypeInva throw new InvalidOperationException(SR.Format(SR.IgnoreConditionOnValueTypeInvalid, memberInfo.Name, memberInfo.DeclaringType)); } + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowInvalidOperationException_NumberHandlingOnPropertyInvalid(JsonPropertyInfo jsonPropertyInfo) + { + MemberInfo? memberInfo = jsonPropertyInfo.MemberInfo; + + if (!jsonPropertyInfo.ConverterBase.IsInternalConverter) + { + throw new InvalidOperationException(SR.Format( + SR.NumberHandlingConverterMustBeBuiltIn, + jsonPropertyInfo.ConverterBase.GetType(), + jsonPropertyInfo.IsForClassInfo ? jsonPropertyInfo.DeclaredPropertyType : memberInfo!.DeclaringType)); + } + + // This exception is only thrown for object properties. + Debug.Assert(!jsonPropertyInfo.IsForClassInfo && memberInfo != null); + throw new InvalidOperationException(SR.Format( + SR.NumberHandlingOnPropertyTypeMustBeNumberOrCollection, + memberInfo.Name, + memberInfo.DeclaringType)); + } + [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotHonored( diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Decimal.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Decimal.cs index ca5b059be50bbd..a30af2e6546ad1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Decimal.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Decimal.cs @@ -93,5 +93,13 @@ private void WriteNumberValueIndented(decimal value) Debug.Assert(result); BytesPending += bytesWritten; } + + internal void WriteNumberValueAsString(decimal value) + { + Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatDecimalLength]; + bool result = Utf8Formatter.TryFormat(value, utf8Number, out int bytesWritten); + Debug.Assert(result); + WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesWritten)); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs index cf8732bc44ec8a..f8a46a31468ca9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs @@ -143,5 +143,33 @@ private static bool TryFormatDouble(double value, Span destination, out in } #endif } + + internal void WriteNumberValueAsString(double value) + { + Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatDoubleLength]; + bool result = TryFormatDouble(value, utf8Number, out int bytesWritten); + Debug.Assert(result); + WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesWritten)); + } + + internal void WriteFloatingPointConstant(double value) + { + if (double.IsNaN(value)) + { + WriteNumberValueAsStringUnescaped(JsonConstants.NaNValue); + } + else if (double.IsPositiveInfinity(value)) + { + WriteNumberValueAsStringUnescaped(JsonConstants.PositiveInfinityValue); + } + else if (double.IsNegativeInfinity(value)) + { + WriteNumberValueAsStringUnescaped(JsonConstants.NegativeInfinityValue); + } + else + { + WriteNumberValue(value); + } + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs index f120a09373c479..2f046d872f6972 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs @@ -143,5 +143,33 @@ private static bool TryFormatSingle(float value, Span destination, out int } #endif } + + internal void WriteNumberValueAsString(float value) + { + Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatSingleLength]; + bool result = TryFormatSingle(value, utf8Number, out int bytesWritten); + Debug.Assert(result); + WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesWritten)); + } + + internal void WriteFloatingPointConstant(float value) + { + if (float.IsNaN(value)) + { + WriteNumberValueAsStringUnescaped(JsonConstants.NaNValue); + } + else if (float.IsPositiveInfinity(value)) + { + WriteNumberValueAsStringUnescaped(JsonConstants.PositiveInfinityValue); + } + else if (float.IsNegativeInfinity(value)) + { + WriteNumberValueAsStringUnescaped(JsonConstants.NegativeInfinityValue); + } + else + { + WriteNumberValue(value); + } + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.SignedNumber.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.SignedNumber.cs index 957573ddb7a7f4..2d6120a6a87680 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.SignedNumber.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.SignedNumber.cs @@ -106,5 +106,13 @@ private void WriteNumberValueIndented(long value) Debug.Assert(result); BytesPending += bytesWritten; } + + internal void WriteNumberValueAsString(long value) + { + Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatInt64Length]; + bool result = Utf8Formatter.TryFormat(value, utf8Number, out int bytesWritten); + Debug.Assert(result); + WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesWritten)); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.String.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.String.cs index c86a8af22f1628..15cb0b8d1af5ec 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.String.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.String.cs @@ -349,5 +349,19 @@ private void WriteStringEscapeValue(ReadOnlySpan utf8Value, int firstEscap ArrayPool.Shared.Return(valueArray); } } + + /// + /// Writes a number as a JSON string. The string value is not escaped. + /// + /// + internal void WriteNumberValueAsStringUnescaped(ReadOnlySpan utf8Value) + { + // The value has been validated prior to calling this method. + + WriteStringByOptions(utf8Value); + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.UnsignedNumber.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.UnsignedNumber.cs index 2c15441d432593..d3cf96947db468 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.UnsignedNumber.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.UnsignedNumber.cs @@ -108,5 +108,13 @@ private void WriteNumberValueIndented(ulong value) Debug.Assert(result); BytesPending += bytesWritten; } + + internal void WriteNumberValueAsString(ulong value) + { + Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatUInt64Length]; + bool result = Utf8Formatter.TryFormat(value, utf8Number, out int bytesWritten); + Debug.Assert(result); + WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesWritten)); + } } } diff --git a/src/libraries/System.Text.Json/tests/JsonNumberTestData.cs b/src/libraries/System.Text.Json/tests/JsonNumberTestData.cs index d07064a3e487be..384488485c9806 100644 --- a/src/libraries/System.Text.Json/tests/JsonNumberTestData.cs +++ b/src/libraries/System.Text.Json/tests/JsonNumberTestData.cs @@ -3,8 +3,7 @@ using System.Collections.Generic; using System.Globalization; -using System.IO; -using Newtonsoft.Json; +using System.Linq; namespace System.Text.Json.Tests { @@ -21,6 +20,19 @@ internal class JsonNumberTestData public static List Floats { get; set; } public static List Doubles { get; set; } public static List Decimals { get; set; } + + public static List NullableBytes { get; set; } + public static List NullableSBytes { get; set; } + public static List NullableShorts { get; set; } + public static List NullableInts { get; set; } + public static List NullableLongs { get; set; } + public static List NullableUShorts { get; set; } + public static List NullableUInts { get; set; } + public static List NullableULongs { get; set; } + public static List NullableFloats { get; set; } + public static List NullableDoubles { get; set; } + public static List NullableDecimals { get; set; } + public static byte[] JsonData { get; set; } static JsonNumberTestData() @@ -295,6 +307,19 @@ static JsonNumberTestData() builder.Append("\"intEnd\": 0}"); #endregion + // Make collections of nullable numbers. + NullableBytes = new List(Bytes.Select(num => (byte?)num)); + NullableSBytes = new List(SBytes.Select(num => (sbyte?)num)); + NullableShorts = new List(Shorts.Select(num => (short?)num)); + NullableInts = new List(Ints.Select(num => (int?)num)); + NullableLongs = new List(Longs.Select(num => (long?)num)); + NullableUShorts = new List(UShorts.Select(num => (ushort?)num)); + NullableUInts = new List(UInts.Select(num => (uint?)num)); + NullableULongs = new List(ULongs.Select(num => (ulong?)num)); + NullableFloats = new List(Floats.Select(num => (float?)num)); + NullableDoubles = new List(Doubles.Select(num => (double?)num)); + NullableDecimals = new List(Decimals.Select(num => (decimal?)num)); + string jsonString = builder.ToString(); JsonData = Encoding.UTF8.GetBytes(jsonString); } diff --git a/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs new file mode 100644 index 00000000000000..60acb54094d6f9 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs @@ -0,0 +1,1414 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Text.Encodings.Web; +using System.Text.Json.Tests; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static partial class NumberHandlingTests + { + private static readonly JsonSerializerOptions s_optionReadFromStr = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.AllowReadingFromString + }; + + private static readonly JsonSerializerOptions s_optionWriteAsStr = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.WriteAsString + }; + + private static readonly JsonSerializerOptions s_optionReadAndWriteFromStr = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString + }; + + private static readonly JsonSerializerOptions s_optionsAllowFloatConstants = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals + }; + + private static readonly JsonSerializerOptions s_optionReadFromStrAllowFloatConstants = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals + }; + + private static readonly JsonSerializerOptions s_optionWriteAsStrAllowFloatConstants = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowNamedFloatingPointLiterals + }; + + [Fact] + public static void Number_AsRootType_RoundTrip() + { + RunAsRootTypeTest(JsonNumberTestData.Bytes); + RunAsRootTypeTest(JsonNumberTestData.SBytes); + RunAsRootTypeTest(JsonNumberTestData.Shorts); + RunAsRootTypeTest(JsonNumberTestData.Ints); + RunAsRootTypeTest(JsonNumberTestData.Longs); + RunAsRootTypeTest(JsonNumberTestData.UShorts); + RunAsRootTypeTest(JsonNumberTestData.UInts); + RunAsRootTypeTest(JsonNumberTestData.ULongs); + RunAsRootTypeTest(JsonNumberTestData.Floats); + RunAsRootTypeTest(JsonNumberTestData.Doubles); + RunAsRootTypeTest(JsonNumberTestData.Decimals); + RunAsRootTypeTest(JsonNumberTestData.NullableBytes); + RunAsRootTypeTest(JsonNumberTestData.NullableSBytes); + RunAsRootTypeTest(JsonNumberTestData.NullableShorts); + RunAsRootTypeTest(JsonNumberTestData.NullableInts); + RunAsRootTypeTest(JsonNumberTestData.NullableLongs); + RunAsRootTypeTest(JsonNumberTestData.NullableUShorts); + RunAsRootTypeTest(JsonNumberTestData.NullableUInts); + RunAsRootTypeTest(JsonNumberTestData.NullableULongs); + RunAsRootTypeTest(JsonNumberTestData.NullableFloats); + RunAsRootTypeTest(JsonNumberTestData.NullableDoubles); + RunAsRootTypeTest(JsonNumberTestData.NullableDecimals); + } + + private static void RunAsRootTypeTest(List numbers) + { + foreach (T number in numbers) + { + string numberAsString = GetNumberAsString(number); + string json = $"{numberAsString}"; + string jsonWithNumberAsString = @$"""{numberAsString}"""; + PerformAsRootTypeSerialization(number, json, jsonWithNumberAsString); + } + } + + private static string GetNumberAsString(T number) + { + return number switch + { + double @double => @double.ToString(JsonTestHelper.DoubleFormatString, CultureInfo.InvariantCulture), + float @float => @float.ToString(JsonTestHelper.SingleFormatString, CultureInfo.InvariantCulture), + decimal @decimal => @decimal.ToString(CultureInfo.InvariantCulture), + _ => number.ToString() + }; + } + + private static void PerformAsRootTypeSerialization(T number, string jsonWithNumberAsNumber, string jsonWithNumberAsString) + { + // Option: read from string + + // Deserialize + Assert.Equal(number, JsonSerializer.Deserialize(jsonWithNumberAsNumber, s_optionReadFromStr)); + Assert.Equal(number, JsonSerializer.Deserialize(jsonWithNumberAsString, s_optionReadFromStr)); + + // Serialize + Assert.Equal(jsonWithNumberAsNumber, JsonSerializer.Serialize(number, s_optionReadFromStr)); + + // Option: write as string + + // Deserialize + Assert.Equal(number, JsonSerializer.Deserialize(jsonWithNumberAsNumber, s_optionWriteAsStr)); + Assert.Throws(() => JsonSerializer.Deserialize(jsonWithNumberAsString, s_optionWriteAsStr)); + + // Serialize + Assert.Equal(jsonWithNumberAsString, JsonSerializer.Serialize(number, s_optionWriteAsStr)); + + // Option: read and write from/to string + + // Deserialize + Assert.Equal(number, JsonSerializer.Deserialize(jsonWithNumberAsNumber, s_optionReadAndWriteFromStr)); + Assert.Equal(number, JsonSerializer.Deserialize(jsonWithNumberAsString, s_optionReadAndWriteFromStr)); + + // Serialize + Assert.Equal(jsonWithNumberAsString, JsonSerializer.Serialize(number, s_optionReadAndWriteFromStr)); + } + + [Fact] + public static void Number_AsBoxedRootType() + { + string numberAsString = @"""2"""; + + int @int = 2; + float @float = 2; + int? nullableInt = 2; + float? nullableFloat = 2; + + Assert.Equal(numberAsString, JsonSerializer.Serialize((object)@int, s_optionReadAndWriteFromStr)); + Assert.Equal(numberAsString, JsonSerializer.Serialize((object)@float, s_optionReadAndWriteFromStr)); + Assert.Equal(numberAsString, JsonSerializer.Serialize((object)nullableInt, s_optionReadAndWriteFromStr)); + Assert.Equal(numberAsString, JsonSerializer.Serialize((object)nullableFloat, s_optionReadAndWriteFromStr)); + + Assert.Equal(2, (int)JsonSerializer.Deserialize(numberAsString, typeof(int), s_optionReadAndWriteFromStr)); + Assert.Equal(2, (float)JsonSerializer.Deserialize(numberAsString, typeof(float), s_optionReadAndWriteFromStr)); + Assert.Equal(2, (int?)JsonSerializer.Deserialize(numberAsString, typeof(int?), s_optionReadAndWriteFromStr)); + Assert.Equal(2, (float?)JsonSerializer.Deserialize(numberAsString, typeof(float?), s_optionReadAndWriteFromStr)); + } + + [Fact] + public static void Number_AsCollectionElement_RoundTrip() + { + RunAsCollectionElementTest(JsonNumberTestData.Bytes); + RunAsCollectionElementTest(JsonNumberTestData.SBytes); + RunAsCollectionElementTest(JsonNumberTestData.Shorts); + RunAsCollectionElementTest(JsonNumberTestData.Ints); + RunAsCollectionElementTest(JsonNumberTestData.Longs); + RunAsCollectionElementTest(JsonNumberTestData.UShorts); + RunAsCollectionElementTest(JsonNumberTestData.UInts); + RunAsCollectionElementTest(JsonNumberTestData.ULongs); + RunAsCollectionElementTest(JsonNumberTestData.Floats); + RunAsCollectionElementTest(JsonNumberTestData.Doubles); + RunAsCollectionElementTest(JsonNumberTestData.Decimals); + RunAsCollectionElementTest(JsonNumberTestData.NullableBytes); + RunAsCollectionElementTest(JsonNumberTestData.NullableSBytes); + RunAsCollectionElementTest(JsonNumberTestData.NullableShorts); + RunAsCollectionElementTest(JsonNumberTestData.NullableInts); + RunAsCollectionElementTest(JsonNumberTestData.NullableLongs); + RunAsCollectionElementTest(JsonNumberTestData.NullableUShorts); + RunAsCollectionElementTest(JsonNumberTestData.NullableUInts); + RunAsCollectionElementTest(JsonNumberTestData.NullableULongs); + RunAsCollectionElementTest(JsonNumberTestData.NullableFloats); + RunAsCollectionElementTest(JsonNumberTestData.NullableDoubles); + RunAsCollectionElementTest(JsonNumberTestData.NullableDecimals); + } + + private static void RunAsCollectionElementTest(List numbers) + { + StringBuilder jsonBuilder_NumbersAsNumbers = new StringBuilder(); + StringBuilder jsonBuilder_NumbersAsStrings = new StringBuilder(); + StringBuilder jsonBuilder_NumbersAsNumbersAndStrings = new StringBuilder(); + StringBuilder jsonBuilder_NumbersAsNumbersAndStrings_Alternate = new StringBuilder(); + bool asNumber = false; + + jsonBuilder_NumbersAsNumbers.Append("["); + jsonBuilder_NumbersAsStrings.Append("["); + jsonBuilder_NumbersAsNumbersAndStrings.Append("["); + jsonBuilder_NumbersAsNumbersAndStrings_Alternate.Append("["); + + foreach (T number in numbers) + { + string numberAsString = GetNumberAsString(number); + + string jsonWithNumberAsString = @$"""{numberAsString}"""; + + jsonBuilder_NumbersAsNumbers.Append($"{numberAsString},"); + jsonBuilder_NumbersAsStrings.Append($"{jsonWithNumberAsString},"); + jsonBuilder_NumbersAsNumbersAndStrings.Append(asNumber + ? $"{numberAsString}," + : $"{jsonWithNumberAsString},"); + jsonBuilder_NumbersAsNumbersAndStrings_Alternate.Append(!asNumber + ? $"{numberAsString}," + : $"{jsonWithNumberAsString},"); + + asNumber = !asNumber; + } + + jsonBuilder_NumbersAsNumbers.Remove(jsonBuilder_NumbersAsNumbers.Length - 1, 1); + jsonBuilder_NumbersAsStrings.Remove(jsonBuilder_NumbersAsStrings.Length - 1, 1); + jsonBuilder_NumbersAsNumbersAndStrings.Remove(jsonBuilder_NumbersAsNumbersAndStrings.Length - 1, 1); + jsonBuilder_NumbersAsNumbersAndStrings_Alternate.Remove(jsonBuilder_NumbersAsNumbersAndStrings_Alternate.Length - 1, 1); + + jsonBuilder_NumbersAsNumbers.Append("]"); + jsonBuilder_NumbersAsStrings.Append("]"); + jsonBuilder_NumbersAsNumbersAndStrings.Append("]"); + jsonBuilder_NumbersAsNumbersAndStrings_Alternate.Append("]"); + + string jsonNumbersAsStrings = jsonBuilder_NumbersAsStrings.ToString(); + + PerformAsCollectionElementSerialization( + numbers, + jsonBuilder_NumbersAsNumbers.ToString(), + jsonNumbersAsStrings, + jsonBuilder_NumbersAsNumbersAndStrings.ToString(), + jsonBuilder_NumbersAsNumbersAndStrings_Alternate.ToString()); + + // Reflection based tests for every collection type. + RunAllCollectionsRoundTripTest(jsonNumbersAsStrings); + } + + private static void PerformAsCollectionElementSerialization( + List numbers, + string json_NumbersAsNumbers, + string json_NumbersAsStrings, + string json_NumbersAsNumbersAndStrings, + string json_NumbersAsNumbersAndStrings_Alternate) + { + List deserialized; + + // Option: read from string + + // Deserialize + deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbers, s_optionReadFromStr); + AssertIEnumerableEqual(numbers, deserialized); + + deserialized = JsonSerializer.Deserialize>(json_NumbersAsStrings, s_optionReadFromStr); + AssertIEnumerableEqual(numbers, deserialized); + + deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings, s_optionReadFromStr); + AssertIEnumerableEqual(numbers, deserialized); + + deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings_Alternate, s_optionReadFromStr); + AssertIEnumerableEqual(numbers, deserialized); + + // Serialize + Assert.Equal(json_NumbersAsNumbers, JsonSerializer.Serialize(numbers, s_optionReadFromStr)); + + // Option: write as string + + // Deserialize + deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbers, s_optionWriteAsStr); + AssertIEnumerableEqual(numbers, deserialized); + + Assert.Throws(() => JsonSerializer.Deserialize>(json_NumbersAsStrings, s_optionWriteAsStr)); + + Assert.Throws(() => JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings, s_optionWriteAsStr)); + + Assert.Throws(() => JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings_Alternate, s_optionWriteAsStr)); + + // Serialize + Assert.Equal(json_NumbersAsStrings, JsonSerializer.Serialize(numbers, s_optionWriteAsStr)); + + // Option: read and write from/to string + + // Deserialize + deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbers, s_optionReadAndWriteFromStr); + AssertIEnumerableEqual(numbers, deserialized); + + deserialized = JsonSerializer.Deserialize>(json_NumbersAsStrings, s_optionReadAndWriteFromStr); + AssertIEnumerableEqual(numbers, deserialized); + + deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings, s_optionReadAndWriteFromStr); + AssertIEnumerableEqual(numbers, deserialized); + + deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings_Alternate, s_optionReadAndWriteFromStr); + AssertIEnumerableEqual(numbers, deserialized); + + // Serialize + Assert.Equal(json_NumbersAsStrings, JsonSerializer.Serialize(numbers, s_optionReadAndWriteFromStr)); + } + + private static void AssertIEnumerableEqual(IEnumerable list1, IEnumerable list2) + { + IEnumerator enumerator1 = list1.GetEnumerator(); + IEnumerator enumerator2 = list2.GetEnumerator(); + + while (enumerator1.MoveNext()) + { + enumerator2.MoveNext(); + Assert.Equal(enumerator1.Current, enumerator2.Current); + } + + Assert.False(enumerator2.MoveNext()); + } + + private static void RunAllCollectionsRoundTripTest(string json) + { + foreach (Type type in CollectionTestTypes.DeserializableGenericEnumerableTypes()) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(HashSet<>)) + { + HashSet obj1 = (HashSet)JsonSerializer.Deserialize(json, type, s_optionReadAndWriteFromStr); + string serialized = JsonSerializer.Serialize(obj1, s_optionReadAndWriteFromStr); + + HashSet obj2 = (HashSet)JsonSerializer.Deserialize(serialized, type, s_optionReadAndWriteFromStr); + + Assert.Equal(obj1.Count, obj2.Count); + foreach (T element in obj1) + { + Assert.True(obj2.Contains(element)); + } + } + else if (type != typeof(byte[])) + { + object obj = JsonSerializer.Deserialize(json, type, s_optionReadAndWriteFromStr); + string serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr); + Assert.Equal(json, serialized); + } + } + + foreach (Type type in CollectionTestTypes.DeserializableNonGenericEnumerableTypes()) + { + // Deserialized as collection of JsonElements. + object obj = JsonSerializer.Deserialize(json, type, s_optionReadAndWriteFromStr); + // Serialized as strings with escaping. + string serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr); + + // Ensure escaped values were serialized accurately + List list = JsonSerializer.Deserialize>(serialized, s_optionReadAndWriteFromStr); + serialized = JsonSerializer.Serialize(list, s_optionReadAndWriteFromStr); + Assert.Equal(json, serialized); + + // Serialize instance which is a collection of numbers (not JsonElements). + obj = Activator.CreateInstance(type, new[] { list }); + serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr); + Assert.Equal(json, serialized); + } + } + + [Fact] + public static void Number_AsDictionaryElement_RoundTrip() + { + var dict = new Dictionary(); + for (int i = 0; i < 10; i++) + { + dict[JsonNumberTestData.Ints[i]] = JsonNumberTestData.Floats[i]; + } + + // Serialize + string serialized = JsonSerializer.Serialize(dict, s_optionReadAndWriteFromStr); + AssertDictionaryElements_StringValues(serialized); + + // Deserialize + dict = JsonSerializer.Deserialize>(serialized, s_optionReadAndWriteFromStr); + + // Test roundtrip + JsonTestHelper.AssertJsonEqual(serialized, JsonSerializer.Serialize(dict, s_optionReadAndWriteFromStr)); + } + + private static void AssertDictionaryElements_StringValues(string serialized) + { + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(serialized)); + reader.Read(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + else if (reader.TokenType == JsonTokenType.String) + { +#if BUILDING_INBOX_LIBRARY + Assert.False(reader.ValueSpan.Contains((byte)'\\')); +#else + foreach (byte val in reader.ValueSpan) + { + if (val == (byte)'\\') + { + Assert.True(false, "Unexpected escape token."); + } + } +#endif + } + else + { + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + } + } + } + + [Fact] + [ActiveIssue("https://github.com/dotnet/runtime/issues/39674", typeof(PlatformDetection), nameof(PlatformDetection.IsMonoInterpreter))] + public static void DictionariesRoundTrip() + { + RunAllDictionariessRoundTripTest(JsonNumberTestData.ULongs); + RunAllDictionariessRoundTripTest(JsonNumberTestData.Floats); + RunAllDictionariessRoundTripTest(JsonNumberTestData.Doubles); + } + + private static void RunAllDictionariessRoundTripTest(List numbers) + { + StringBuilder jsonBuilder_NumbersAsStrings = new StringBuilder(); + + jsonBuilder_NumbersAsStrings.Append("{"); + + foreach (T number in numbers) + { + string numberAsString = GetNumberAsString(number); + string jsonWithNumberAsString = @$"""{numberAsString}"""; + + jsonBuilder_NumbersAsStrings.Append($"{jsonWithNumberAsString}:"); + jsonBuilder_NumbersAsStrings.Append($"{jsonWithNumberAsString},"); + } + + jsonBuilder_NumbersAsStrings.Remove(jsonBuilder_NumbersAsStrings.Length - 1, 1); + jsonBuilder_NumbersAsStrings.Append("}"); + + string jsonNumbersAsStrings = jsonBuilder_NumbersAsStrings.ToString(); + + foreach (Type type in CollectionTestTypes.DeserializableDictionaryTypes()) + { + object obj = JsonSerializer.Deserialize(jsonNumbersAsStrings, type, s_optionReadAndWriteFromStr); + JsonTestHelper.AssertJsonEqual(jsonNumbersAsStrings, JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr)); + } + + foreach (Type type in CollectionTestTypes.DeserializableNonDictionaryTypes()) + { + Dictionary dict = JsonSerializer.Deserialize>(jsonNumbersAsStrings, s_optionReadAndWriteFromStr); + + // Serialize instance which is a dictionary of numbers (not JsonElements). + object obj = Activator.CreateInstance(type, new[] { dict }); + string serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr); + JsonTestHelper.AssertJsonEqual(jsonNumbersAsStrings, serialized); + } + } + + [Fact] + public static void Number_AsPropertyValue_RoundTrip() + { + var obj = new Class_With_NullableUInt64_And_Float() + { + NullableUInt64Number = JsonNumberTestData.NullableULongs.LastOrDefault(), + FloatNumbers = JsonNumberTestData.Floats + }; + + // Serialize + string serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr); + + // Deserialize + obj = JsonSerializer.Deserialize(serialized, s_optionReadAndWriteFromStr); + + // Test roundtrip + JsonTestHelper.AssertJsonEqual(serialized, JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr)); + } + + private class Class_With_NullableUInt64_And_Float + { + public ulong? NullableUInt64Number { get; set; } + [JsonInclude] + public List FloatNumbers; + } + + [Fact] + public static void Number_AsKeyValuePairValue_RoundTrip() + { + var obj = new KeyValuePair>(JsonNumberTestData.NullableULongs.LastOrDefault(), JsonNumberTestData.Floats); + + // Serialize + string serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr); + + // Deserialize + obj = JsonSerializer.Deserialize>>(serialized, s_optionReadAndWriteFromStr); + + // Test roundtrip + JsonTestHelper.AssertJsonEqual(serialized, JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr)); + } + + [Fact] + public static void Number_AsObjectWithParameterizedCtor_RoundTrip() + { + var obj = new MyClassWithNumbers(JsonNumberTestData.NullableULongs.LastOrDefault(), JsonNumberTestData.Floats); + + // Serialize + string serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr); + + // Deserialize + obj = JsonSerializer.Deserialize(serialized, s_optionReadAndWriteFromStr); + + // Test roundtrip + JsonTestHelper.AssertJsonEqual(serialized, JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr)); + } + + private class MyClassWithNumbers + { + public ulong? Ulong { get; } + public List ListOfFloats { get; } + + public MyClassWithNumbers(ulong? @ulong, List listOfFloats) + { + Ulong = @ulong; + ListOfFloats = listOfFloats; + } + } + + [Fact] + public static void Number_AsObjectWithParameterizedCtor_PropHasAttribute() + { + string json = @"{""ListOfFloats"":[""1""]}"; + // Strict handling on property overrides loose global policy. + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionReadFromStr)); + + // Serialize + json = @"{""ListOfFloats"":[1]}"; + MyClassWithNumbers_PropsHasAttribute obj = JsonSerializer.Deserialize(json); + + // Number serialized as JSON number due to strict handling on property which overrides loose global policy. + Assert.Equal(json, JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr)); + } + + private class MyClassWithNumbers_PropsHasAttribute + { + [JsonNumberHandling(JsonNumberHandling.Strict)] + public List ListOfFloats { get; } + + public MyClassWithNumbers_PropsHasAttribute(List listOfFloats) + { + ListOfFloats = listOfFloats; + } + } + + [Fact] + public static void FloatingPointConstants_Pass() + { + // Valid values + PerformFloatingPointSerialization("NaN"); + PerformFloatingPointSerialization("Infinity"); + PerformFloatingPointSerialization("-Infinity"); + + static void PerformFloatingPointSerialization(string testString) + { + string testStringAsJson = $@"""{testString}"""; + string testJson = @$"{{""FloatNumber"":{testStringAsJson},""DoubleNumber"":{testStringAsJson}}}"; + + StructWithNumbers obj; + switch (testString) + { + case "NaN": + obj = JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants); + Assert.Equal(float.NaN, obj.FloatNumber); + Assert.Equal(double.NaN, obj.DoubleNumber); + + obj = JsonSerializer.Deserialize(testJson, s_optionReadFromStr); + Assert.Equal(float.NaN, obj.FloatNumber); + Assert.Equal(double.NaN, obj.DoubleNumber); + break; + case "Infinity": + obj = JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants); + Assert.Equal(float.PositiveInfinity, obj.FloatNumber); + Assert.Equal(double.PositiveInfinity, obj.DoubleNumber); + + obj = JsonSerializer.Deserialize(testJson, s_optionReadFromStr); + Assert.Equal(float.PositiveInfinity, obj.FloatNumber); + Assert.Equal(double.PositiveInfinity, obj.DoubleNumber); + break; + case "-Infinity": + obj = JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants); + Assert.Equal(float.NegativeInfinity, obj.FloatNumber); + Assert.Equal(double.NegativeInfinity, obj.DoubleNumber); + + obj = JsonSerializer.Deserialize(testJson, s_optionReadFromStr); + Assert.Equal(float.NegativeInfinity, obj.FloatNumber); + Assert.Equal(double.NegativeInfinity, obj.DoubleNumber); + break; + default: + Assert.Throws(() => JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants)); + return; + } + + JsonTestHelper.AssertJsonEqual(testJson, JsonSerializer.Serialize(obj, s_optionsAllowFloatConstants)); + JsonTestHelper.AssertJsonEqual(testJson, JsonSerializer.Serialize(obj, s_optionWriteAsStr)); + } + } + + [Theory] + [InlineData("naN")] + [InlineData("Nan")] + [InlineData("NAN")] + [InlineData("+Infinity")] + [InlineData("+infinity")] + [InlineData("infinity")] + [InlineData("infinitY")] + [InlineData("INFINITY")] + [InlineData("+INFINITY")] + [InlineData("-infinity")] + [InlineData("-infinitY")] + [InlineData("-INFINITY")] + [InlineData(" NaN")] + [InlineData(" Infinity")] + [InlineData(" -Infinity")] + [InlineData("NaN ")] + [InlineData("Infinity ")] + [InlineData("-Infinity ")] + [InlineData("a-Infinity")] + [InlineData("NaNa")] + [InlineData("Infinitya")] + [InlineData("-Infinitya")] + public static void FloatingPointConstants_Fail(string testString) + { + string testStringAsJson = $@"""{testString}"""; + string testJson = @$"{{""FloatNumber"":{testStringAsJson},""DoubleNumber"":{testStringAsJson}}}"; + Assert.Throws(() => JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(testJson, s_optionReadFromStr)); + } + + [Fact] + public static void AllowFloatingPointConstants_WriteAsNumber_IfNotConstant() + { + float @float = 1; + // Not written as "1" + Assert.Equal("1", JsonSerializer.Serialize(@float, s_optionsAllowFloatConstants)); + + double @double = 1; + // Not written as "1" + Assert.Equal("1", JsonSerializer.Serialize(@double, s_optionsAllowFloatConstants)); + } + + [Theory] + [InlineData("NaN")] + [InlineData("Infinity")] + [InlineData("-Infinity")] + public static void Unquoted_FloatingPointConstants_Read_Fail(string testString) + { + Assert.Throws(() => JsonSerializer.Deserialize(testString, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(testString, s_optionReadFromStr)); + Assert.Throws(() => JsonSerializer.Deserialize(testString, s_optionReadFromStrAllowFloatConstants)); + } + + private struct StructWithNumbers + { + public float FloatNumber { get; set; } + public double DoubleNumber { get; set; } + } + + [Fact] + public static void ReadFromString_AllowFloatingPoint() + { + string json = @"{""IntNumber"":""1"",""FloatNumber"":""NaN""}"; + ClassWithNumbers obj = JsonSerializer.Deserialize(json, s_optionReadFromStrAllowFloatConstants); + + Assert.Equal(1, obj.IntNumber); + Assert.Equal(float.NaN, obj.FloatNumber); + + JsonTestHelper.AssertJsonEqual(@"{""IntNumber"":1,""FloatNumber"":""NaN""}", JsonSerializer.Serialize(obj, s_optionReadFromStrAllowFloatConstants)); + } + + [Fact] + public static void WriteAsString_AllowFloatingPoint() + { + string json = @"{""IntNumber"":""1"",""FloatNumber"":""NaN""}"; + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionWriteAsStrAllowFloatConstants)); + + var obj = new ClassWithNumbers + { + IntNumber = 1, + FloatNumber = float.NaN + }; + + JsonTestHelper.AssertJsonEqual(json, JsonSerializer.Serialize(obj, s_optionWriteAsStrAllowFloatConstants)); + } + + public class ClassWithNumbers + { + public int IntNumber { get; set; } + public float FloatNumber { get; set; } + } + + [Fact] + public static void FloatingPointConstants_IncompatibleNumber() + { + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + AssertFloatingPointIncompatible_Fails(); + } + + private static void AssertFloatingPointIncompatible_Fails() + { + string[] testCases = new[] + { + @"""NaN""", + @"""Infinity""", + @"""-Infinity""", + }; + + foreach (string test in testCases) + { + Assert.Throws(() => JsonSerializer.Deserialize(test, s_optionReadFromStrAllowFloatConstants)); + } + } + + [Fact] + public static void UnsupportedFormats() + { + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + AssertUnsupportedFormatThrows(); + } + + private static void AssertUnsupportedFormatThrows() + { + string[] testCases = new[] + { + "$123.46", // Currency + "100.00 %", // Percent + "1234,57", // Fixed point + "00FF", // Hexadecimal + }; + + foreach (string test in testCases) + { + Assert.Throws(() => JsonSerializer.Deserialize(test, s_optionReadFromStr)); + } + } + + [Fact] + public static void EscapingTest() + { + // Cause all characters to be escaped. + var encoderSettings = new TextEncoderSettings(); + encoderSettings.ForbidCharacters('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '+', '-', 'e', 'E'); + + JavaScriptEncoder encoder = JavaScriptEncoder.Create(encoderSettings); + var options = new JsonSerializerOptions(s_optionReadAndWriteFromStr) + { + Encoder = encoder + }; + + PerformEscapingTest(JsonNumberTestData.Bytes, options); + PerformEscapingTest(JsonNumberTestData.SBytes, options); + PerformEscapingTest(JsonNumberTestData.Shorts, options); + PerformEscapingTest(JsonNumberTestData.Ints, options); + PerformEscapingTest(JsonNumberTestData.Longs, options); + PerformEscapingTest(JsonNumberTestData.UShorts, options); + PerformEscapingTest(JsonNumberTestData.UInts, options); + PerformEscapingTest(JsonNumberTestData.ULongs, options); + PerformEscapingTest(JsonNumberTestData.Floats, options); + PerformEscapingTest(JsonNumberTestData.Doubles, options); + PerformEscapingTest(JsonNumberTestData.Decimals, options); + } + + private static void PerformEscapingTest(List numbers, JsonSerializerOptions options) + { + // All input characters are escaped + IEnumerable numbersAsStrings = numbers.Select(num => GetNumberAsString(num)); + string input = JsonSerializer.Serialize(numbersAsStrings, options); + AssertListNumbersEscaped(input); + + // Unescaping works + List deserialized = JsonSerializer.Deserialize>(input, options); + Assert.Equal(numbers.Count, deserialized.Count); + for (int i = 0; i < numbers.Count; i++) + { + Assert.Equal(numbers[i], deserialized[i]); + } + + // Every number is written as a string, and custom escaping is not honored. + string serialized = JsonSerializer.Serialize(deserialized, options); + AssertListNumbersUnescaped(serialized); + } + + private static void AssertListNumbersEscaped(string json) + { + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); + reader.Read(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + else + { + Assert.Equal(JsonTokenType.String, reader.TokenType); +#if BUILDING_INBOX_LIBRARY + Assert.True(reader.ValueSpan.Contains((byte)'\\')); +#else + bool foundBackSlash = false; + foreach (byte val in reader.ValueSpan) + { + if (val == (byte)'\\') + { + foundBackSlash = true; + break; + } + } + + Assert.True(foundBackSlash, "Expected escape token."); +#endif + } + } + } + + private static void AssertListNumbersUnescaped(string json) + { + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); + reader.Read(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + else + { + Assert.Equal(JsonTokenType.String, reader.TokenType); +#if BUILDING_INBOX_LIBRARY + Assert.False(reader.ValueSpan.Contains((byte)'\\')); +#else + foreach (byte val in reader.ValueSpan) + { + if (val == (byte)'\\') + { + Assert.True(false, "Unexpected escape token."); + } + } +#endif + } + } + } + + [Fact] + public static void Number_RoundtripNull() + { + Perform_Number_RoundTripNull_Test(); + Perform_Number_RoundTripNull_Test(); + Perform_Number_RoundTripNull_Test(); + Perform_Number_RoundTripNull_Test(); + Perform_Number_RoundTripNull_Test(); + Perform_Number_RoundTripNull_Test(); + Perform_Number_RoundTripNull_Test(); + Perform_Number_RoundTripNull_Test(); + Perform_Number_RoundTripNull_Test(); + Perform_Number_RoundTripNull_Test(); + } + + private static void Perform_Number_RoundTripNull_Test() + { + string nullAsJson = "null"; + string nullAsQuotedJson = $@"""{nullAsJson}"""; + + Assert.Throws(() => JsonSerializer.Deserialize(nullAsJson, s_optionReadAndWriteFromStr)); + Assert.Equal("0", JsonSerializer.Serialize(default(T))); + Assert.Throws(() => JsonSerializer.Deserialize(nullAsQuotedJson, s_optionReadAndWriteFromStr)); + } + + [Fact] + public static void NullableNumber_RoundtripNull() + { + Perform_NullableNumber_RoundTripNull_Test(); + Perform_NullableNumber_RoundTripNull_Test(); + Perform_NullableNumber_RoundTripNull_Test(); + Perform_NullableNumber_RoundTripNull_Test(); + Perform_NullableNumber_RoundTripNull_Test(); + Perform_NullableNumber_RoundTripNull_Test(); + Perform_NullableNumber_RoundTripNull_Test(); + Perform_NullableNumber_RoundTripNull_Test(); + Perform_NullableNumber_RoundTripNull_Test(); + Perform_NullableNumber_RoundTripNull_Test(); + } + + private static void Perform_NullableNumber_RoundTripNull_Test() + { + string nullAsJson = "null"; + string nullAsQuotedJson = $@"""{nullAsJson}"""; + + Assert.Null(JsonSerializer.Deserialize(nullAsJson, s_optionReadAndWriteFromStr)); + Assert.Equal(nullAsJson, JsonSerializer.Serialize(default(T))); + Assert.Throws(() => JsonSerializer.Deserialize(nullAsQuotedJson, s_optionReadAndWriteFromStr)); + } + + [Fact] + public static void Disallow_ArbritaryStrings_On_AllowFloatingPointConstants() + { + string json = @"""12345"""; + + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants)); + } + + [Fact] + public static void Attributes_OnMembers_Work() + { + // Bad JSON because Int should not be string. + string intIsString = @"{""Float"":""1234.5"",""Int"":""12345""}"; + + // Good JSON because Float can be string. + string floatIsString = @"{""Float"":""1234.5"",""Int"":12345}"; + + // Good JSON because Float can be number. + string floatIsNumber = @"{""Float"":1234.5,""Int"":12345}"; + + Assert.Throws(() => JsonSerializer.Deserialize(intIsString)); + + ClassWith_Attribute_OnNumber obj = JsonSerializer.Deserialize(floatIsString); + Assert.Equal(1234.5, obj.Float); + Assert.Equal(12345, obj.Int); + + obj = JsonSerializer.Deserialize(floatIsNumber); + Assert.Equal(1234.5, obj.Float); + Assert.Equal(12345, obj.Int); + + // Per options, float should be written as string. + JsonTestHelper.AssertJsonEqual(floatIsString, JsonSerializer.Serialize(obj)); + } + + private class ClassWith_Attribute_OnNumber + { + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public float Float { get; set; } + + public int Int { get; set; } + } + + [Fact] + public static void Attribute_OnRootType_Works() + { + // Not allowed + string floatIsString = @"{""Float"":""1234"",""Int"":123}"; + + // Allowed + string floatIsNan = @"{""Float"":""NaN"",""Int"":123}"; + + Assert.Throws(() => JsonSerializer.Deserialize(floatIsString)); + + Type_AllowFloatConstants obj = JsonSerializer.Deserialize(floatIsNan); + Assert.Equal(float.NaN, obj.Float); + Assert.Equal(123, obj.Int); + + JsonTestHelper.AssertJsonEqual(floatIsNan, JsonSerializer.Serialize(obj)); + } + + [JsonNumberHandling(JsonNumberHandling.AllowNamedFloatingPointLiterals)] + private class Type_AllowFloatConstants + { + public float Float { get; set; } + + public int Int { get; set; } + } + + [Fact] + public static void AttributeOnType_WinsOver_GlobalOption() + { + // Global options strict, type options loose + string json = @"{""Float"":""12345""}"; + var obj1 = JsonSerializer.Deserialize(json); + + Assert.Equal(@"{""Float"":""12345""}", JsonSerializer.Serialize(obj1)); + + // Global options loose, type options strict + json = @"{""Float"":""12345""}"; + Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionReadAndWriteFromStr)); + + var obj2 = new ClassWith_StrictAttribute() { Float = 12345 }; + Assert.Equal(@"{""Float"":12345}", JsonSerializer.Serialize(obj2, s_optionReadAndWriteFromStr)); + } + + [JsonNumberHandling(JsonNumberHandling.Strict)] + public class ClassWith_StrictAttribute + { + public float Float { get; set; } + } + + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + private class ClassWith_LooseAttribute + { + public float Float { get; set; } + } + + [Fact] + public static void AttributeOnMember_WinsOver_AttributeOnType() + { + string json = @"{""Double"":""NaN""}"; + Assert.Throws(() => JsonSerializer.Deserialize(json)); + + var obj = new ClassWith_Attribute_On_TypeAndMember { Double = float.NaN }; + Assert.Throws(() => JsonSerializer.Serialize(obj)); + } + + [JsonNumberHandling(JsonNumberHandling.AllowNamedFloatingPointLiterals)] + private class ClassWith_Attribute_On_TypeAndMember + { + [JsonNumberHandling(JsonNumberHandling.Strict)] + public double Double { get; set; } + } + + [Fact] + public static void Attribute_OnNestedType_Works() + { + string jsonWithShortProperty = @"{""Short"":""1""}"; + ClassWith_ReadAsStringAttribute obj = JsonSerializer.Deserialize(jsonWithShortProperty); + Assert.Equal(1, obj.Short); + + string jsonWithMyObjectProperty = @"{""MyObject"":{""Float"":""1""}}"; + Assert.Throws(() => JsonSerializer.Deserialize(jsonWithMyObjectProperty)); + } + + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public class ClassWith_ReadAsStringAttribute + { + public short Short { get; set; } + + public ClassWith_StrictAttribute MyObject { get; set; } + } + + [Fact] + public static void MemberAttributeAppliesToCollection_SimpleElements() + { + RunTest(); + RunTest>(); + RunTest>(); + RunTest>(); + RunTest>(); + RunTest>(); + RunTest>(); + RunTest>(); + RunTest>(); + RunTest(); + RunTest>(); + + static void RunTest() + { + string json = @"{""MyList"":[""1"",""2""]}"; + ClassWithSimpleCollectionProperty obj = global::System.Text.Json.JsonSerializer.Deserialize>(json); + Assert.Equal(json, global::System.Text.Json.JsonSerializer.Serialize(obj)); + } + } + + public class ClassWithSimpleCollectionProperty + { + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public T MyList { get; set; } + } + + [Fact] + public static void NestedCollectionElementTypeHandling_Overrides_ParentPropertyHandling() + { + // 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)); + + // 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)); + } + + public class ClassWithComplexListProperty + { + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public List MyList { get; set; } + } + + [Fact] + public static void MemberAttributeAppliesToDictionary_SimpleElements() + { + string json = @"{""First"":""1"",""Second"":""2""}"; + ClassWithSimpleDictionaryProperty obj = JsonSerializer.Deserialize(json); + } + + public class ClassWithSimpleDictionaryProperty + { + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public Dictionary MyDictionary { get; set; } + } + + [Fact] + public static void NestedDictionaryElementTypeHandling_Overrides_ParentPropertyHandling() + { + // 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)); + + // 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)); + } + + public class ClassWithComplexDictionaryProperty + { + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public Dictionary MyDictionary { get; set; } + } + + [Fact] + public static void TypeAttributeAppliesTo_CustomCollectionElements() + { + string json = @"[""1""]"; + MyCustomList obj = JsonSerializer.Deserialize(json); + Assert.Equal(json, JsonSerializer.Serialize(obj)); + } + + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public class MyCustomList : List { } + + [Fact] + public static void TypeAttributeAppliesTo_CustomCollectionElements_HonoredWhenProperty() + { + string json = @"{""List"":[""1""]}"; + ClassWithCustomList obj = JsonSerializer.Deserialize(json); + Assert.Equal(json, JsonSerializer.Serialize(obj)); + } + + public class ClassWithCustomList + { + public MyCustomList List { get; set; } + } + + [Fact] + public static void TypeAttributeAppliesTo_CustomDictionaryElements() + { + string json = @"{""Key"":""1""}"; + MyCustomDictionary obj = JsonSerializer.Deserialize(json); + Assert.Equal(json, JsonSerializer.Serialize(obj)); + } + + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public class MyCustomDictionary : Dictionary { } + + [Fact] + public static void TypeAttributeAppliesTo_CustomDictionaryElements_HonoredWhenProperty() + { + string json = @"{""Dictionary"":{""Key"":""1""}}"; + ClassWithCustomDictionary obj = JsonSerializer.Deserialize(json); + Assert.Equal(json, JsonSerializer.Serialize(obj)); + } + + public class ClassWithCustomDictionary + { + public MyCustomDictionary Dictionary { get; set; } + } + + [Fact] + public static void Attribute_OnType_NotRecursive() + { + // Recursive behavior would allow a string number. + // This is not supported. + string json = @"{""NestedClass"":{""MyInt"":""1""}}"; + Assert.Throws(() => JsonSerializer.Deserialize(json)); + + var obj = new AttributeOnFirstLevel + { + NestedClass = new BadProperty { MyInt = 1 } + }; + Assert.Equal(@"{""NestedClass"":{""MyInt"":1}}", JsonSerializer.Serialize(obj)); + } + + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public class AttributeOnFirstLevel + { + public BadProperty NestedClass { get; set; } + } + + public class BadProperty + { + public int MyInt { get; set; } + } + + [Fact] + public static void HandlingOnMemberOverridesHandlingOnType_Enumerable() + { + string json = @"{""List"":[""1""]}"; + Assert.Throws(() => JsonSerializer.Deserialize(json)); + + var obj = new MyCustomListWrapper + { + List = new MyCustomList { 1 } + }; + Assert.Equal(@"{""List"":[1]}", JsonSerializer.Serialize(obj)); + } + + public class MyCustomListWrapper + { + [JsonNumberHandling(JsonNumberHandling.Strict)] + public MyCustomList List { get; set; } + } + + [Fact] + public static void HandlingOnMemberOverridesHandlingOnType_Dictionary() + { + string json = @"{""Dictionary"":{""Key"":""1""}}"; + Assert.Throws(() => JsonSerializer.Deserialize(json)); + + var obj1 = new MyCustomDictionaryWrapper + { + Dictionary = new MyCustomDictionary { ["Key"] = 1 } + }; + Assert.Equal(@"{""Dictionary"":{""Key"":1}}", JsonSerializer.Serialize(obj1)); + } + + public class MyCustomDictionaryWrapper + { + [JsonNumberHandling(JsonNumberHandling.Strict)] + public MyCustomDictionary Dictionary { get; set; } + } + + [Fact] + public static void Attribute_NotAllowed_On_NonNumber_NonCollection_Property() + { + string json = @""; + InvalidOperationException ex = Assert.Throws(() => JsonSerializer.Deserialize(json)); + string exAsStr = ex.ToString(); + Assert.Contains("MyProp", exAsStr); + Assert.Contains(typeof(ClassWith_NumberHandlingOn_ObjectProperty).ToString(), exAsStr); + + ex = Assert.Throws(() => JsonSerializer.Serialize(new ClassWith_NumberHandlingOn_ObjectProperty())); + exAsStr = ex.ToString(); + Assert.Contains("MyProp", exAsStr); + Assert.Contains(typeof(ClassWith_NumberHandlingOn_ObjectProperty).ToString(), exAsStr); + } + + public class ClassWith_NumberHandlingOn_ObjectProperty + { + [JsonNumberHandling(JsonNumberHandling.Strict)] + public BadProperty MyProp { get; set; } + } + + [Fact] + public static void Attribute_NotAllowed_On_Property_WithCustomConverter() + { + string json = @""; + InvalidOperationException ex = Assert.Throws(() => JsonSerializer.Deserialize(json)); + string exAsStr = ex.ToString(); + Assert.Contains(typeof(ConverterForInt32).ToString(), exAsStr); + Assert.Contains(typeof(ClassWith_NumberHandlingOn_Property_WithCustomConverter).ToString(), exAsStr); + + ex = Assert.Throws(() => JsonSerializer.Serialize(new ClassWith_NumberHandlingOn_Property_WithCustomConverter())); + exAsStr = ex.ToString(); + Assert.Contains(typeof(ConverterForInt32).ToString(), exAsStr); + Assert.Contains(typeof(ClassWith_NumberHandlingOn_Property_WithCustomConverter).ToString(), exAsStr); + } + + public class ClassWith_NumberHandlingOn_Property_WithCustomConverter + { + [JsonNumberHandling(JsonNumberHandling.Strict)] + [JsonConverter(typeof(ConverterForInt32))] + public int MyProp { get; set; } + } + + [Fact] + public static void Attribute_NotAllowed_On_Type_WithCustomConverter() + { + string json = @""; + InvalidOperationException ex = Assert.Throws(() => JsonSerializer.Deserialize(json)); + string exAsStr = ex.ToString(); + Assert.Contains(typeof(ConverterForMyType).ToString(), exAsStr); + Assert.Contains(typeof(ClassWith_NumberHandlingOn_Type_WithCustomConverter).ToString(), exAsStr); + + ex = Assert.Throws(() => JsonSerializer.Serialize(new ClassWith_NumberHandlingOn_Type_WithCustomConverter())); + exAsStr = ex.ToString(); + Assert.Contains(typeof(ConverterForMyType).ToString(), exAsStr); + Assert.Contains(typeof(ClassWith_NumberHandlingOn_Type_WithCustomConverter).ToString(), exAsStr); + } + + [JsonNumberHandling(JsonNumberHandling.Strict)] + [JsonConverter(typeof(ConverterForMyType))] + public class ClassWith_NumberHandlingOn_Type_WithCustomConverter + { + } + + private class ConverterForMyType : JsonConverter + { + public override ClassWith_NumberHandlingOn_Type_WithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, ClassWith_NumberHandlingOn_Type_WithCustomConverter value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + } + + [Fact] + public static void CustomConverterOverridesBuiltInLogic() + { + var options = new JsonSerializerOptions(s_optionReadAndWriteFromStr) + { + Converters = { new ConverterForInt32(), new ConverterForFloat() } + }; + + string json = @"""32"""; + + // Converter returns 25 regardless of input. + Assert.Equal(25, JsonSerializer.Deserialize(json, options)); + + // Converter throws this exception regardless of input. + Assert.Throws(() => JsonSerializer.Serialize(4, options)); + + json = @"""NaN"""; + + // Converter returns 25 if NaN. + Assert.Equal(25, JsonSerializer.Deserialize(json, options)); + + // Converter writes 25 if NaN. + Assert.Equal("25", JsonSerializer.Serialize((float?)float.NaN, options)); + } + + public class ConverterForFloat : JsonConverter + { + public override float? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String && reader.GetString() == "NaN") + { + return 25; + } + + throw new NotSupportedException(); + } + + public override void Write(Utf8JsonWriter writer, float? value, JsonSerializerOptions options) + { + if (float.IsNaN(value.Value)) + { + writer.WriteNumberValue(25); + return; + } + + throw new NotSupportedException(); + } + } + + [Fact] + public static void JsonNumberHandling_ArgOutOfRangeFail() + { + // Global options + ArgumentOutOfRangeException ex = Assert.Throws( + () => new JsonSerializerOptions { NumberHandling = (JsonNumberHandling)(-1) }); + Assert.Contains("value", ex.ToString()); + Assert.Throws( + () => new JsonSerializerOptions { NumberHandling = (JsonNumberHandling)(8) }); + + ex = Assert.Throws( + () => new JsonNumberHandlingAttribute((JsonNumberHandling)(-1))); + Assert.Contains("handling", ex.ToString()); + Assert.Throws( + () => new JsonNumberHandlingAttribute((JsonNumberHandling)(8))); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Serialization/Object.ReadTests.cs b/src/libraries/System.Text.Json/tests/Serialization/Object.ReadTests.cs index 544634359a9683..6a36e96d3818b9 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Object.ReadTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Object.ReadTests.cs @@ -278,7 +278,7 @@ private class CollectionWithoutPublicParameterlessCtor : IList internal CollectionWithoutPublicParameterlessCtor() { - Debug.Fail("The JsonSerializer should not be callin non-public ctors, by default."); + Debug.Fail("The JsonSerializer should not be calling non-public ctors, by default."); } public CollectionWithoutPublicParameterlessCtor(List list) @@ -286,7 +286,7 @@ public CollectionWithoutPublicParameterlessCtor(List list) _list = list; } - public object this[int index] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public object this[int index] { get => _list[index]; set => _list[index] = value; } public bool IsFixedSize => throw new NotImplementedException(); diff --git a/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs index a94055600815c5..a7cc37fb68f1c8 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs @@ -589,6 +589,7 @@ private static JsonSerializerOptions GetFullyPopulatedOptionsInstance() { options.ReadCommentHandling = JsonCommentHandling.Disallow; options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault; + options.NumberHandling = JsonNumberHandling.AllowReadingFromString; } else { @@ -636,6 +637,10 @@ private static void VerifyOptionsEqual(JsonSerializerOptions options, JsonSerial { Assert.Equal(options.DefaultIgnoreCondition, newOptions.DefaultIgnoreCondition); } + else if (property.Name == "NumberHandling") + { + Assert.Equal(options.NumberHandling, newOptions.NumberHandling); + } else { Assert.True(false, $"Public option was added to JsonSerializerOptions but not copied in the copy ctor: {property.Name}"); diff --git a/src/libraries/System.Text.Json/tests/Serialization/Stream.Collections.cs b/src/libraries/System.Text.Json/tests/Serialization/Stream.Collections.cs index eb371a431949c9..3efdfde63502bb 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Stream.Collections.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Stream.Collections.cs @@ -98,7 +98,7 @@ private static async Task TestDeserialization( // TODO: https://github.com/dotnet/runtime/issues/35611. // Can't control order of dictionary elements when serializing, so reference metadata might not match up. - if(!(DictionaryTypes().Contains(type) && options.ReferenceHandler == ReferenceHandler.Preserve)) + if(!(CollectionTestTypes.DictionaryTypes().Contains(type) && options.ReferenceHandler == ReferenceHandler.Preserve)) { JsonTestHelper.AssertJsonEqual(expectedJson, serialized); } @@ -287,7 +287,7 @@ private static IEnumerable BufferSizes() private static IEnumerable CollectionTypes() { - foreach (Type type in EnumerableTypes()) + foreach (Type type in CollectionTestTypes.EnumerableTypes()) { yield return type; } @@ -301,43 +301,17 @@ private static IEnumerable CollectionTypes() yield return type; } // Dictionary types - foreach (Type type in DictionaryTypes()) + foreach (Type type in CollectionTestTypes.DictionaryTypes()) { yield return type; } } - private static IEnumerable EnumerableTypes() - { - yield return typeof(TElement[]); // ArrayConverter - yield return typeof(ConcurrentQueue); // ConcurrentQueueOfTConverter - yield return typeof(GenericICollectionWrapper); // ICollectionOfTConverter - yield return typeof(WrapperForIEnumerable); // IEnumerableConverter - yield return typeof(WrapperForIReadOnlyCollectionOfT); // IEnumerableOfTConverter - yield return typeof(Queue); // IEnumerableWithAddMethodConverter - yield return typeof(WrapperForIList); // IListConverter - yield return typeof(Collection); // IListOfTConverter - yield return typeof(ImmutableList); // ImmutableEnumerableOfTConverter - yield return typeof(HashSet); // ISetOfTConverter - yield return typeof(List); // ListOfTConverter - yield return typeof(Queue); // QueueOfTConverter - } - private static IEnumerable ObjectNotationTypes() { yield return typeof(KeyValuePair); // KeyValuePairConverter } - private static IEnumerable DictionaryTypes() - { - yield return typeof(Dictionary); // DictionaryOfStringTValueConverter - yield return typeof(Hashtable); // IDictionaryConverter - yield return typeof(ConcurrentDictionary); // IDictionaryOfStringTValueConverter - yield return typeof(GenericIDictionaryWrapper); // IDictionaryOfStringTValueConverter - yield return typeof(ImmutableDictionary); // ImmutableDictionaryOfStringTValueConverter - yield return typeof(GenericIReadOnlyDictionaryWrapper); // IReadOnlyDictionaryOfStringTValueConverter - } - private static HashSet StackTypes() => new HashSet { typeof(ConcurrentStack), // ConcurrentStackOfTConverter @@ -389,7 +363,7 @@ public ImmutableStructWithStrings( [InlineData("]")] public static void DeserializeDictionaryStartsWithInvalidJson(string json) { - foreach (Type type in DictionaryTypes()) + foreach (Type type in CollectionTestTypes.DictionaryTypes()) { Assert.ThrowsAsync(async () => { @@ -404,7 +378,7 @@ public static void DeserializeDictionaryStartsWithInvalidJson(string json) [Fact] public static void SerializeEmptyCollection() { - foreach (Type type in EnumerableTypes()) + foreach (Type type in CollectionTestTypes.EnumerableTypes()) { Assert.Equal("[]", JsonSerializer.Serialize(GetEmptyCollection(type))); } @@ -414,7 +388,7 @@ public static void SerializeEmptyCollection() Assert.Equal("[]", JsonSerializer.Serialize(GetEmptyCollection(type))); } - foreach (Type type in DictionaryTypes()) + foreach (Type type in CollectionTestTypes.DictionaryTypes()) { Assert.Equal("{}", JsonSerializer.Serialize(GetEmptyCollection(type))); } diff --git a/src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.NonGenericCollections.cs b/src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.NonGenericCollections.cs index adccf4fc1f4b2a..7ef238b63d453a 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.NonGenericCollections.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.NonGenericCollections.cs @@ -218,6 +218,16 @@ public WrapperForIList(IEnumerable items) _list = new List(items); } + public WrapperForIList(IEnumerable items) + { + _list = new List(); + + foreach (object item in items) + { + _list.Add(item); + } + } + public object this[int index] { get => ((IList)_list)[index]; set => ((IList)_list)[index] = value; } public bool IsFixedSize => ((IList)_list).IsFixedSize; diff --git a/src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.cs b/src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.cs index 8496e4b5817e86..34f96f79fc94b0 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.cs @@ -2,8 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; +using System.Collections.ObjectModel; using System.Linq; using Xunit; @@ -1889,4 +1891,68 @@ public override string ConvertName(string name) return string.Concat(name.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString())).ToLower(); } } + + public static class CollectionTestTypes + { + public static IEnumerable EnumerableTypes() + { + yield return typeof(TElement[]); // ArrayConverter + yield return typeof(ConcurrentQueue); // ConcurrentQueueOfTConverter + yield return typeof(GenericICollectionWrapper); // ICollectionOfTConverter + yield return typeof(WrapperForIEnumerable); // IEnumerableConverter + yield return typeof(WrapperForIReadOnlyCollectionOfT); // IEnumerableOfTConverter + yield return typeof(Queue); // IEnumerableWithAddMethodConverter + yield return typeof(WrapperForIList); // IListConverter + yield return typeof(Collection); // IListOfTConverter + yield return typeof(ImmutableList); // ImmutableEnumerableOfTConverter + yield return typeof(HashSet); // ISetOfTConverter + yield return typeof(List); // ListOfTConverter + yield return typeof(Queue); // QueueOfTConverter + } + + public static IEnumerable DeserializableGenericEnumerableTypes() + { + yield return typeof(TElement[]); // ArrayConverter + yield return typeof(ConcurrentQueue); // ConcurrentQueueOfTConverter + yield return typeof(GenericICollectionWrapper); // ICollectionOfTConverter + yield return typeof(IEnumerable); // IEnumerableConverter + yield return typeof(Collection); // IListOfTConverter + yield return typeof(ImmutableList); // ImmutableEnumerableOfTConverter + yield return typeof(HashSet); // ISetOfTConverter + yield return typeof(List); // ListOfTConverter + yield return typeof(Queue); // QueueOfTConverter + } + + public static IEnumerable DeserializableNonGenericEnumerableTypes() + { + yield return typeof(Queue); // IEnumerableWithAddMethodConverter + yield return typeof(WrapperForIList); // IListConverter + } + + public static IEnumerable DictionaryTypes() + { + yield return typeof(Dictionary); // DictionaryOfStringTValueConverter + yield return typeof(Hashtable); // IDictionaryConverter + yield return typeof(ConcurrentDictionary); // IDictionaryOfStringTValueConverter + yield return typeof(GenericIDictionaryWrapper); // IDictionaryOfStringTValueConverter + yield return typeof(ImmutableDictionary); // ImmutableDictionaryOfStringTValueConverter + yield return typeof(GenericIReadOnlyDictionaryWrapper); // IReadOnlyDictionaryOfStringTValueConverter + } + + public static IEnumerable DeserializableDictionaryTypes() + { + yield return typeof(Dictionary); // DictionaryOfStringTValueConverter + yield return typeof(Hashtable); // IDictionaryConverter + yield return typeof(ConcurrentDictionary); // IDictionaryOfStringTValueConverter + yield return typeof(GenericIDictionaryWrapper); // IDictionaryOfStringTValueConverter + yield return typeof(ImmutableDictionary); // ImmutableDictionaryOfStringTValueConverter + yield return typeof(IReadOnlyDictionary); // IReadOnlyDictionaryOfStringTValueConverter + } + + public static IEnumerable DeserializableNonDictionaryTypes() + { + yield return typeof(Hashtable); // IDictionaryConverter + yield return typeof(SortedList); // IDictionaryConverter + } + } } 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 7701738f903180..59035d01c6c671 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 @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);$(NetFrameworkCurrent) true @@ -93,6 +93,7 @@ +