From b8c4563edf7ac12f8372b5bc1e5e72d4447a2245 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Sat, 11 Jul 2020 21:28:47 -0700 Subject: [PATCH 1/4] Add JsonNumberHandling & support for (de)serializing numbers from/to string --- .../System.Text.Json/ref/System.Text.Json.cs | 9 + .../src/System.Text.Json.csproj | 1 + .../src/System/Text/Json/JsonConstants.cs | 5 + .../Text/Json/Reader/JsonReaderHelper.cs | 13 + .../Text/Json/Reader/Utf8JsonReader.TryGet.cs | 145 +++- .../Converters/Value/ByteConverter.cs | 16 +- .../Converters/Value/DecimalConverter.cs | 16 +- .../Converters/Value/DoubleConverter.cs | 16 +- .../Converters/Value/Int16Converter.cs | 20 +- .../Converters/Value/Int32Converter.cs | 18 +- .../Converters/Value/Int64Converter.cs | 16 +- .../Converters/Value/SByteConverter.cs | 16 +- .../Converters/Value/SingleConverter.cs | 17 +- .../Converters/Value/StringConverter.cs | 2 - .../Converters/Value/UInt16Converter.cs | 18 +- .../Converters/Value/UInt32Converter.cs | 18 +- .../Converters/Value/UInt64Converter.cs | 16 +- .../Json/Serialization/JsonNumberHandling.cs | 33 + .../Serialization/JsonSerializerOptions.cs | 18 + .../Utf8JsonWriter.WriteValues.Decimal.cs | 8 + .../Utf8JsonWriter.WriteValues.Double.cs | 8 + .../Utf8JsonWriter.WriteValues.Float.cs | 8 + ...Utf8JsonWriter.WriteValues.SignedNumber.cs | 11 + .../Utf8JsonWriter.WriteValues.String.cs | 14 + ...f8JsonWriter.WriteValues.UnsignedNumber.cs | 11 + .../tests/JsonNumberTestData.cs | 29 +- .../Serialization/NumberHandlingTests.cs | 694 ++++++++++++++++++ .../tests/Serialization/OptionsTests.cs | 5 + .../tests/System.Text.Json.Tests.csproj | 3 +- 29 files changed, 1157 insertions(+), 47 deletions(-) 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 0ab83e8b52656..7f6ce0c644f6a 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() { } 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 e381a1b381d46..796c05babdd91 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -135,6 +135,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 2bfc41c6bd912..27ff9dd2af18a 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 @@ -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.Text.Json.Serialization; + namespace System.Text.Json { internal static class JsonConstants @@ -105,5 +107,8 @@ internal static class JsonConstants // The maximum number of parameters a constructor can have where it can be supported. public const int MaxParameterCount = 64; + + public const JsonNumberHandling ReadNumberOrFloatingConstantFromString = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals; + public const JsonNumberHandling WriteNumberOrFloatingConstantAsString = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowNamedFloatingPointLiterals; } } 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 fcd7f0d3fb8da..3dc0f62a988c8 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,18 @@ public static bool TryGetEscapedGuid(ReadOnlySpan source, out Guid value) value = default; return false; } + + public static char GetFloatingPointStandardParseFormat(ReadOnlySpan span) + { + foreach (byte token in span) + { + if (token == 'E' || token == 'e') + { + return JsonConstants.ScientificNotationFormat; + } + } + + return default; + } } } 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 dbc52d94b79d5..20d88ce49fae0 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,64 @@ public float GetSingle() internal float GetSingleWithQuotes() { ReadOnlySpan span = GetUnescapedSpan(); - if (!TryGetSingleCore(out float value, span)) + + char numberFormat = JsonReaderHelper.GetFloatingPointStandardParseFormat(span); + if (Utf8Parser.TryParse(span, out float value, out int bytesConsumed, numberFormat) + && span.Length == bytesConsumed) { - throw ThrowHelper.GetFormatException(NumericType.Single); + bool shouldThrow = false; + + // Enforce consistency between NETCOREAPP and NETFX given that + // Utf8Parser.TryParse behaves differently for these values. + + if (float.IsNaN(value)) + { + if (!(span.Length == 3 && span[0] == (byte)'N' && span[1] == (byte)'a' && span[2] == (byte)'N')) + { + shouldThrow = true; + } + } + else if (float.IsPositiveInfinity(value)) + { + if (!( + span.Length == 8 && + span[0] == (byte)'I' && + span[1] == (byte)'n' && + span[2] == (byte)'f' && + span[3] == (byte)'i' && + span[4] == (byte)'n' && + span[5] == (byte)'i' && + span[6] == (byte)'t' && + span[7] == (byte)'y')) + { + shouldThrow = true; + } + } + else if (float.IsNegativeInfinity(value)) + { + if (!( + span.Length == 9 && + span[0] == (byte)'-' && + span[1] == (byte)'I' && + span[2] == (byte)'n' && + span[3] == (byte)'f' && + span[4] == (byte)'i' && + span[5] == (byte)'n' && + span[6] == (byte)'i' && + span[7] == (byte)'t' && + span[8] == (byte)'y')) + { + shouldThrow = true; + } + } + + if (!shouldThrow) + { + return value; + } } - return value; + + throw ThrowHelper.GetFormatException(NumericType.Single); } /// @@ -449,11 +502,64 @@ public double GetDouble() internal double GetDoubleWithQuotes() { ReadOnlySpan span = GetUnescapedSpan(); - if (!TryGetDoubleCore(out double value, span)) + + char numberFormat = JsonReaderHelper.GetFloatingPointStandardParseFormat(span); + if (Utf8Parser.TryParse(span, out double value, out int bytesConsumed, numberFormat) + && span.Length == bytesConsumed) { - throw ThrowHelper.GetFormatException(NumericType.Double); + bool shouldThrow = false; + + // Enforce consistency between NETCOREAPP and NETFX given that + // Utf8Parser.TryParse behaves differently for these values. + + if (double.IsNaN(value)) + { + if (!(span.Length == 3 && span[0] == (byte)'N' && span[1] == (byte)'a' && span[2] == (byte)'N')) + { + shouldThrow = true; + } + } + else if (double.IsPositiveInfinity(value)) + { + if (!( + span.Length == 8 && + span[0] == (byte)'I' && + span[1] == (byte)'n' && + span[2] == (byte)'f' && + span[3] == (byte)'i' && + span[4] == (byte)'n' && + span[5] == (byte)'i' && + span[6] == (byte)'t' && + span[7] == (byte)'y')) + { + shouldThrow = true; + } + } + else if (double.IsNegativeInfinity(value)) + { + if (!( + span.Length == 9 && + span[0] == (byte)'-' && + span[1] == (byte)'I' && + span[2] == (byte)'n' && + span[3] == (byte)'f' && + span[4] == (byte)'i' && + span[5] == (byte)'n' && + span[6] == (byte)'i' && + span[7] == (byte)'t' && + span[8] == (byte)'y')) + { + shouldThrow = true; + } + } + + if (!shouldThrow) + { + return value; + } } - return value; + + throw ThrowHelper.GetFormatException(NumericType.Double); } /// @@ -482,11 +588,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 +1029,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 +1060,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 +1090,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/Converters/Value/ByteConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs index 774341c8e39ae..15e6fae05e31c 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 @@ -7,12 +7,26 @@ internal sealed class ByteConverter : JsonConverter { public override byte Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.String && + options != null && + (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + { + return reader.GetByteWithQuotes(); + } + return reader.GetByte(); } public override void Write(Utf8JsonWriter writer, byte value, JsonSerializerOptions options) { - writer.WriteNumberValue(value); + if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + { + writer.WriteNumberValueAsString(value); + } + else + { + writer.WriteNumberValue(value); + } } internal override byte ReadWithQuotes(ref Utf8JsonReader reader) 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 c78316a88f95f..2060cb5a51317 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 @@ -7,12 +7,26 @@ internal sealed class DecimalConverter : JsonConverter { public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.String && + options != null && + (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + { + return reader.GetDecimalWithQuotes(); + } + return reader.GetDecimal(); } public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) { - writer.WriteNumberValue(value); + if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + { + writer.WriteNumberValueAsString(value); + } + else + { + writer.WriteNumberValue(value); + } } internal override decimal ReadWithQuotes(ref Utf8JsonReader reader) 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 641ad8e9991e8..928e58d141052 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 @@ -7,12 +7,26 @@ internal sealed class DoubleConverter : JsonConverter { public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.String && + options != null && + ((JsonConstants.ReadNumberOrFloatingConstantFromString) & options.NumberHandling) != 0) + { + return reader.GetDoubleWithQuotes(); + } + return reader.GetDouble(); } public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) { - writer.WriteNumberValue(value); + if (options != null && ((JsonConstants.WriteNumberOrFloatingConstantAsString) & options.NumberHandling) != 0) + { + writer.WriteNumberValueAsString(value); + } + else + { + writer.WriteNumberValue(value); + } } internal override double ReadWithQuotes(ref Utf8JsonReader reader) 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 c01ee00032cc5..abd786f373faf 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,21 +1,33 @@ // 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 override short Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.String && + options != null && + (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + { + return reader.GetInt16WithQuotes(); + } + return reader.GetInt16(); } public override void Write(Utf8JsonWriter writer, short value, JsonSerializerOptions options) { - // For performance, lift up the writer implementation. - writer.WriteNumberValue((long)value); + if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + { + writer.WriteNumberValueAsString(value); + } + else + { + // For performance, lift up the writer implementation. + writer.WriteNumberValue((long)value); + } } internal override short ReadWithQuotes(ref Utf8JsonReader reader) 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 5fc881a6b2e4d..e88f3c41be788 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 @@ -7,13 +7,27 @@ internal sealed class Int32Converter : JsonConverter { public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.String && + options != null && + (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + { + return reader.GetInt32WithQuotes(); + } + return reader.GetInt32(); } public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) { - // For performance, lift up the writer implementation. - writer.WriteNumberValue((long)value); + if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + { + writer.WriteNumberValueAsString(value); + } + else + { + // For performance, lift up the writer implementation. + writer.WriteNumberValue((long)value); + } } internal override int ReadWithQuotes(ref Utf8JsonReader reader) 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 4aec3ff4e4856..0c436242ed1b6 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 @@ -7,12 +7,26 @@ internal sealed class Int64Converter : JsonConverter { public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.String && + options != null && + (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + { + return reader.GetInt64WithQuotes(); + } + return reader.GetInt64(); } public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options) { - writer.WriteNumberValue(value); + if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + { + writer.WriteNumberValueAsString(value); + } + else + { + writer.WriteNumberValue(value); + } } internal override long ReadWithQuotes(ref Utf8JsonReader reader) 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 49ba6699b4530..99040f8c0ff1f 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 @@ -7,12 +7,26 @@ internal sealed class SByteConverter : JsonConverter { public override sbyte Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.String && + options != null && + (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + { + return reader.GetSByteWithQuotes(); + } + return reader.GetSByte(); } public override void Write(Utf8JsonWriter writer, sbyte value, JsonSerializerOptions options) { - writer.WriteNumberValue(value); + if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + { + writer.WriteNumberValueAsString(value); + } + else + { + writer.WriteNumberValue(value); + } } internal override sbyte ReadWithQuotes(ref Utf8JsonReader reader) 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 e2f7d76761e00..05bca56dd874b 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 @@ -7,12 +7,27 @@ internal sealed class SingleConverter : JsonConverter { public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.String && + options != null && + ((JsonConstants.ReadNumberOrFloatingConstantFromString) & options.NumberHandling) != 0) + { + return reader.GetSingleWithQuotes(); + } + return reader.GetSingle(); } public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) { - writer.WriteNumberValue(value); + if (options != null && + ((JsonConstants.WriteNumberOrFloatingConstantAsString) & options.NumberHandling) != 0) + { + writer.WriteNumberValueAsString(value); + } + else + { + writer.WriteNumberValue(value); + } } internal override float ReadWithQuotes(ref Utf8JsonReader reader) 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 f03d9ac1cba12..08972856b698c 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 db2af0a5f2e3b..b6414d39058e3 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 @@ -7,13 +7,27 @@ internal sealed class UInt16Converter : JsonConverter { public override ushort Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.String && + options != null && + (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + { + return reader.GetUInt16WithQuotes(); + } + return reader.GetUInt16(); } public override void Write(Utf8JsonWriter writer, ushort value, JsonSerializerOptions options) { - // For performance, lift up the writer implementation. - writer.WriteNumberValue((long)value); + if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + { + writer.WriteNumberValueAsString(value); + } + else + { + // For performance, lift up the writer implementation. + writer.WriteNumberValue((long)value); + } } internal override ushort ReadWithQuotes(ref Utf8JsonReader reader) 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 131e6feb1b054..133354d2c1340 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 @@ -7,13 +7,27 @@ internal sealed class UInt32Converter : JsonConverter { public override uint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.String && + options != null && + (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + { + return reader.GetUInt32WithQuotes(); + } + return reader.GetUInt32(); } public override void Write(Utf8JsonWriter writer, uint value, JsonSerializerOptions options) { - // For performance, lift up the writer implementation. - writer.WriteNumberValue((ulong)value); + if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + { + writer.WriteNumberValueAsString(value); + } + else + { + // For performance, lift up the writer implementation. + writer.WriteNumberValue((ulong)value); + } } internal override uint ReadWithQuotes(ref Utf8JsonReader reader) 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 95f9025c39812..8fdb8964f575f 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 @@ -7,12 +7,26 @@ internal sealed class UInt64Converter : JsonConverter { public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.String && + options != null && + (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + { + return reader.GetUInt64WithQuotes(); + } + return reader.GetUInt64(); } public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options) { - writer.WriteNumberValue(value); + if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + { + writer.WriteNumberValueAsString(value); + } + else + { + writer.WriteNumberValue(value); + } } internal override ulong ReadWithQuotes(ref Utf8JsonReader reader) 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 0000000000000..798e66a4b6f9a --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberHandling.cs @@ -0,0 +1,33 @@ +// 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 = 0, + /// + /// Numbers can be read from tokens. + /// Does not prevent numbers from being read from token. + /// + AllowReadingFromString = 1, + /// + /// Numbers will be written as JSON strings (with quotes), not as JSON numbers. + /// + WriteAsString = 2, + /// + /// Floating-point constants represented as + /// tokens such as "NaN", "Infinity", "-Infinity", can be read when reading, + /// and such CLR values such as , , + /// will be written as their corresponding JSON string representations. + /// + AllowNamedFloatingPointLiterals = 4 + } +} 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 e26eb9e0176aa..3dd16c3c19f07 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,22 @@ 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(); + _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/Writer/Utf8JsonWriter.WriteValues.Decimal.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Decimal.cs index ca5b059be50bb..b9305df12bf04 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); + WriteNumberValueAsString(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 cf8732bc44ec8..a2641f207af25 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,13 @@ 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); + WriteNumberValueAsString(utf8Number.Slice(0, bytesWritten)); + } } } 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 f120a09373c47..757f752ee97a0 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,13 @@ 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); + WriteNumberValueAsString(utf8Number.Slice(0, bytesWritten)); + } } } 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 957573ddb7a7f..15c02fe1c4b75 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,16 @@ private void WriteNumberValueIndented(long value) Debug.Assert(result); BytesPending += bytesWritten; } + + internal void WriteNumberValueAsString(int value) + => WriteNumberValueAsString((long)value); + + internal void WriteNumberValueAsString(long value) + { + Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatInt64Length]; + bool result = Utf8Formatter.TryFormat(value, utf8Number, out int bytesWritten); + Debug.Assert(result); + WriteNumberValueAsString(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 c86a8af22f162..7e594c3b96fca 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 WriteNumberValueAsString(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 2c15441d43259..ed37e86db6f5a 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,16 @@ private void WriteNumberValueIndented(ulong value) Debug.Assert(result); BytesPending += bytesWritten; } + + internal void WriteNumberValueAsString(uint value) + => WriteNumberValueAsString((ulong)value); + + internal void WriteNumberValueAsString(ulong value) + { + Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatUInt64Length]; + bool result = Utf8Formatter.TryFormat(value, utf8Number, out int bytesWritten); + Debug.Assert(result); + WriteNumberValueAsString(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 d07064a3e487b..384488485c980 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 0000000000000..685972fe740e7 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs @@ -0,0 +1,694 @@ +// 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.Generic; +using System.Globalization; +using System.IO; +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_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("]"); + + PerformAsCollectionElementSerialization( + numbers, + jsonBuilder_NumbersAsNumbers.ToString(), + jsonBuilder_NumbersAsStrings.ToString(), + jsonBuilder_NumbersAsNumbersAndStrings.ToString(), + jsonBuilder_NumbersAsNumbersAndStrings_Alternate.ToString()); + } + + private static void PerformAsCollectionElementSerialization( + List numbers, + string json_NumbersAsNumbers, + string json_NumbersAsStrings, + string json_NumbersAsNumbersAndStrings, + string json_NumbersAsNumbersAndStrings_Alternate) + { + // Option: read from string + + // Deserialize + List deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbers, s_optionReadFromStr); + AssertListsEqual(numbers, deserialized); + + deserialized = JsonSerializer.Deserialize>(json_NumbersAsStrings, s_optionReadFromStr); + AssertListsEqual(numbers, deserialized); + + deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings, s_optionReadFromStr); + AssertListsEqual(numbers, deserialized); + + deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings_Alternate, s_optionReadFromStr); + AssertListsEqual(numbers, deserialized); + + // Serialize + Assert.Equal(json_NumbersAsNumbers, JsonSerializer.Serialize(numbers, s_optionReadFromStr)); + + // Option: write as string + + // Deserialize + deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbers, s_optionWriteAsStr); + AssertListsEqual(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); + AssertListsEqual(numbers, deserialized); + + deserialized = JsonSerializer.Deserialize>(json_NumbersAsStrings, s_optionReadAndWriteFromStr); + AssertListsEqual(numbers, deserialized); + + deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings, s_optionReadAndWriteFromStr); + AssertListsEqual(numbers, deserialized); + + deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings_Alternate, s_optionReadAndWriteFromStr); + AssertListsEqual(numbers, deserialized); + + // Serialize + Assert.Equal(json_NumbersAsStrings, JsonSerializer.Serialize(numbers, s_optionReadAndWriteFromStr)); + } + + private static void AssertListsEqual(List list1, List list2) + { + Assert.Equal(list1.Count, list2.Count); + for (int i = 0; i < list1.Count; i++) + { + Assert.Equal(list1[i], list2[i]); + } + } + + [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); + + // Deserialize + dict = JsonSerializer.Deserialize>(serialized, s_optionReadAndWriteFromStr); + + // Test roundtrip + JsonTestHelper.AssertJsonEqual(serialized, JsonSerializer.Serialize(dict, s_optionReadAndWriteFromStr)); + } + + [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 FloatingPointConstants() + { + // Valid values + PerformFloatingPointSerialization("NaN", "NaN"); + PerformFloatingPointSerialization("Infinity", "Infinity"); + PerformFloatingPointSerialization("-Infinity", "-Infinity"); + + // Invalid values + PerformFloatingPointSerialization("naN"); + PerformFloatingPointSerialization("Nan"); + PerformFloatingPointSerialization("NAN"); + + PerformFloatingPointSerialization("+Infinity"); + PerformFloatingPointSerialization("+infinity"); + PerformFloatingPointSerialization("infinity"); + PerformFloatingPointSerialization("infinitY"); + PerformFloatingPointSerialization("INFINITY"); + PerformFloatingPointSerialization("+INFINITY"); + + PerformFloatingPointSerialization("-infinity"); + PerformFloatingPointSerialization("-infinitY"); + PerformFloatingPointSerialization("-INFINITY"); + + PerformFloatingPointSerialization(" NaN"); + PerformFloatingPointSerialization(" Infinity"); + PerformFloatingPointSerialization(" -Infinity"); + PerformFloatingPointSerialization("NaN "); + PerformFloatingPointSerialization("Infinity "); + PerformFloatingPointSerialization("-Infinity "); + PerformFloatingPointSerialization("a-Infinity"); + PerformFloatingPointSerialization("NaNa"); + PerformFloatingPointSerialization("Infinitya"); + PerformFloatingPointSerialization("-Infinitya"); + } + + private static void PerformFloatingPointSerialization(string testString, string constantAsString = null) + { + string constantAsJson = $@"""{constantAsString}"""; + string expectedJson = @$"{{""FloatNumber"":{constantAsJson},""DoubleNumber"":{constantAsJson}}}"; + + string testStringAsJson = $@"""{testString}"""; + string testJson = @$"{{""FloatNumber"":{testStringAsJson},""DoubleNumber"":{testStringAsJson}}}"; + + StructWithNumbers obj; + switch (constantAsString) + { + case "NaN": + obj = JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants); + 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); + break; + case "-Infinity": + obj = JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants); + Assert.Equal(float.NegativeInfinity, obj.FloatNumber); + Assert.Equal(double.NegativeInfinity, obj.DoubleNumber); + break; + default: + Assert.Throws(() => JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants)); + return; + } + + JsonTestHelper.AssertJsonEqual(expectedJson, JsonSerializer.Serialize(obj, s_optionsAllowFloatConstants)); + } + + 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(); + 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 CustomConverterOverridesBuiltInLogic() + { + var options = new JsonSerializerOptions(s_optionReadAndWriteFromStr) + { + Converters = { new ConverterForInt32() } + }; + + 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)); + } + + [Fact] + public static void EncodingTest() + { + // 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 + }; + + PerformEncodingTest(JsonNumberTestData.Bytes, options); + PerformEncodingTest(JsonNumberTestData.SBytes, options); + PerformEncodingTest(JsonNumberTestData.Shorts, options); + PerformEncodingTest(JsonNumberTestData.Ints, options); + PerformEncodingTest(JsonNumberTestData.Longs, options); + PerformEncodingTest(JsonNumberTestData.UShorts, options); + PerformEncodingTest(JsonNumberTestData.UInts, options); + PerformEncodingTest(JsonNumberTestData.ULongs, options); + PerformEncodingTest(JsonNumberTestData.Floats, options); + PerformEncodingTest(JsonNumberTestData.Doubles, options); + PerformEncodingTest(JsonNumberTestData.Decimals, options); + } + + private static void PerformEncodingTest(List numbers, JsonSerializerOptions options) + { + // All input characters are escaped + IEnumerable numbersAsStrings = numbers.Select(num => GetNumberAsString(num)); + string input = JsonSerializer.Serialize(numbersAsStrings, options); + + // 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); + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(serialized)); + 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 NullableNumber_RoundtripNull() + { + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + PerformNullabilityTest(); + } + + private static void PerformNullabilityTest() + { + string nullAsJson = "null"; + string nullAsQuotedJson = $@"""{nullAsJson}"""; + + if (Nullable.GetUnderlyingType(typeof(T)) != null) + { + Assert.Null(JsonSerializer.Deserialize(nullAsJson, s_optionReadAndWriteFromStr)); + Assert.Equal(nullAsJson, JsonSerializer.Serialize(default(T))); + } + else + { + 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 ConvertersHaveNullChecks() + { + RunConvertersHaveNullChecksTest(JsonNumberTestData.Bytes[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.SBytes[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.Shorts[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.Ints[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.Longs[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.UShorts[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.UInts[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.ULongs[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.Floats[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.Doubles[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.Decimals[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableBytes[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableSBytes[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableShorts[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableInts[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableLongs[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableUShorts[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableUInts[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableULongs[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableFloats[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableDoubles[0]); + RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableDecimals[0]); + } + + private static void RunConvertersHaveNullChecksTest(T number) + { + string numberAsJsonNumber = $"{number}"; + string numberAsJsonString = $@"""{number}"""; + var options = new JsonSerializerOptions(); + + var converter = (JsonConverter)options.GetConverter(typeof(T)); + + var reader_JsonNumber = new Utf8JsonReader(Encoding.UTF8.GetBytes(numberAsJsonNumber)); + var reader_JsonString = new Utf8JsonReader(Encoding.UTF8.GetBytes(numberAsJsonString)); + + reader_JsonNumber.Read(); + reader_JsonString.Read(); + + T val = converter.Read(ref reader_JsonNumber, typeof(T), options: null); + Assert.Equal(number, val); + + try + { + converter.Read(ref reader_JsonString, typeof(T), options: null); + Assert.True(false, "InvalidOperationException expected."); + } + catch (InvalidOperationException) { } + + using (MemoryStream stream = new MemoryStream()) + { + using (Utf8JsonWriter writer = new Utf8JsonWriter(stream)) + { + converter.Write(writer, number, options: null); + } + Assert.Equal(numberAsJsonNumber, Encoding.UTF8.GetString(stream.ToArray())); + } + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs index a94055600815c..a7cc37fb68f1c 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/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj index 72f5dbc5097b3..6edf36ca7a305 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 @@ + From dac0e6ae9a05f03d24773f8a1ea6d0421b2808be Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Wed, 15 Jul 2020 08:45:34 -0700 Subject: [PATCH 2/4] Add JsonNumberHandlingAttribute --- .../System.Text.Json/ref/System.Text.Json.cs | 6 + .../src/Resources/Strings.resx | 6 + .../src/System.Text.Json.csproj | 1 + .../src/System/Text/Json/JsonConstants.cs | 6 +- .../Text/Json/Reader/JsonReaderHelper.cs | 95 ++ .../Text/Json/Reader/Utf8JsonReader.TryGet.cs | 140 +-- .../Attributes/JsonNumberHandlingAttribute.cs | 23 + .../Converters/Collection/ArrayConverter.cs | 16 +- .../Collection/DictionaryDefaultConverter.cs | 78 +- .../DictionaryOfTKeyTValueConverter.cs | 28 +- .../Collection/IDictionaryConverter.cs | 2 +- .../IDictionaryOfTKeyTValueConverter.cs | 2 +- .../Collection/IEnumerableDefaultConverter.cs | 51 +- ...ReadOnlyDictionaryOfTKeyTValueConverter.cs | 2 +- ...mmutableDictionaryOfTKeyTValueConverter.cs | 2 +- .../Converters/Collection/ListOfTConverter.cs | 16 +- .../Object/KeyValuePairConverter.cs | 1 + .../Object/ObjectDefaultConverter.cs | 2 + ...ctWithParameterizedConstructorConverter.cs | 3 + .../Converters/Value/ByteConverter.cs | 38 +- .../Converters/Value/DecimalConverter.cs | 37 +- .../Converters/Value/DoubleConverter.cs | 51 +- .../Converters/Value/Int16Converter.cs | 38 +- .../Converters/Value/Int32Converter.cs | 38 +- .../Converters/Value/Int64Converter.cs | 37 +- .../Converters/Value/NullableConverter.cs | 31 + .../Converters/Value/SByteConverter.cs | 37 +- .../Converters/Value/SingleConverter.cs | 52 +- .../Converters/Value/UInt16Converter.cs | 38 +- .../Converters/Value/UInt32Converter.cs | 38 +- .../Converters/Value/UInt64Converter.cs | 37 +- .../Json/Serialization/JsonClassInfo.Cache.cs | 16 +- .../Text/Json/Serialization/JsonClassInfo.cs | 17 +- .../Text/Json/Serialization/JsonConverter.cs | 12 +- .../Json/Serialization/JsonConverterOfT.cs | 78 +- .../Json/Serialization/JsonParameterInfo.cs | 3 + .../Json/Serialization/JsonPropertyInfo.cs | 72 +- .../Json/Serialization/JsonPropertyInfoOfT.cs | 17 +- .../JsonSerializer.Read.HandlePropertyName.cs | 1 + .../JsonSerializer.Read.Helpers.cs | 1 + .../JsonSerializerOptions.Converters.cs | 2 +- .../Text/Json/Serialization/ReadStack.cs | 4 + .../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.Double.cs | 45 + .../Utf8JsonWriter.WriteValues.Float.cs | 45 + ...Utf8JsonWriter.WriteValues.SignedNumber.cs | 4 +- .../Serialization/NumberHandlingTests.cs | 912 +++++++++++++++--- .../tests/Serialization/Stream.Collections.cs | 38 +- .../TestClasses.NonGenericCollections.cs | 10 + .../Serialization/TestClasses/TestClasses.cs | 66 ++ 53 files changed, 1809 insertions(+), 527 deletions(-) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonNumberHandlingAttribute.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 7f6ce0c644f6a..0de194ebde5bc 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -542,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 41cf9de418c48..d5db3cfc6fb1b 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 796c05babdd91..e787defd06be0 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 @@ + 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 27ff9dd2af18a..0c851cb1aaddf 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 @@ -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.Text.Json.Serialization; - namespace System.Text.Json { internal static class JsonConstants @@ -85,6 +83,7 @@ internal static class JsonConstants (DateTimeParseNumFractionDigits - DateTimeNumFractionDigits)); // Like StandardFormat 'O' for DateTimeOffset, but allowing 9 additional (up to 16) fraction digits. public const int MinimumDateTimeParseLength = 10; // YYYY-MM-DD public const int MaximumEscapedDateTimeOffsetParseLength = MaxExpansionFactorWhileEscaping * MaximumDateTimeOffsetParseLength; + public const int NegativeInfinityLiteralConstantLength = 9; // Character count for -Infinity is 9 internal const char ScientificNotationFormat = 'e'; @@ -107,8 +106,5 @@ internal static class JsonConstants // The maximum number of parameters a constructor can have where it can be supported. public const int MaxParameterCount = 64; - - public const JsonNumberHandling ReadNumberOrFloatingConstantFromString = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals; - public const JsonNumberHandling WriteNumberOrFloatingConstantAsString = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowNamedFloatingPointLiterals; } } 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 3dc0f62a988c8..341da8493a9dd 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 @@ -334,5 +334,100 @@ public static char GetFloatingPointStandardParseFormat(ReadOnlySpan span) return default; } + + public static bool TryGetFloatingPointConstant(ReadOnlySpan span, out float value) + { + if (span.Length == 3) + { + if (ValueIsNan(span)) + { + value = float.NaN; + return true; + } + } + else if (span.Length == 8) + { + if (ValueIsPositiveInfinity(span)) + { + value = float.PositiveInfinity; + return true; + } + } + else if (span.Length == 9) + { + if (ValueIsNegativeInfinity(span)) + { + value = float.NegativeInfinity; + return true; + } + } + + value = 0; + return false; + } + + public static bool TryGetFloatingPointConstant(ReadOnlySpan span, out double value) + { + if (span.Length == 3) + { + if (ValueIsNan(span)) + { + value = double.NaN; + return true; + } + } + else if (span.Length == 8) + { + if (ValueIsPositiveInfinity(span)) + { + value = double.PositiveInfinity; + return true; + } + } + else if (span.Length == 9) + { + if (ValueIsNegativeInfinity(span)) + { + value = double.NegativeInfinity; + return true; + } + } + + value = 0; + return false; + } + + private static bool ValueIsNan(ReadOnlySpan span) + { + Debug.Assert(span.Length == 3); + return span[0] == (byte)'N' && span[1] == (byte)'a' && span[2] == (byte)'N'; + } + + private static bool ValueIsPositiveInfinity(ReadOnlySpan span) + { + Debug.Assert(span.Length == 8); + return span[0] == (byte)'I' && + span[1] == (byte)'n' && + span[2] == (byte)'f' && + span[3] == (byte)'i' && + span[4] == (byte)'n' && + span[5] == (byte)'i' && + span[6] == (byte)'t' && + span[7] == (byte)'y'; + } + + private static bool ValueIsNegativeInfinity(ReadOnlySpan span) + { + Debug.Assert(span.Length == 9); + return span[0] == (byte)'-' && + span[1] == (byte)'I' && + span[2] == (byte)'n' && + span[3] == (byte)'f' && + span[4] == (byte)'i' && + span[5] == (byte)'n' && + span[6] == (byte)'i' && + span[7] == (byte)'t' && + span[8] == (byte)'y'; + } } } 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 20d88ce49fae0..5b338c5091a1d 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 @@ -417,57 +417,19 @@ internal float GetSingleWithQuotes() { ReadOnlySpan span = GetUnescapedSpan(); + if (JsonReaderHelper.TryGetFloatingPointConstant(span, out float value)) + { + return value; + } + char numberFormat = JsonReaderHelper.GetFloatingPointStandardParseFormat(span); - if (Utf8Parser.TryParse(span, out float value, out int bytesConsumed, numberFormat) + if (Utf8Parser.TryParse(span, out value, out int bytesConsumed, numberFormat) && span.Length == bytesConsumed) { - bool shouldThrow = false; - - // Enforce consistency between NETCOREAPP and NETFX given that - // Utf8Parser.TryParse behaves differently for these values. - - if (float.IsNaN(value)) - { - if (!(span.Length == 3 && span[0] == (byte)'N' && span[1] == (byte)'a' && span[2] == (byte)'N')) - { - shouldThrow = true; - } - } - else if (float.IsPositiveInfinity(value)) - { - if (!( - span.Length == 8 && - span[0] == (byte)'I' && - span[1] == (byte)'n' && - span[2] == (byte)'f' && - span[3] == (byte)'i' && - span[4] == (byte)'n' && - span[5] == (byte)'i' && - span[6] == (byte)'t' && - span[7] == (byte)'y')) - { - shouldThrow = true; - } - } - else if (float.IsNegativeInfinity(value)) - { - if (!( - span.Length == 9 && - span[0] == (byte)'-' && - span[1] == (byte)'I' && - span[2] == (byte)'n' && - span[3] == (byte)'f' && - span[4] == (byte)'i' && - span[5] == (byte)'n' && - span[6] == (byte)'i' && - span[7] == (byte)'t' && - span[8] == (byte)'y')) - { - shouldThrow = true; - } - } - - if (!shouldThrow) + // NETCOREAPP implementation of the TryParse method above permits case-insenstive variants of the + // float constants "NaN", "Infinity", "-Infinity". This differs from the NETFRAMEWORK implmentation. + // The following logic reconciles the two implementations to enforce consistent behavior. + if (!float.IsNaN(value) && !float.IsPositiveInfinity(value) && !float.IsNegativeInfinity(value)) { return value; } @@ -476,6 +438,18 @@ internal float GetSingleWithQuotes() 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); + } + /// /// Parses the current JSON token value from the source as a . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a @@ -503,57 +477,19 @@ internal double GetDoubleWithQuotes() { ReadOnlySpan span = GetUnescapedSpan(); + if (JsonReaderHelper.TryGetFloatingPointConstant(span, out double value)) + { + return value; + } + char numberFormat = JsonReaderHelper.GetFloatingPointStandardParseFormat(span); - if (Utf8Parser.TryParse(span, out double value, out int bytesConsumed, numberFormat) + if (Utf8Parser.TryParse(span, out value, out int bytesConsumed, numberFormat) && span.Length == bytesConsumed) { - bool shouldThrow = false; - - // Enforce consistency between NETCOREAPP and NETFX given that - // Utf8Parser.TryParse behaves differently for these values. - - if (double.IsNaN(value)) - { - if (!(span.Length == 3 && span[0] == (byte)'N' && span[1] == (byte)'a' && span[2] == (byte)'N')) - { - shouldThrow = true; - } - } - else if (double.IsPositiveInfinity(value)) - { - if (!( - span.Length == 8 && - span[0] == (byte)'I' && - span[1] == (byte)'n' && - span[2] == (byte)'f' && - span[3] == (byte)'i' && - span[4] == (byte)'n' && - span[5] == (byte)'i' && - span[6] == (byte)'t' && - span[7] == (byte)'y')) - { - shouldThrow = true; - } - } - else if (double.IsNegativeInfinity(value)) - { - if (!( - span.Length == 9 && - span[0] == (byte)'-' && - span[1] == (byte)'I' && - span[2] == (byte)'n' && - span[3] == (byte)'f' && - span[4] == (byte)'i' && - span[5] == (byte)'n' && - span[6] == (byte)'i' && - span[7] == (byte)'t' && - span[8] == (byte)'y')) - { - shouldThrow = true; - } - } - - if (!shouldThrow) + // NETCOREAPP implmentation of the TryParse method above permits case-insenstive variants of the + // float constants "NaN", "Infinity", "-Infinity". This differs from the NETFRAMEWORK implmentation. + // The following logic reconciles the two implementations to enforce consistent behavior. + if (!double.IsNaN(value) && !double.IsPositiveInfinity(value) && !double.IsNegativeInfinity(value)) { return value; } @@ -562,6 +498,18 @@ internal double GetDoubleWithQuotes() 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); + } + /// /// Parses the current JSON token value from the source as a . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a 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 0000000000000..5b9896f9dbcf6 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonNumberHandlingAttribute.cs @@ -0,0 +1,23 @@ +// 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) => 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 4337037817f05..a70b3b02cde44 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 @@ -41,9 +41,21 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, if (elementConverter.CanUseDirectReadOrWrite) { // Fast path that avoids validation and extra indirection. - for (; index < array.Length; index++) + + JsonNumberHandling? numberHandling = state.Current.NumberHandling; + if (numberHandling.HasValue && elementConverter.IsInternalConverterForNumberType) + { + for (; index < array.Length; index++) + { + elementConverter.WriteNumberWithCustomHandling(writer, array[index], numberHandling.Value); + } + } + else { - elementConverter.Write(writer, array[index], options); + for (; index < array.Length; index++) + { + elementConverter.Write(writer, array[index], options); + } } } else 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 cd20a06206db8..e4efa0dad7371 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,29 +72,54 @@ internal sealed override bool OnTryRead( CreateCollection(ref reader, ref state); - JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo); + JsonConverter valueConverter = _valueConverter ??= GetValueConverter(elementClassInfo); + if (valueConverter.CanUseDirectReadOrWrite) { - // Process all elements. - while (true) + // Use number handling on the enumerable property which was preserved when pushing it as a new ReadStackFrame, or + // on the enumerable itself (likely custom). Handling on property wins. + JsonNumberHandling? numberHandling = state.Current.NumberHandling ?? state.Current.JsonPropertyInfo!.NumberHandling; + if (numberHandling.HasValue && valueConverter.IsInternalConverterForNumberType) { - // Read the key name. - reader.ReadWithVerify(); - - if (reader.TokenType == JsonTokenType.EndObject) + // Process all elements. + while (true) { - break; + // Read the key name. + reader.ReadWithVerify(); + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + // Read method would have thrown if otherwise. + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + + TKey key = ReadDictionaryKey(ref reader, ref state); + // Read the value and add. + reader.ReadWithVerify(); + TValue element = valueConverter.ReadNumberWithCustomHandling(ref reader, numberHandling.Value); + Add(key, element!, options, ref state); + } + } + else + { + // Process all elements. + while (true) + { + // Read the key name. + reader.ReadWithVerify(); + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + // Read method would have thrown if otherwise. + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + + TKey key = ReadDictionaryKey(ref reader, ref state); + // Read the value and add. + reader.ReadWithVerify(); + TValue element = valueConverter.Read(ref reader, s_valueType, options); + Add(key, element!, options, ref state); } - - // Read method would have thrown if otherwise. - Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); - - TKey key = ReadDictionaryKey(ref reader, ref state); - - // Read the value and add. - reader.ReadWithVerify(); - TValue element = valueConverter.Read(ref reader, typeof(TValue), options); - Add(key, element!, options, ref state); } } else @@ -114,7 +143,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 +201,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) @@ -293,6 +322,9 @@ internal sealed override bool OnTryWrite( } } + // If this enumerable is not a property on an object, or no handling was specified on the property + // use the handling specified on the enumrarable type (likely custom) or globally on JsonSerializerOptions. + state.Current.NumberHandling ??= state.Current.JsonClassInfo.PropertyInfoForClassInfo!.NumberHandling; state.Current.DeclaredJsonPropertyInfo = state.Current.JsonClassInfo.ElementClassInfo!.PropertyInfoForClassInfo; } 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 10818b7d31eee..1063519d9da46 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,18 +49,32 @@ 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); + JsonConverter valueConverter = _valueConverter ??= GetValueConverter(elementClassInfo); if (!state.SupportContinuation && valueConverter.CanUseDirectReadOrWrite) { + JsonNumberHandling? numberHandling = state.Current.NumberHandling; // Fast path that avoids validation and extra indirection. - do + if (numberHandling.HasValue && valueConverter.IsInternalConverterForNumberType) { - TKey key = enumerator.Current.Key; - keyConverter.WriteWithQuotes(writer, key, options, ref state); - - valueConverter.Write(writer, enumerator.Current.Value, options); - } while (enumerator.MoveNext()); + do + { + TKey key = enumerator.Current.Key; + keyConverter.WriteWithQuotes(writer, key, options, ref state); + valueConverter.WriteNumberWithCustomHandling(writer, enumerator.Current.Value, numberHandling.Value); + } while (enumerator.MoveNext()); + } + else + { + do + { + TKey key = enumerator.Current.Key; + keyConverter.WriteWithQuotes(writer, key, options, ref state); + valueConverter.Write(writer, enumerator.Current.Value, options); + } while (enumerator.MoveNext()); + } } else { 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 d1055df9ec581..5bfef5de0b1eb 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 4fb795ce71a8f..44c2069f6fd2e 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 dbe43be8bdc80..92fa6753f255f 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,21 +52,41 @@ internal override bool OnTryRead( CreateCollection(ref reader, ref state, options); - JsonConverter elementConverter = GetElementConverter(ref state); + JsonConverter elementConverter = GetElementConverter(elementClassInfo); if (elementConverter.CanUseDirectReadOrWrite) { - // Fast path that avoids validation and extra indirection. - while (true) + // Use number handling on the enumerable property which was preserved when pushing it as a new ReadStackFrame, or + // on the enumerable itself (likely custom). Handling on property wins. + JsonNumberHandling? numberHandling = state.Current.NumberHandling ?? state.Current.JsonPropertyInfo!.NumberHandling; + if (numberHandling.HasValue && elementConverter.IsInternalConverterForNumberType) { - reader.ReadWithVerify(); - if (reader.TokenType == JsonTokenType.EndArray) + // Fast path that avoids validation and extra indirection. + while (true) { - break; + reader.ReadWithVerify(); + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + // Obtain the CLR value from the JSON and apply to the object. + TElement element = elementConverter.ReadNumberWithCustomHandling(ref reader, numberHandling.Value); + Add(element!, ref state); + } + } + else + { + // Fast path that avoids validation and extra indirection. + while (true) + { + reader.ReadWithVerify(); + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + // Obtain the CLR value from the JSON and apply to the object. + TElement element = elementConverter.Read(ref reader, elementConverter.TypeToConvert, options); + Add(element!, ref state); } - - // Obtain the CLR value from the JSON and apply to the object. - TElement element = elementConverter.Read(ref reader, elementConverter.TypeToConvert, options); - Add(element!, ref state); } } else @@ -154,7 +176,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) @@ -275,6 +297,9 @@ internal sealed override bool OnTryWrite( state.Current.MetadataPropertyName = metadata; } + // If this enumerable is not a property on an object, or no handling was specified on the property + // use the handling specified on the enumrarable type (likely custom) or globally on JsonSerializerOptions. + state.Current.NumberHandling ??= state.Current.JsonClassInfo.PropertyInfoForClassInfo!.NumberHandling; state.Current.DeclaredJsonPropertyInfo = state.Current.JsonClassInfo.ElementClassInfo!.PropertyInfoForClassInfo; } 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 125f37534c1cc..28c3c245b1c23 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 9f5e5d0506474..ac9544d052859 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 5004bc1ded4c6..8e9eb1f3c579a 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 @@ -36,9 +36,21 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, if (elementConverter.CanUseDirectReadOrWrite) { // Fast path that avoids validation and extra indirection. - for (; index < list.Count; index++) + + JsonNumberHandling? numberHandling = state.Current.NumberHandling; + if (numberHandling.HasValue && elementConverter.IsInternalConverterForNumberType) + { + for (; index < list.Count; index++) + { + elementConverter.WriteNumberWithCustomHandling(writer, list[index], numberHandling.Value); + } + } + else { - elementConverter.Write(writer, list[index], options); + for (; index < list.Count; index++) + { + elementConverter.Write(writer, list[index], options); + } } } else 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 bee101fbfc181..9f2de6a1897ea 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 25bbcb0dc103a..c9f8f51952911 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 120b687bdabc0..f001c86198ed5 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 15e6fae05e31c..71a5330622872 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 @@ -7,9 +7,27 @@ internal sealed class ByteConverter : JsonConverter { public override byte Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.String && - options != null && - (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + return reader.GetByte(); + } + + public override void Write(Utf8JsonWriter writer, byte value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value); + } + + internal override byte ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetByteWithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, byte value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } + + internal override byte ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) + { + if (reader.TokenType == JsonTokenType.String && (JsonNumberHandling.AllowReadingFromString & handling) != 0) { return reader.GetByteWithQuotes(); } @@ -17,9 +35,9 @@ public override byte Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer return reader.GetByte(); } - public override void Write(Utf8JsonWriter writer, byte value, JsonSerializerOptions options) + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, byte value, JsonNumberHandling handling) { - if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + if ((JsonNumberHandling.WriteAsString & handling) != 0) { writer.WriteNumberValueAsString(value); } @@ -29,14 +47,6 @@ public override void Write(Utf8JsonWriter writer, byte value, JsonSerializerOpti } } - internal override byte ReadWithQuotes(ref Utf8JsonReader reader) - { - return reader.GetByteWithQuotes(); - } - - internal override void WriteWithQuotes(Utf8JsonWriter writer, byte value, JsonSerializerOptions options, ref WriteStack state) - { - writer.WritePropertyName(value); - } + internal override bool IsInternalConverterForNumberType => true; } } 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 2060cb5a51317..897555bb6e238 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 @@ -6,10 +6,29 @@ namespace System.Text.Json.Serialization.Converters internal sealed class DecimalConverter : JsonConverter { public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetDecimal(); + } + + public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value); + } + + internal override decimal ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetDecimalWithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } + + internal override decimal ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) { if (reader.TokenType == JsonTokenType.String && - options != null && - (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + (JsonNumberHandling.AllowReadingFromString & handling) != 0) { return reader.GetDecimalWithQuotes(); } @@ -17,9 +36,9 @@ public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, Json return reader.GetDecimal(); } - public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, decimal value, JsonNumberHandling handling) { - if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + if ((JsonNumberHandling.WriteAsString & handling) != 0) { writer.WriteNumberValueAsString(value); } @@ -29,14 +48,6 @@ public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerO } } - internal override decimal ReadWithQuotes(ref Utf8JsonReader reader) - { - return reader.GetDecimalWithQuotes(); - } - - internal override void WriteWithQuotes(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options, ref WriteStack state) - { - writer.WritePropertyName(value); - } + internal override bool IsInternalConverterForNumberType => true; } } 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 928e58d141052..6da670fb285c4 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 @@ -7,26 +7,12 @@ internal sealed class DoubleConverter : JsonConverter { public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.String && - options != null && - ((JsonConstants.ReadNumberOrFloatingConstantFromString) & options.NumberHandling) != 0) - { - return reader.GetDoubleWithQuotes(); - } - return reader.GetDouble(); } public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) { - if (options != null && ((JsonConstants.WriteNumberOrFloatingConstantAsString) & options.NumberHandling) != 0) - { - writer.WriteNumberValueAsString(value); - } - else - { - writer.WriteNumberValue(value); - } + writer.WriteNumberValue(value); } internal override double ReadWithQuotes(ref Utf8JsonReader reader) @@ -38,5 +24,40 @@ 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); + } + } + + internal override bool IsInternalConverterForNumberType => true; } } 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 abd786f373faf..a1a2ecf30fda6 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 @@ -6,10 +6,30 @@ namespace System.Text.Json.Serialization.Converters internal sealed class Int16Converter : JsonConverter { public override short Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetInt16(); + } + + public override void Write(Utf8JsonWriter writer, short value, JsonSerializerOptions options) + { + // For performance, lift up the writer implementation. + writer.WriteNumberValue((long)value); + } + + internal override short ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetInt16WithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, short value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } + + internal override short ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) { if (reader.TokenType == JsonTokenType.String && - options != null && - (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + (JsonNumberHandling.AllowReadingFromString & handling) != 0) { return reader.GetInt16WithQuotes(); } @@ -17,9 +37,9 @@ public override short Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe return reader.GetInt16(); } - public override void Write(Utf8JsonWriter writer, short value, JsonSerializerOptions options) + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, short value, JsonNumberHandling handling) { - if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + if ((JsonNumberHandling.WriteAsString & handling) != 0) { writer.WriteNumberValueAsString(value); } @@ -30,14 +50,6 @@ public override void Write(Utf8JsonWriter writer, short value, JsonSerializerOpt } } - internal override short ReadWithQuotes(ref Utf8JsonReader reader) - { - return reader.GetInt16WithQuotes(); - } - - internal override void WriteWithQuotes(Utf8JsonWriter writer, short value, JsonSerializerOptions options, ref WriteStack state) - { - writer.WritePropertyName(value); - } + internal override bool IsInternalConverterForNumberType => true; } } 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 e88f3c41be788..c95894dffcee2 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 @@ -6,10 +6,30 @@ namespace System.Text.Json.Serialization.Converters internal sealed class Int32Converter : JsonConverter { public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetInt32(); + } + + public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) + { + // For performance, lift up the writer implementation. + writer.WriteNumberValue((long)value); + } + + internal override int ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetInt32WithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, int value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } + + internal override int ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) { if (reader.TokenType == JsonTokenType.String && - options != null && - (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + (JsonNumberHandling.AllowReadingFromString & handling) != 0) { return reader.GetInt32WithQuotes(); } @@ -17,9 +37,9 @@ public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSeri return reader.GetInt32(); } - public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, int value, JsonNumberHandling handling) { - if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + if ((JsonNumberHandling.WriteAsString & handling) != 0) { writer.WriteNumberValueAsString(value); } @@ -30,14 +50,6 @@ public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptio } } - internal override int ReadWithQuotes(ref Utf8JsonReader reader) - { - return reader.GetInt32WithQuotes(); - } - - internal override void WriteWithQuotes(Utf8JsonWriter writer, int value, JsonSerializerOptions options, ref WriteStack state) - { - writer.WritePropertyName(value); - } + internal override bool IsInternalConverterForNumberType => true; } } 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 0c436242ed1b6..437bd094914cd 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 @@ -6,10 +6,29 @@ namespace System.Text.Json.Serialization.Converters internal sealed class Int64Converter : JsonConverter { public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetInt64(); + } + + public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value); + } + + internal override long ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetInt64WithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, long value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } + + internal override long ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) { if (reader.TokenType == JsonTokenType.String && - options != null && - (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + (JsonNumberHandling.AllowReadingFromString & handling) != 0) { return reader.GetInt64WithQuotes(); } @@ -17,9 +36,9 @@ public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer return reader.GetInt64(); } - public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options) + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, long value, JsonNumberHandling handling) { - if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + if ((JsonNumberHandling.WriteAsString & handling) != 0) { writer.WriteNumberValueAsString(value); } @@ -29,14 +48,6 @@ public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOpti } } - internal override long ReadWithQuotes(ref Utf8JsonReader reader) - { - return reader.GetInt64WithQuotes(); - } - - internal override void WriteWithQuotes(Utf8JsonWriter writer, long value, JsonSerializerOptions options, ref WriteStack state) - { - writer.WritePropertyName(value); - } + internal override bool IsInternalConverterForNumberType => true; } } 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 e2c0ddcc43e01..2d7b591dc9c81 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 @@ -40,5 +42,34 @@ 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); + } + } + + internal override bool IsInternalConverterForNumberType => _converter.IsInternalConverterForNumberType; } } 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 99040f8c0ff1f..e1a9d4a4859c1 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 @@ -6,10 +6,29 @@ namespace System.Text.Json.Serialization.Converters internal sealed class SByteConverter : JsonConverter { public override sbyte Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetSByte(); + } + + public override void Write(Utf8JsonWriter writer, sbyte value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value); + } + + internal override sbyte ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetSByteWithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, sbyte value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } + + internal override sbyte ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) { if (reader.TokenType == JsonTokenType.String && - options != null && - (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + (JsonNumberHandling.AllowReadingFromString & handling) != 0) { return reader.GetSByteWithQuotes(); } @@ -17,9 +36,9 @@ public override sbyte Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe return reader.GetSByte(); } - public override void Write(Utf8JsonWriter writer, sbyte value, JsonSerializerOptions options) + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, sbyte value, JsonNumberHandling handling) { - if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + if ((JsonNumberHandling.WriteAsString & handling) != 0) { writer.WriteNumberValueAsString(value); } @@ -29,14 +48,6 @@ public override void Write(Utf8JsonWriter writer, sbyte value, JsonSerializerOpt } } - internal override sbyte ReadWithQuotes(ref Utf8JsonReader reader) - { - return reader.GetSByteWithQuotes(); - } - - internal override void WriteWithQuotes(Utf8JsonWriter writer, sbyte value, JsonSerializerOptions options, ref WriteStack state) - { - writer.WritePropertyName(value); - } + internal override bool IsInternalConverterForNumberType => true; } } 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 05bca56dd874b..bd4fdeed38af3 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 @@ -7,27 +7,12 @@ internal sealed class SingleConverter : JsonConverter { public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.String && - options != null && - ((JsonConstants.ReadNumberOrFloatingConstantFromString) & options.NumberHandling) != 0) - { - return reader.GetSingleWithQuotes(); - } - return reader.GetSingle(); } public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) { - if (options != null && - ((JsonConstants.WriteNumberOrFloatingConstantAsString) & options.NumberHandling) != 0) - { - writer.WriteNumberValueAsString(value); - } - else - { - writer.WriteNumberValue(value); - } + writer.WriteNumberValue(value); } internal override float ReadWithQuotes(ref Utf8JsonReader reader) @@ -39,5 +24,40 @@ 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); + } + } + + internal override bool IsInternalConverterForNumberType => true; } } 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 b6414d39058e3..c59cf5accc486 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 @@ -6,10 +6,30 @@ namespace System.Text.Json.Serialization.Converters internal sealed class UInt16Converter : JsonConverter { public override ushort Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetUInt16(); + } + + public override void Write(Utf8JsonWriter writer, ushort value, JsonSerializerOptions options) + { + // For performance, lift up the writer implementation. + writer.WriteNumberValue((long)value); + } + + internal override ushort ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetUInt16WithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, ushort value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } + + internal override ushort ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) { if (reader.TokenType == JsonTokenType.String && - options != null && - (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + (JsonNumberHandling.AllowReadingFromString & handling) != 0) { return reader.GetUInt16WithQuotes(); } @@ -17,9 +37,9 @@ public override ushort Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS return reader.GetUInt16(); } - public override void Write(Utf8JsonWriter writer, ushort value, JsonSerializerOptions options) + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, ushort value, JsonNumberHandling handling) { - if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + if ((JsonNumberHandling.WriteAsString & handling) != 0) { writer.WriteNumberValueAsString(value); } @@ -30,14 +50,6 @@ public override void Write(Utf8JsonWriter writer, ushort value, JsonSerializerOp } } - internal override ushort ReadWithQuotes(ref Utf8JsonReader reader) - { - return reader.GetUInt16WithQuotes(); - } - - internal override void WriteWithQuotes(Utf8JsonWriter writer, ushort value, JsonSerializerOptions options, ref WriteStack state) - { - writer.WritePropertyName(value); - } + internal override bool IsInternalConverterForNumberType => true; } } 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 133354d2c1340..fc005fa279632 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 @@ -6,10 +6,30 @@ namespace System.Text.Json.Serialization.Converters internal sealed class UInt32Converter : JsonConverter { public override uint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetUInt32(); + } + + public override void Write(Utf8JsonWriter writer, uint value, JsonSerializerOptions options) + { + // For performance, lift up the writer implementation. + writer.WriteNumberValue((ulong)value); + } + + internal override uint ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetUInt32WithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, uint value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } + + internal override uint ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) { if (reader.TokenType == JsonTokenType.String && - options != null && - (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + (JsonNumberHandling.AllowReadingFromString & handling) != 0) { return reader.GetUInt32WithQuotes(); } @@ -17,9 +37,9 @@ public override uint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer return reader.GetUInt32(); } - public override void Write(Utf8JsonWriter writer, uint value, JsonSerializerOptions options) + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, uint value, JsonNumberHandling handling) { - if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + if ((JsonNumberHandling.WriteAsString & handling) != 0) { writer.WriteNumberValueAsString(value); } @@ -30,14 +50,6 @@ public override void Write(Utf8JsonWriter writer, uint value, JsonSerializerOpti } } - internal override uint ReadWithQuotes(ref Utf8JsonReader reader) - { - return reader.GetUInt32WithQuotes(); - } - - internal override void WriteWithQuotes(Utf8JsonWriter writer, uint value, JsonSerializerOptions options, ref WriteStack state) - { - writer.WritePropertyName(value); - } + internal override bool IsInternalConverterForNumberType => true; } } 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 8fdb8964f575f..d88a5c889ce45 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 @@ -6,10 +6,29 @@ namespace System.Text.Json.Serialization.Converters internal sealed class UInt64Converter : JsonConverter { public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetUInt64(); + } + + public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value); + } + + internal override ulong ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetUInt64WithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } + + internal override ulong ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) { if (reader.TokenType == JsonTokenType.String && - options != null && - (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) + (JsonNumberHandling.AllowReadingFromString & handling) != 0) { return reader.GetUInt64WithQuotes(); } @@ -17,9 +36,9 @@ public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe return reader.GetUInt64(); } - public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options) + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, ulong value, JsonNumberHandling handling) { - if (options != null && ((JsonNumberHandling.WriteAsString & options.NumberHandling) != 0)) + if ((JsonNumberHandling.WriteAsString & handling) != 0) { writer.WriteNumberValueAsString(value); } @@ -29,14 +48,6 @@ public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOpt } } - internal override ulong ReadWithQuotes(ref Utf8JsonReader reader) - { - return reader.GetUInt64WithQuotes(); - } - - internal override void WriteWithQuotes(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options, ref WriteStack state) - { - writer.WritePropertyName(value); - } + internal override bool IsInternalConverterForNumberType => true; } } 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 785abae8a6079..7b01b16fb4772 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 bdd5f7442671a..6f523b19b8294 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 @@ -92,6 +92,8 @@ public JsonClassInfo(Type type, JsonSerializerOptions options) Options); ClassType = converter.ClassType; + JsonNumberHandling? typeNumberHandling = GetNumberHandlingForType(Type); + PropertyInfoForClassInfo = CreatePropertyInfoForClassInfo(Type, runtimeType, converter, Options); switch (ClassType) @@ -127,7 +129,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 { @@ -153,7 +155,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 @@ -228,10 +230,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; @@ -573,5 +576,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 b2c34e14599a1..89ce02d824876 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 virtual bool IsInternalConverterForNumberType { get; } + /// /// 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 52dba1df4bb4d..1341a462e835e 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) { @@ -84,14 +78,30 @@ internal sealed override bool TryWriteAsObject(Utf8JsonWriter writer, object? va // Provide a default implementation for value converters. internal virtual bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state) { - Write(writer, value, options); + JsonNumberHandling? numberHandling = state.Current.NumberHandling; + if (numberHandling.HasValue) + { + WriteNumberWithCustomHandling(writer, value, numberHandling.Value); + } + else + { + Write(writer, value, options); + } return true; } // Provide a default implementation for value converters. internal virtual bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNull] out T value) { - value = Read(ref reader, typeToConvert, options); + JsonNumberHandling? numberHandling = state.Current.NumberHandling; + if (numberHandling.HasValue) + { + value = ReadNumberWithCustomHandling(ref reader, numberHandling.Value); + } + else + { + value = Read(ref reader, typeToConvert, options); + } return true; } @@ -127,11 +137,21 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali return true; } + JsonNumberHandling? numberHandling = state.Current.JsonClassInfo.PropertyInfoForClassInfo!.NumberHandling; + #if !DEBUG // For performance, only perform validation on internal converters on debug builds. if (IsInternalConverter) { - value = Read(ref reader, typeToConvert, options); + if (IsInternalConverterForNumberType) + { + Debug.Assert(numberHandling.HasValue); + value = ReadNumberWithCustomHandling(ref reader, numberHandling.Value); + } + else + { + value = Read(ref reader, typeToConvert, options); + } } else #endif @@ -140,7 +160,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 (numberHandling.HasValue && IsInternalConverterForNumberType) + { + value = ReadNumberWithCustomHandling(ref reader, numberHandling.Value); + } + else + { + value = Read(ref reader, typeToConvert, options); + } + VerifyRead( originalPropertyTokenType, originalPropertyDepth, @@ -269,7 +297,17 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions Debug.Assert(!state.IsContinuation); int originalPropertyDepth = writer.CurrentDepth; - Write(writer, value, options); + + JsonNumberHandling? numberHandling = state.Current.NumberHandling ?? state.Current.DeclaredJsonPropertyInfo!.NumberHandling; + if (IsInternalConverterForNumberType && numberHandling.HasValue) + { + WriteNumberWithCustomHandling(writer, value, numberHandling.Value); + } + else + { + Write(writer, value, options); + } + VerifyWrite(originalPropertyDepth, writer); } @@ -309,7 +347,17 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions int originalPropertyDepth = writer.CurrentDepth; - Write(writer, value, options); + JsonNumberHandling? numberHandling = state.Current.NumberHandling ?? state.Current.DeclaredJsonPropertyInfo!.NumberHandling; + if (IsInternalConverterForNumberType && numberHandling.HasValue) + { + WriteNumberWithCustomHandling(writer, value, numberHandling.Value); + } + else + { + Write(writer, value, options); + } + + // TODO, for internal converters, do we need to verify write here? VerifyWrite(originalPropertyDepth, writer); return true; } @@ -452,5 +500,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/JsonParameterInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfo.cs index 0ccdbeeafda1e..4687107f7f82b 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 38adc919f9ed0..d6db08fa57b88 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,59 @@ private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition, bool #pragma warning restore CS0618 // IgnoreNullValues is obsolete } + private void DetermineNumberHandling(JsonNumberHandling? parentTypeNumberHandling) + { + bool converterIsInternalAndForNumbers = ConverterBase.IsInternalConverterForNumberType; + 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 && + !converterIsInternalAndForNumbers && + ((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; + } + + IsNumberTypeWithCustomHandling = converterIsInternalAndForNumbers && NumberHandling != JsonNumberHandling.Strict; + } + public static TAttribute? GetAttribute(MemberInfo memberInfo) where TAttribute : Attribute { return (TAttribute?)memberInfo.GetCustomAttribute(typeof(TAttribute), inherit: false); @@ -182,13 +243,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 +256,7 @@ public virtual void Initialize( MemberInfo? memberInfo, JsonConverter converter, JsonIgnoreCondition? ignoreCondition, + JsonNumberHandling? parentTypeNumberHandling, JsonSerializerOptions options) { Debug.Assert(converter != null); @@ -344,5 +399,8 @@ public JsonClassInfo RuntimeClassInfo public bool ShouldSerialize { get; private set; } public bool ShouldDeserialize { get; private set; } public bool IsIgnored { get; private set; } + + public bool IsNumberTypeWithCustomHandling { 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 83c5ee116372d..45561c1396326 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 @@ -213,9 +215,13 @@ public override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref U { if (!isNullToken || !IgnoreDefaultValuesOnRead || !Converter.CanBeNull) { + JsonNumberHandling? numberHandling = state.Current.NumberHandling; // Optimize for internal converters by avoiding the extra call to TryRead. - T fastvalue = Converter.Read(ref reader, RuntimePropertyType!, Options); - Set!(obj, fastvalue!); + T fastValue = numberHandling.HasValue && Converter.IsInternalConverterForNumberType + ? Converter.ReadNumberWithCustomHandling(ref reader, numberHandling.Value) + : Converter.Read(ref reader, RuntimePropertyType!, Options); + + Set!(obj, fastValue!); } success = true; @@ -255,7 +261,10 @@ public override bool ReadJsonAsObject(ref ReadStack state, ref Utf8JsonReader re // Optimize for internal converters by avoiding the extra call to TryRead. if (Converter.CanUseDirectReadOrWrite) { - value = Converter.Read(ref reader, RuntimePropertyType!, Options); + JsonNumberHandling? numberHandling = state.Current.NumberHandling; + value = numberHandling.HasValue && Converter.IsInternalConverterForNumberType + ? Converter.ReadNumberWithCustomHandling(ref reader, numberHandling.Value) + : Converter.Read(ref reader, RuntimePropertyType!, Options); success = true; } else 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 04bb332121f9f..17a0669b2a4ab 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 4e7946872454c..30c3c0dd33dbf 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 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 0796280c369dc..860b49315a404 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/ReadStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs index c3b1ba7c38a74..87364988ceaea 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 @@ -109,15 +109,18 @@ public void Push() else { JsonClassInfo jsonClassInfo; + JsonNumberHandling? numberHandling = null; if (Current.JsonClassInfo.ClassType == ClassType.Object) { if (Current.JsonPropertyInfo != null) { jsonClassInfo = Current.JsonPropertyInfo.RuntimeClassInfo; + numberHandling = Current.JsonPropertyInfo.NumberHandling; } else { jsonClassInfo = Current.CtorArgumentState!.JsonParameterInfo!.RuntimeClassInfo; + numberHandling = Current.CtorArgumentState!.JsonParameterInfo!.NumberHandling; } } else if ((Current.JsonClassInfo.ClassType & (ClassType.Value | ClassType.NewValue)) != 0) @@ -135,6 +138,7 @@ public void Push() Current.JsonClassInfo = jsonClassInfo; Current.JsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo; + Current.NumberHandling = numberHandling; } } else if (_continuationCount == 1) 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 b0c333568bda3..3c179370293e7 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 { @@ -43,6 +44,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; @@ -89,6 +93,7 @@ public void Reset() CtorArgumentStateIndex = 0; CtorArgumentState = null; JsonClassInfo = null!; + NumberHandling = null; ObjectState = StackFrameObjectState.None; OriginalDepth = 0; OriginalTokenType = JsonTokenType.None; 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 ec90b0c75e021..01ced35ca0b7d 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 @@ -73,6 +73,7 @@ public JsonConverter Initialize(Type type, JsonSerializerOptions options, bool s if ((jsonClassInfo.ClassType & (ClassType.Enumerable | ClassType.Dictionary)) == 0) { Current.DeclaredJsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo; + Current.NumberHandling = Current.DeclaredJsonPropertyInfo.NumberHandling; } if (options.ReferenceHandler != null) @@ -96,13 +97,21 @@ public void Push() } else { - JsonClassInfo jsonClassInfo = Current.GetPolymorphicJsonPropertyInfo().RuntimeClassInfo; + JsonPropertyInfo jsonPropertyInfo = Current.GetPolymorphicJsonPropertyInfo(); + JsonClassInfo jsonClassInfo = jsonPropertyInfo.RuntimeClassInfo; + JsonNumberHandling? numberHandling = null; + + if (((ClassType.Enumerable | ClassType.Dictionary) & jsonClassInfo.ClassType) != 0) + { + numberHandling = jsonPropertyInfo.NumberHandling; + } AddCurrent(); Current.Reset(); Current.JsonClassInfo = jsonClassInfo; Current.DeclaredJsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo; + Current.NumberHandling = 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 9689568809b24..d5a671dd4794f 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 2194ab03fab14..58ea337c53bb8 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.Double.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs index a2641f207af25..328a6ccbc5da6 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 @@ -151,5 +151,50 @@ internal void WriteNumberValueAsString(double value) Debug.Assert(result); WriteNumberValueAsString(utf8Number.Slice(0, bytesWritten)); } + + internal void WriteFloatingPointConstant(double value) + { + Span utf8Number = stackalloc byte[JsonConstants.NegativeInfinityLiteralConstantLength]; + int bytesToWrite; + if (double.IsNaN(value)) + { + utf8Number[0] = (byte)'N'; + utf8Number[1] = (byte)'a'; + utf8Number[2] = (byte)'N'; + bytesToWrite = 3; + } + else if (double.IsPositiveInfinity(value)) + { + utf8Number[0] = (byte)'I'; + utf8Number[1] = (byte)'n'; + utf8Number[2] = (byte)'f'; + utf8Number[3] = (byte)'i'; + utf8Number[4] = (byte)'n'; + utf8Number[5] = (byte)'i'; + utf8Number[6] = (byte)'t'; + utf8Number[7] = (byte)'y'; + bytesToWrite = 8; + } + else if (double.IsNegativeInfinity(value)) + { + utf8Number[0] = (byte)'-'; + utf8Number[1] = (byte)'I'; + utf8Number[2] = (byte)'n'; + utf8Number[3] = (byte)'f'; + utf8Number[4] = (byte)'i'; + utf8Number[5] = (byte)'n'; + utf8Number[6] = (byte)'i'; + utf8Number[7] = (byte)'t'; + utf8Number[8] = (byte)'y'; + bytesToWrite = 9; + } + else + { + WriteNumberValue(value); + return; + } + + WriteNumberValueAsString(utf8Number.Slice(0, bytesToWrite)); + } } } 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 757f752ee97a0..4286d3a117744 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 @@ -151,5 +151,50 @@ internal void WriteNumberValueAsString(float value) Debug.Assert(result); WriteNumberValueAsString(utf8Number.Slice(0, bytesWritten)); } + + internal void WriteFloatingPointConstant(float value) + { + Span utf8Number = stackalloc byte[JsonConstants.NegativeInfinityLiteralConstantLength]; + int bytesToWrite; + if (float.IsNaN(value)) + { + utf8Number[0] = (byte)'N'; + utf8Number[1] = (byte)'a'; + utf8Number[2] = (byte)'N'; + bytesToWrite = 3; + } + else if (float.IsPositiveInfinity(value)) + { + utf8Number[0] = (byte)'I'; + utf8Number[1] = (byte)'n'; + utf8Number[2] = (byte)'f'; + utf8Number[3] = (byte)'i'; + utf8Number[4] = (byte)'n'; + utf8Number[5] = (byte)'i'; + utf8Number[6] = (byte)'t'; + utf8Number[7] = (byte)'y'; + bytesToWrite = 8; + } + else if (float.IsNegativeInfinity(value)) + { + utf8Number[0] = (byte)'-'; + utf8Number[1] = (byte)'I'; + utf8Number[2] = (byte)'n'; + utf8Number[3] = (byte)'f'; + utf8Number[4] = (byte)'i'; + utf8Number[5] = (byte)'n'; + utf8Number[6] = (byte)'i'; + utf8Number[7] = (byte)'t'; + utf8Number[8] = (byte)'y'; + bytesToWrite = 9; + } + else + { + WriteNumberValue(value); + return; + } + + WriteNumberValueAsString(utf8Number.Slice(0, bytesToWrite)); + } } } 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 15c02fe1c4b75..1c3101149b280 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 @@ -107,8 +107,8 @@ private void WriteNumberValueIndented(long value) BytesPending += bytesWritten; } - internal void WriteNumberValueAsString(int value) - => WriteNumberValueAsString((long)value); + //internal void WriteNumberValueAsString(int value) + // => WriteNumberValueAsString((long)value); internal void WriteNumberValueAsString(long value) { diff --git a/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs index 685972fe740e7..998d2426439af 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs @@ -1,10 +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.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text.Encodings.Web; using System.Text.Json.Tests; using Xunit; @@ -122,6 +127,27 @@ private static void PerformAsRootTypeSerialization(T number, string jsonWithN 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() { @@ -190,12 +216,17 @@ private static void RunAsCollectionElementTest(List numbers) jsonBuilder_NumbersAsNumbersAndStrings.Append("]"); jsonBuilder_NumbersAsNumbersAndStrings_Alternate.Append("]"); + string jsonNumbersAsStrings = jsonBuilder_NumbersAsStrings.ToString(); + PerformAsCollectionElementSerialization( numbers, jsonBuilder_NumbersAsNumbers.ToString(), - jsonBuilder_NumbersAsStrings.ToString(), + jsonNumbersAsStrings, jsonBuilder_NumbersAsNumbersAndStrings.ToString(), jsonBuilder_NumbersAsNumbersAndStrings_Alternate.ToString()); + + // Reflection based tests for every collection type. + RunAllCollectionsRoundTripTest(jsonNumbersAsStrings); } private static void PerformAsCollectionElementSerialization( @@ -205,20 +236,22 @@ private static void PerformAsCollectionElementSerialization( string json_NumbersAsNumbersAndStrings, string json_NumbersAsNumbersAndStrings_Alternate) { + List deserialized; + // Option: read from string // Deserialize - List deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbers, s_optionReadFromStr); - AssertListsEqual(numbers, deserialized); + deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbers, s_optionReadFromStr); + AssertIEnumerableEqual(numbers, deserialized); deserialized = JsonSerializer.Deserialize>(json_NumbersAsStrings, s_optionReadFromStr); - AssertListsEqual(numbers, deserialized); + AssertIEnumerableEqual(numbers, deserialized); deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings, s_optionReadFromStr); - AssertListsEqual(numbers, deserialized); + AssertIEnumerableEqual(numbers, deserialized); deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings_Alternate, s_optionReadFromStr); - AssertListsEqual(numbers, deserialized); + AssertIEnumerableEqual(numbers, deserialized); // Serialize Assert.Equal(json_NumbersAsNumbers, JsonSerializer.Serialize(numbers, s_optionReadFromStr)); @@ -227,7 +260,7 @@ private static void PerformAsCollectionElementSerialization( // Deserialize deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbers, s_optionWriteAsStr); - AssertListsEqual(numbers, deserialized); + AssertIEnumerableEqual(numbers, deserialized); Assert.Throws(() => JsonSerializer.Deserialize>(json_NumbersAsStrings, s_optionWriteAsStr)); @@ -242,27 +275,76 @@ private static void PerformAsCollectionElementSerialization( // Deserialize deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbers, s_optionReadAndWriteFromStr); - AssertListsEqual(numbers, deserialized); + AssertIEnumerableEqual(numbers, deserialized); deserialized = JsonSerializer.Deserialize>(json_NumbersAsStrings, s_optionReadAndWriteFromStr); - AssertListsEqual(numbers, deserialized); + AssertIEnumerableEqual(numbers, deserialized); deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings, s_optionReadAndWriteFromStr); - AssertListsEqual(numbers, deserialized); + AssertIEnumerableEqual(numbers, deserialized); deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings_Alternate, s_optionReadAndWriteFromStr); - AssertListsEqual(numbers, deserialized); + AssertIEnumerableEqual(numbers, deserialized); // Serialize Assert.Equal(json_NumbersAsStrings, JsonSerializer.Serialize(numbers, s_optionReadAndWriteFromStr)); } - private static void AssertListsEqual(List list1, List list2) + private static void AssertIEnumerableEqual(IEnumerable list1, IEnumerable list2) { - Assert.Equal(list1.Count, list2.Count); - for (int i = 0; i < list1.Count; i++) + IEnumerator enumerator1 = list1.GetEnumerator(); + IEnumerator enumerator2 = list2.GetEnumerator(); + + while (enumerator1.MoveNext()) { - Assert.Equal(list1[i], list2[i]); + 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); } } @@ -277,6 +359,7 @@ public static void Number_AsDictionaryElement_RoundTrip() // Serialize string serialized = JsonSerializer.Serialize(dict, s_optionReadAndWriteFromStr); + AssertDictionaryElements_StringValues(serialized); // Deserialize dict = JsonSerializer.Deserialize>(serialized, s_optionReadAndWriteFromStr); @@ -285,6 +368,83 @@ public static void Number_AsDictionaryElement_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] + public static void DictionariesRoundTripTest() + { + 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() { @@ -312,13 +472,90 @@ private class Class_With_NullableUInt64_And_Float } [Fact] - public static void FloatingPointConstants() + 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 FloatingPointConstants_Pass() { // Valid values - PerformFloatingPointSerialization("NaN", "NaN"); - PerformFloatingPointSerialization("Infinity", "Infinity"); - PerformFloatingPointSerialization("-Infinity", "-Infinity"); + 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); + break; + case "Infinity": + obj = JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants); + 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); + break; + default: + Assert.Throws(() => JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants)); + return; + } + + JsonTestHelper.AssertJsonEqual(testJson, JsonSerializer.Serialize(obj, s_optionsAllowFloatConstants)); + } + } + + [Fact] + public static void FloatingPointConstants_Fail() + { // Invalid values PerformFloatingPointSerialization("naN"); PerformFloatingPointSerialization("Nan"); @@ -345,40 +582,25 @@ public static void FloatingPointConstants() PerformFloatingPointSerialization("NaNa"); PerformFloatingPointSerialization("Infinitya"); PerformFloatingPointSerialization("-Infinitya"); - } - - private static void PerformFloatingPointSerialization(string testString, string constantAsString = null) - { - string constantAsJson = $@"""{constantAsString}"""; - string expectedJson = @$"{{""FloatNumber"":{constantAsJson},""DoubleNumber"":{constantAsJson}}}"; - - string testStringAsJson = $@"""{testString}"""; - string testJson = @$"{{""FloatNumber"":{testStringAsJson},""DoubleNumber"":{testStringAsJson}}}"; - StructWithNumbers obj; - switch (constantAsString) + static void PerformFloatingPointSerialization(string testString) { - case "NaN": - obj = JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants); - 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); - break; - case "-Infinity": - obj = JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants); - Assert.Equal(float.NegativeInfinity, obj.FloatNumber); - Assert.Equal(double.NegativeInfinity, obj.DoubleNumber); - break; - default: - Assert.Throws(() => JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants)); - return; + string testStringAsJson = $@"""{testString}"""; + string testJson = @$"{{""FloatNumber"":{testStringAsJson},""DoubleNumber"":{testStringAsJson}}}"; + Assert.Throws(() => JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants)); } + } + + [Fact] + public static void AllowFloatingPointConstants_WriteAsNumber_IfNotConstant() + { + float @float = 1; + // Not written as "1" + Assert.Equal("1", JsonSerializer.Serialize(@float, s_optionsAllowFloatConstants)); - JsonTestHelper.AssertJsonEqual(expectedJson, JsonSerializer.Serialize(obj, s_optionsAllowFloatConstants)); + double @double = 1; + // Not written as "1" + Assert.Equal("1", JsonSerializer.Serialize(@double, s_optionsAllowFloatConstants)); } private struct StructWithNumbers @@ -431,7 +653,6 @@ public static void FloatingPointConstants_IncompatibleNumber() AssertFloatingPointIncompatible_Fails(); AssertFloatingPointIncompatible_Fails(); AssertFloatingPointIncompatible_Fails(); - AssertFloatingPointIncompatible_Fails(); AssertFloatingPointIncompatible_Fails(); AssertFloatingPointIncompatible_Fails(); AssertFloatingPointIncompatible_Fails(); @@ -441,7 +662,6 @@ public static void FloatingPointConstants_IncompatibleNumber() AssertFloatingPointIncompatible_Fails(); AssertFloatingPointIncompatible_Fails(); AssertFloatingPointIncompatible_Fails(); - AssertFloatingPointIncompatible_Fails(); AssertFloatingPointIncompatible_Fails(); } @@ -449,9 +669,9 @@ private static void AssertFloatingPointIncompatible_Fails() { string[] testCases = new[] { - "NaN", - "Infinity", - "-Infinity", + @"""NaN""", + @"""Infinity""", + @"""-Infinity""", }; foreach (string test in testCases) @@ -502,24 +722,7 @@ private static void AssertUnsupportedFormatThrows() } [Fact] - public static void CustomConverterOverridesBuiltInLogic() - { - var options = new JsonSerializerOptions(s_optionReadAndWriteFromStr) - { - Converters = { new ConverterForInt32() } - }; - - 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)); - } - - [Fact] - public static void EncodingTest() + public static void EscapingTest() { // Cause all characters to be escaped. var encoderSettings = new TextEncoderSettings(); @@ -531,20 +734,20 @@ public static void EncodingTest() Encoder = encoder }; - PerformEncodingTest(JsonNumberTestData.Bytes, options); - PerformEncodingTest(JsonNumberTestData.SBytes, options); - PerformEncodingTest(JsonNumberTestData.Shorts, options); - PerformEncodingTest(JsonNumberTestData.Ints, options); - PerformEncodingTest(JsonNumberTestData.Longs, options); - PerformEncodingTest(JsonNumberTestData.UShorts, options); - PerformEncodingTest(JsonNumberTestData.UInts, options); - PerformEncodingTest(JsonNumberTestData.ULongs, options); - PerformEncodingTest(JsonNumberTestData.Floats, options); - PerformEncodingTest(JsonNumberTestData.Doubles, options); - PerformEncodingTest(JsonNumberTestData.Decimals, options); + 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 PerformEncodingTest(List numbers, JsonSerializerOptions options) + private static void PerformEscapingTest(List numbers, JsonSerializerOptions options) { // All input characters are escaped IEnumerable numbersAsStrings = numbers.Select(num => GetNumberAsString(num)); @@ -586,109 +789,498 @@ private static void PerformEncodingTest(List numbers, JsonSerializerOption } } + [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() { - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - PerformNullabilityTest(); - } - - private static void PerformNullabilityTest() + 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}"""; - if (Nullable.GetUnderlyingType(typeof(T)) != null) + 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() { - Assert.Null(JsonSerializer.Deserialize(nullAsJson, s_optionReadAndWriteFromStr)); - Assert.Equal(nullAsJson, JsonSerializer.Serialize(default(T))); + string json = @"{""MyList"":[""1"",""2""]}"; + ClassWithSimpleCollectionProperty obj = global::System.Text.Json.JsonSerializer.Deserialize>(json); + Assert.Equal(json, global::System.Text.Json.JsonSerializer.Serialize(obj)); } - else + } + + 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 { - Assert.Throws(() => JsonSerializer.Deserialize(nullAsJson, s_optionReadAndWriteFromStr)); - Assert.Equal("0", JsonSerializer.Serialize(default(T))); - } + MyList = new List { new ClassWith_StrictAttribute { Float = 1 } } + }; + Assert.Equal(@"{""MyList"":[{""Float"":1}]}", JsonSerializer.Serialize(obj)); + } - Assert.Throws(() => JsonSerializer.Deserialize(nullAsQuotedJson, s_optionReadAndWriteFromStr)); + 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 obj1 = new MyCustomListWrapper + { + List = new MyCustomList { 1 } + }; + Assert.Equal(@"{""List"":[1]}", JsonSerializer.Serialize(obj1)); + } + + public class MyCustomListWrapper + { + [JsonNumberHandling(JsonNumberHandling.Strict)] + public MyCustomList List { get; set; } } [Fact] - public static void ConvertersHaveNullChecks() + public static void HandlingOnMemberOverridesHandlingOnType_Dictionary() { - RunConvertersHaveNullChecksTest(JsonNumberTestData.Bytes[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.SBytes[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.Shorts[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.Ints[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.Longs[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.UShorts[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.UInts[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.ULongs[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.Floats[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.Doubles[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.Decimals[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableBytes[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableSBytes[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableShorts[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableInts[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableLongs[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableUShorts[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableUInts[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableULongs[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableFloats[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableDoubles[0]); - RunConvertersHaveNullChecksTest(JsonNumberTestData.NullableDecimals[0]); + 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)); } - private static void RunConvertersHaveNullChecksTest(T number) + public class MyCustomDictionaryWrapper { - string numberAsJsonNumber = $"{number}"; - string numberAsJsonString = $@"""{number}"""; - var options = new JsonSerializerOptions(); + [JsonNumberHandling(JsonNumberHandling.Strict)] + public MyCustomDictionary Dictionary { get; set; } + } - var converter = (JsonConverter)options.GetConverter(typeof(T)); + [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; } + } - var reader_JsonNumber = new Utf8JsonReader(Encoding.UTF8.GetBytes(numberAsJsonNumber)); - var reader_JsonString = new Utf8JsonReader(Encoding.UTF8.GetBytes(numberAsJsonString)); + [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())); + Assert.Contains(typeof(ConverterForInt32).ToString(), exAsStr); + Assert.Contains(typeof(ClassWith_NumberHandlingOn_Property_WithCustomConverter).ToString(), exAsStr); + } - reader_JsonNumber.Read(); - reader_JsonString.Read(); + public class ClassWith_NumberHandlingOn_Property_WithCustomConverter + { + [JsonNumberHandling(JsonNumberHandling.Strict)] + [JsonConverter(typeof(ConverterForInt32))] + public int MyProp { get; set; } + } - T val = converter.Read(ref reader_JsonNumber, typeof(T), options: null); - Assert.Equal(number, val); + [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); + } - try + [JsonNumberHandling(JsonNumberHandling.Strict)] + [JsonConverter(typeof(ConverterForMyType))] + public class ClassWith_NumberHandlingOn_Type_WithCustomConverter + { + } + + private class ConverterForMyType : JsonConverter + { + [return: MaybeNull] + public override ClassWith_NumberHandlingOn_Type_WithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - converter.Read(ref reader_JsonString, typeof(T), options: null); - Assert.True(false, "InvalidOperationException expected."); + throw new NotImplementedException(); } - catch (InvalidOperationException) { } - using (MemoryStream stream = new MemoryStream()) + public override void Write(Utf8JsonWriter writer, ClassWith_NumberHandlingOn_Type_WithCustomConverter value, JsonSerializerOptions options) { - using (Utf8JsonWriter writer = new Utf8JsonWriter(stream)) - { - converter.Write(writer, number, options: null); - } - Assert.Equal(numberAsJsonNumber, Encoding.UTF8.GetString(stream.ToArray())); + throw new NotImplementedException(); } } + + [Fact] + public static void CustomConverterOverridesBuiltInLogic() + { + var options = new JsonSerializerOptions(s_optionReadAndWriteFromStr) + { + Converters = { new ConverterForInt32() } + }; + + 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)); + } } } 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 eb371a431949c..3efdfde63502b 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 adccf4fc1f4b2..7ef238b63d453 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 8496e4b5817e8..34f96f79fc94b 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 + } + } } From 8385f18a28463a8141e3ea6db16618b68c54f211 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Sun, 19 Jul 2020 08:33:59 -0700 Subject: [PATCH 3/4] Address review feedback + fixups --- .../Text/Json/Reader/JsonReaderHelper.cs | 12 +- .../Attributes/JsonNumberHandlingAttribute.cs | 9 +- .../Converters/Collection/ArrayConverter.cs | 18 +-- .../Collection/DictionaryDefaultConverter.cs | 64 +++----- .../DictionaryOfTKeyTValueConverter.cs | 26 +--- .../Collection/IEnumerableDefaultConverter.cs | 43 ++---- .../Converters/Collection/IListConverter.cs | 45 +++--- .../Converters/Collection/ListOfTConverter.cs | 18 +-- .../Json/Serialization/JsonConverterOfT.cs | 49 ++---- .../Json/Serialization/JsonNumberHandling.cs | 8 +- .../Json/Serialization/JsonPropertyInfo.cs | 6 +- .../Json/Serialization/JsonPropertyInfoOfT.cs | 15 +- .../JsonSerializer.Read.Helpers.cs | 6 + .../Serialization/JsonSerializerOptions.cs | 5 + .../Text/Json/Serialization/ReadStack.cs | 19 ++- .../Text/Json/Serialization/ReadStackFrame.cs | 2 +- .../Text/Json/Serialization/WriteStack.cs | 22 +-- .../Utf8JsonWriter.WriteValues.Decimal.cs | 2 +- .../Utf8JsonWriter.WriteValues.Double.cs | 4 +- .../Utf8JsonWriter.WriteValues.Float.cs | 4 +- ...Utf8JsonWriter.WriteValues.SignedNumber.cs | 2 +- .../Utf8JsonWriter.WriteValues.String.cs | 2 +- ...f8JsonWriter.WriteValues.UnsignedNumber.cs | 2 +- .../Serialization/NumberHandlingTests.cs | 140 +++++++++++++++++- .../tests/Serialization/Object.ReadTests.cs | 4 +- 25 files changed, 272 insertions(+), 255 deletions(-) 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 341da8493a9dd..a879caaf1fa4a 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 @@ -324,14 +324,16 @@ public static bool TryGetEscapedGuid(ReadOnlySpan source, out Guid value) public static char GetFloatingPointStandardParseFormat(ReadOnlySpan span) { - foreach (byte token in 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; } @@ -353,7 +355,7 @@ public static bool TryGetFloatingPointConstant(ReadOnlySpan span, out floa return true; } } - else if (span.Length == 9) + else if (span.Length == JsonConstants.NegativeInfinityLiteralConstantLength) { if (ValueIsNegativeInfinity(span)) { @@ -384,7 +386,7 @@ public static bool TryGetFloatingPointConstant(ReadOnlySpan span, out doub return true; } } - else if (span.Length == 9) + else if (span.Length == JsonConstants.NegativeInfinityLiteralConstantLength) { if (ValueIsNegativeInfinity(span)) { @@ -418,7 +420,7 @@ private static bool ValueIsPositiveInfinity(ReadOnlySpan span) private static bool ValueIsNegativeInfinity(ReadOnlySpan span) { - Debug.Assert(span.Length == 9); + Debug.Assert(span.Length == JsonConstants.NegativeInfinityLiteralConstantLength); return span[0] == (byte)'-' && span[1] == (byte)'I' && span[2] == (byte)'n' && 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 index 5b9896f9dbcf6..581d4aacd766e 100644 --- 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 @@ -18,6 +18,13 @@ public sealed class JsonNumberHandlingAttribute : JsonAttribute /// /// Initializes a new instance of . /// - public JsonNumberHandlingAttribute(JsonNumberHandling handling) => Handling = handling; + 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 a70b3b02cde44..9147243ef95be 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,24 +38,12 @@ 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. - - JsonNumberHandling? numberHandling = state.Current.NumberHandling; - if (numberHandling.HasValue && elementConverter.IsInternalConverterForNumberType) - { - for (; index < array.Length; index++) - { - elementConverter.WriteNumberWithCustomHandling(writer, array[index], numberHandling.Value); - } - } - else + for (; index < array.Length; index++) { - for (; index < array.Length; index++) - { - elementConverter.Write(writer, array[index], options); - } + elementConverter.Write(writer, array[index], options); } } else 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 e4efa0dad7371..36d3acd1606f2 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 @@ -73,53 +73,28 @@ internal sealed override bool OnTryRead( CreateCollection(ref reader, ref state); JsonConverter valueConverter = _valueConverter ??= GetValueConverter(elementClassInfo); - - if (valueConverter.CanUseDirectReadOrWrite) + if (valueConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { - // Use number handling on the enumerable property which was preserved when pushing it as a new ReadStackFrame, or - // on the enumerable itself (likely custom). Handling on property wins. - JsonNumberHandling? numberHandling = state.Current.NumberHandling ?? state.Current.JsonPropertyInfo!.NumberHandling; - if (numberHandling.HasValue && valueConverter.IsInternalConverterForNumberType) - { - // Process all elements. - while (true) - { - // Read the key name. - reader.ReadWithVerify(); - if (reader.TokenType == JsonTokenType.EndObject) - { - break; - } - // Read method would have thrown if otherwise. - Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); - - TKey key = ReadDictionaryKey(ref reader, ref state); - // Read the value and add. - reader.ReadWithVerify(); - TValue element = valueConverter.ReadNumberWithCustomHandling(ref reader, numberHandling.Value); - Add(key, element!, options, ref state); - } - } - else + // Process all elements. + while (true) { - // Process all elements. - while (true) + // Read the key name. + reader.ReadWithVerify(); + + if (reader.TokenType == JsonTokenType.EndObject) { - // Read the key name. - reader.ReadWithVerify(); - if (reader.TokenType == JsonTokenType.EndObject) - { - break; - } - // Read method would have thrown if otherwise. - Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); - - TKey key = ReadDictionaryKey(ref reader, ref state); - // Read the value and add. - reader.ReadWithVerify(); - TValue element = valueConverter.Read(ref reader, s_valueType, options); - Add(key, element!, options, ref state); + break; } + + // Read method would have thrown if otherwise. + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + + TKey key = ReadDictionaryKey(ref reader, ref state); + + // Read the value and add. + reader.ReadWithVerify(); + TValue element = valueConverter.Read(ref reader, s_valueType, options); + Add(key, element!, options, ref state); } } else @@ -322,9 +297,6 @@ internal sealed override bool OnTryWrite( } } - // If this enumerable is not a property on an object, or no handling was specified on the property - // use the handling specified on the enumrarable type (likely custom) or globally on JsonSerializerOptions. - state.Current.NumberHandling ??= state.Current.JsonClassInfo.PropertyInfoForClassInfo!.NumberHandling; state.Current.DeclaredJsonPropertyInfo = state.Current.JsonClassInfo.ElementClassInfo!.PropertyInfoForClassInfo; } 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 1063519d9da46..c89e1f9be07ee 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 @@ -53,28 +53,16 @@ protected internal override bool OnWriteResume( JsonConverter keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options); JsonConverter valueConverter = _valueConverter ??= GetValueConverter(elementClassInfo); - if (!state.SupportContinuation && valueConverter.CanUseDirectReadOrWrite) + + if (!state.SupportContinuation && valueConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { - JsonNumberHandling? numberHandling = state.Current.NumberHandling; // Fast path that avoids validation and extra indirection. - if (numberHandling.HasValue && valueConverter.IsInternalConverterForNumberType) - { - do - { - TKey key = enumerator.Current.Key; - keyConverter.WriteWithQuotes(writer, key, options, ref state); - valueConverter.WriteNumberWithCustomHandling(writer, enumerator.Current.Value, numberHandling.Value); - } while (enumerator.MoveNext()); - } - else + do { - do - { - TKey key = enumerator.Current.Key; - keyConverter.WriteWithQuotes(writer, key, options, ref state); - valueConverter.Write(writer, enumerator.Current.Value, options); - } while (enumerator.MoveNext()); - } + TKey key = enumerator.Current.Key; + keyConverter.WriteWithQuotes(writer, key, options, ref state); + valueConverter.Write(writer, enumerator.Current.Value, options); + } while (enumerator.MoveNext()); } else { 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 92fa6753f255f..250ba4c14713f 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 @@ -53,40 +53,20 @@ internal override bool OnTryRead( CreateCollection(ref reader, ref state, options); JsonConverter elementConverter = GetElementConverter(elementClassInfo); - if (elementConverter.CanUseDirectReadOrWrite) + if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { - // Use number handling on the enumerable property which was preserved when pushing it as a new ReadStackFrame, or - // on the enumerable itself (likely custom). Handling on property wins. - JsonNumberHandling? numberHandling = state.Current.NumberHandling ?? state.Current.JsonPropertyInfo!.NumberHandling; - if (numberHandling.HasValue && elementConverter.IsInternalConverterForNumberType) - { - // Fast path that avoids validation and extra indirection. - while (true) - { - reader.ReadWithVerify(); - if (reader.TokenType == JsonTokenType.EndArray) - { - break; - } - // Obtain the CLR value from the JSON and apply to the object. - TElement element = elementConverter.ReadNumberWithCustomHandling(ref reader, numberHandling.Value); - Add(element!, ref state); - } - } - else + // Fast path that avoids validation and extra indirection. + while (true) { - // Fast path that avoids validation and extra indirection. - while (true) + reader.ReadWithVerify(); + if (reader.TokenType == JsonTokenType.EndArray) { - reader.ReadWithVerify(); - if (reader.TokenType == JsonTokenType.EndArray) - { - break; - } - // Obtain the CLR value from the JSON and apply to the object. - TElement element = elementConverter.Read(ref reader, elementConverter.TypeToConvert, options); - Add(element!, ref state); + break; } + + // Obtain the CLR value from the JSON and apply to the object. + TElement element = elementConverter.Read(ref reader, elementConverter.TypeToConvert, options); + Add(element!, ref state); } } else @@ -297,9 +277,6 @@ internal sealed override bool OnTryWrite( state.Current.MetadataPropertyName = metadata; } - // If this enumerable is not a property on an object, or no handling was specified on the property - // use the handling specified on the enumrarable type (likely custom) or globally on JsonSerializerOptions. - state.Current.NumberHandling ??= state.Current.JsonClassInfo.PropertyInfoForClassInfo!.NumberHandling; state.Current.DeclaredJsonPropertyInfo = state.Current.JsonClassInfo.ElementClassInfo!.PropertyInfoForClassInfo; } 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 3f6516cdfbb2d..606ddfa7ca3d7 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/ListOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ListOfTConverter.cs index 8e9eb1f3c579a..52362693749eb 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,24 +33,12 @@ 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. - - JsonNumberHandling? numberHandling = state.Current.NumberHandling; - if (numberHandling.HasValue && elementConverter.IsInternalConverterForNumberType) - { - for (; index < list.Count; index++) - { - elementConverter.WriteNumberWithCustomHandling(writer, list[index], numberHandling.Value); - } - } - else + for (; index < list.Count; index++) { - for (; index < list.Count; index++) - { - elementConverter.Write(writer, list[index], options); - } + elementConverter.Write(writer, list[index], options); } } else 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 1341a462e835e..dfd81ffd871b2 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 @@ -78,30 +78,14 @@ internal sealed override bool TryWriteAsObject(Utf8JsonWriter writer, object? va // Provide a default implementation for value converters. internal virtual bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state) { - JsonNumberHandling? numberHandling = state.Current.NumberHandling; - if (numberHandling.HasValue) - { - WriteNumberWithCustomHandling(writer, value, numberHandling.Value); - } - else - { - Write(writer, value, options); - } + Write(writer, value, options); return true; } // Provide a default implementation for value converters. internal virtual bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNull] out T value) { - JsonNumberHandling? numberHandling = state.Current.NumberHandling; - if (numberHandling.HasValue) - { - value = ReadNumberWithCustomHandling(ref reader, numberHandling.Value); - } - else - { - value = Read(ref reader, typeToConvert, options); - } + value = Read(ref reader, typeToConvert, options); return true; } @@ -137,16 +121,13 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali return true; } - JsonNumberHandling? numberHandling = state.Current.JsonClassInfo.PropertyInfoForClassInfo!.NumberHandling; - #if !DEBUG // For performance, only perform validation on internal converters on debug builds. if (IsInternalConverter) { - if (IsInternalConverterForNumberType) + if (IsInternalConverterForNumberType && state.Current.NumberHandling != null) { - Debug.Assert(numberHandling.HasValue); - value = ReadNumberWithCustomHandling(ref reader, numberHandling.Value); + value = ReadNumberWithCustomHandling(ref reader, state.Current.NumberHandling.Value); } else { @@ -160,9 +141,9 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali int originalPropertyDepth = reader.CurrentDepth; long originalPropertyBytesConsumed = reader.BytesConsumed; - if (numberHandling.HasValue && IsInternalConverterForNumberType) + if (IsInternalConverterForNumberType && state.Current.NumberHandling != null) { - value = ReadNumberWithCustomHandling(ref reader, numberHandling.Value); + value = ReadNumberWithCustomHandling(ref reader, state.Current.NumberHandling.Value); } else { @@ -297,17 +278,7 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions Debug.Assert(!state.IsContinuation); int originalPropertyDepth = writer.CurrentDepth; - - JsonNumberHandling? numberHandling = state.Current.NumberHandling ?? state.Current.DeclaredJsonPropertyInfo!.NumberHandling; - if (IsInternalConverterForNumberType && numberHandling.HasValue) - { - WriteNumberWithCustomHandling(writer, value, numberHandling.Value); - } - else - { - Write(writer, value, options); - } - + Write(writer, value, options); VerifyWrite(originalPropertyDepth, writer); } @@ -347,17 +318,15 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions int originalPropertyDepth = writer.CurrentDepth; - JsonNumberHandling? numberHandling = state.Current.NumberHandling ?? state.Current.DeclaredJsonPropertyInfo!.NumberHandling; - if (IsInternalConverterForNumberType && numberHandling.HasValue) + if (IsInternalConverterForNumberType && state.Current.NumberHandling != null) { - WriteNumberWithCustomHandling(writer, value, numberHandling.Value); + WriteNumberWithCustomHandling(writer, value, state.Current.NumberHandling.Value); } else { Write(writer, value, options); } - // TODO, for internal converters, do we need to verify write here? VerifyWrite(originalPropertyDepth, writer); return true; } 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 index 798e66a4b6f9a..687b4e0dc3482 100644 --- 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 @@ -12,22 +12,22 @@ public enum JsonNumberHandling /// /// Numbers will only be read from tokens and will only be written as JSON numbers (without quotes). /// - Strict = 0, + Strict = 0x0, /// /// Numbers can be read from tokens. /// Does not prevent numbers from being read from token. /// - AllowReadingFromString = 1, + AllowReadingFromString = 0x1, /// /// Numbers will be written as JSON strings (with quotes), not as JSON numbers. /// - WriteAsString = 2, + WriteAsString = 0x2, /// /// Floating-point constants represented as /// tokens such as "NaN", "Infinity", "-Infinity", can be read when reading, /// and such CLR values such as , , /// will be written as their corresponding JSON string representations. /// - AllowNamedFloatingPointLiterals = 4 + AllowNamedFloatingPointLiterals = 0x4 } } 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 d6db08fa57b88..10b92e26fac96 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs @@ -184,7 +184,6 @@ private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition, bool private void DetermineNumberHandling(JsonNumberHandling? parentTypeNumberHandling) { - bool converterIsInternalAndForNumbers = ConverterBase.IsInternalConverterForNumberType; if (IsForClassInfo) { if (parentTypeNumberHandling != null && !ConverterBase.IsInternalConverter) @@ -211,7 +210,7 @@ private void DetermineNumberHandling(JsonNumberHandling? parentTypeNumberHandlin JsonNumberHandlingAttribute? attribute = GetAttribute(MemberInfo); if (attribute != null && - !converterIsInternalAndForNumbers && + !ConverterBase.IsInternalConverterForNumberType && ((ClassType.Enumerable | ClassType.Dictionary) & ClassType) == 0) { ThrowHelper.ThrowInvalidOperationException_NumberHandlingOnPropertyInvalid(this); @@ -231,8 +230,6 @@ private void DetermineNumberHandling(JsonNumberHandling? parentTypeNumberHandlin NumberHandling = handling; } - - IsNumberTypeWithCustomHandling = converterIsInternalAndForNumbers && NumberHandling != JsonNumberHandling.Strict; } public static TAttribute? GetAttribute(MemberInfo memberInfo) where TAttribute : Attribute @@ -400,7 +397,6 @@ public JsonClassInfo RuntimeClassInfo public bool ShouldDeserialize { get; private set; } public bool IsIgnored { get; private set; } - public bool IsNumberTypeWithCustomHandling { 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 45561c1396326..85962cdb0bf51 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 @@ -211,16 +211,12 @@ 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) { - JsonNumberHandling? numberHandling = state.Current.NumberHandling; // Optimize for internal converters by avoiding the extra call to TryRead. - T fastValue = numberHandling.HasValue && Converter.IsInternalConverterForNumberType - ? Converter.ReadNumberWithCustomHandling(ref reader, numberHandling.Value) - : Converter.Read(ref reader, RuntimePropertyType!, Options); - + T fastValue = Converter.Read(ref reader, RuntimePropertyType!, Options); Set!(obj, fastValue!); } @@ -259,12 +255,9 @@ 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) { - JsonNumberHandling? numberHandling = state.Current.NumberHandling; - value = numberHandling.HasValue && Converter.IsInternalConverterForNumberType - ? Converter.ReadNumberWithCustomHandling(ref reader, numberHandling.Value) - : Converter.Read(ref reader, RuntimePropertyType!, Options); + value = Converter.Read(ref reader, RuntimePropertyType!, Options); success = true; } else 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 30c3c0dd33dbf..8b01c199e6e47 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 @@ -36,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.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 3dd16c3c19f07..4217f61a61fc8 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 @@ -276,6 +276,11 @@ public JsonNumberHandling NumberHandling set { VerifyMutable(); + + if (!JsonSerializer.IsValidNumberHandlingValue(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } _numberHandling = value; } } 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 87364988ceaea..ac03e3b6adba6 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,27 +111,27 @@ public void Push() else { JsonClassInfo jsonClassInfo; - JsonNumberHandling? numberHandling = null; + JsonNumberHandling? numberHandling = Current.NumberHandling; + if (Current.JsonClassInfo.ClassType == ClassType.Object) { if (Current.JsonPropertyInfo != null) { jsonClassInfo = Current.JsonPropertyInfo.RuntimeClassInfo; - numberHandling = Current.JsonPropertyInfo.NumberHandling; } else { jsonClassInfo = Current.CtorArgumentState!.JsonParameterInfo!.RuntimeClassInfo; - numberHandling = Current.CtorArgumentState!.JsonParameterInfo!.NumberHandling; } } - 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!; } @@ -138,7 +140,8 @@ public void Push() Current.JsonClassInfo = jsonClassInfo; Current.JsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo; - Current.NumberHandling = numberHandling; + // Allow number handling on property to win over handling on type. + Current.NumberHandling = numberHandling ?? Current.JsonPropertyInfo.NumberHandling; } } else if (_continuationCount == 1) @@ -163,7 +166,7 @@ public void Push() } } - SetConstrutorArgumentState(); + SetConstructorArgumentState(); } public void Pop(bool success) @@ -214,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: @@ -332,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 3c179370293e7..785610283f52a 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 @@ -63,6 +63,7 @@ public void EndProperty() MetadataId = null; // No need to clear these since they are overwritten each time: + // NumberHandling // UseExtensionProperty } @@ -93,7 +94,6 @@ public void Reset() CtorArgumentStateIndex = 0; CtorArgumentState = null; JsonClassInfo = null!; - NumberHandling = null; ObjectState = StackFrameObjectState.None; OriginalDepth = 0; OriginalTokenType = JsonTokenType.None; 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 01ced35ca0b7d..e3378711eb1eb 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,13 +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.NumberHandling = Current.DeclaredJsonPropertyInfo.NumberHandling; - } + Current.JsonClassInfo = jsonClassInfo; + Current.DeclaredJsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo; + Current.NumberHandling = Current.DeclaredJsonPropertyInfo.NumberHandling; if (options.ReferenceHandler != null) { @@ -97,21 +94,16 @@ public void Push() } else { - JsonPropertyInfo jsonPropertyInfo = Current.GetPolymorphicJsonPropertyInfo(); - JsonClassInfo jsonClassInfo = jsonPropertyInfo.RuntimeClassInfo; - JsonNumberHandling? numberHandling = null; - - if (((ClassType.Enumerable | ClassType.Dictionary) & jsonClassInfo.ClassType) != 0) - { - numberHandling = jsonPropertyInfo.NumberHandling; - } + JsonClassInfo jsonClassInfo = Current.GetPolymorphicJsonPropertyInfo().RuntimeClassInfo; + JsonNumberHandling? numberHandling = Current.NumberHandling; AddCurrent(); Current.Reset(); Current.JsonClassInfo = jsonClassInfo; Current.DeclaredJsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo; - Current.NumberHandling = numberHandling; + // 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/Writer/Utf8JsonWriter.WriteValues.Decimal.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Decimal.cs index b9305df12bf04..a30af2e6546ad 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 @@ -99,7 +99,7 @@ internal void WriteNumberValueAsString(decimal value) Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatDecimalLength]; bool result = Utf8Formatter.TryFormat(value, utf8Number, out int bytesWritten); Debug.Assert(result); - WriteNumberValueAsString(utf8Number.Slice(0, bytesWritten)); + 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 328a6ccbc5da6..6fb292637086a 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 @@ -149,7 +149,7 @@ internal void WriteNumberValueAsString(double value) Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatDoubleLength]; bool result = TryFormatDouble(value, utf8Number, out int bytesWritten); Debug.Assert(result); - WriteNumberValueAsString(utf8Number.Slice(0, bytesWritten)); + WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesWritten)); } internal void WriteFloatingPointConstant(double value) @@ -194,7 +194,7 @@ internal void WriteFloatingPointConstant(double value) return; } - WriteNumberValueAsString(utf8Number.Slice(0, bytesToWrite)); + WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesToWrite)); } } } 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 4286d3a117744..b0b4906208937 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 @@ -149,7 +149,7 @@ internal void WriteNumberValueAsString(float value) Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatSingleLength]; bool result = TryFormatSingle(value, utf8Number, out int bytesWritten); Debug.Assert(result); - WriteNumberValueAsString(utf8Number.Slice(0, bytesWritten)); + WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesWritten)); } internal void WriteFloatingPointConstant(float value) @@ -194,7 +194,7 @@ internal void WriteFloatingPointConstant(float value) return; } - WriteNumberValueAsString(utf8Number.Slice(0, bytesToWrite)); + WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesToWrite)); } } } 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 1c3101149b280..ee4576a903193 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 @@ -115,7 +115,7 @@ internal void WriteNumberValueAsString(long value) Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatInt64Length]; bool result = Utf8Formatter.TryFormat(value, utf8Number, out int bytesWritten); Debug.Assert(result); - WriteNumberValueAsString(utf8Number.Slice(0, bytesWritten)); + 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 7e594c3b96fca..15cb0b8d1af5e 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 @@ -354,7 +354,7 @@ private void WriteStringEscapeValue(ReadOnlySpan utf8Value, int firstEscap /// Writes a number as a JSON string. The string value is not escaped. /// /// - internal void WriteNumberValueAsString(ReadOnlySpan utf8Value) + internal void WriteNumberValueAsStringUnescaped(ReadOnlySpan utf8Value) { // The value has been validated prior to calling this method. 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 ed37e86db6f5a..09c2478dea360 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 @@ -117,7 +117,7 @@ internal void WriteNumberValueAsString(ulong value) Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatUInt64Length]; bool result = Utf8Formatter.TryFormat(value, utf8Number, out int bytesWritten); Debug.Assert(result); - WriteNumberValueAsString(utf8Number.Slice(0, bytesWritten)); + WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesWritten)); } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs index 998d2426439af..4832b84d7e42f 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs @@ -6,12 +6,12 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; -using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; using System.Text.Encodings.Web; using System.Text.Json.Tests; +using Microsoft.DotNet.XUnitExtensions; using Xunit; namespace System.Text.Json.Serialization.Tests @@ -400,6 +400,7 @@ private static void AssertDictionaryElements_StringValues(string serialized) } [Fact] + [ActiveIssue("", typeof(PlatformDetection), nameof(PlatformDetection.IsMonoInterpreter))] public static void DictionariesRoundTripTest() { RunAllDictionariessRoundTripTest(JsonNumberTestData.ULongs); @@ -513,6 +514,33 @@ public MyClassWithNumbers(ulong? @ulong, List 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() { @@ -533,16 +561,28 @@ static void PerformFloatingPointSerialization(string testString) 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)); @@ -550,6 +590,7 @@ static void PerformFloatingPointSerialization(string testString) } JsonTestHelper.AssertJsonEqual(testJson, JsonSerializer.Serialize(obj, s_optionsAllowFloatConstants)); + JsonTestHelper.AssertJsonEqual(testJson, JsonSerializer.Serialize(obj, s_optionWriteAsStr)); } } @@ -588,6 +629,7 @@ static void PerformFloatingPointSerialization(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)); } } @@ -709,7 +751,7 @@ private static void AssertUnsupportedFormatThrows() { string[] testCases = new[] { - " $123.46", // Currency + "$123.46", // Currency "100.00 %", // Percent "1234,57", // Fixed point "00FF", // Hexadecimal @@ -752,6 +794,7 @@ private static void PerformEscapingTest(List numbers, JsonSerializerOption // 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); @@ -763,7 +806,44 @@ private static void PerformEscapingTest(List numbers, JsonSerializerOption // Every number is written as a string, and custom escaping is not honored. string serialized = JsonSerializer.Serialize(deserialized, options); - var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(serialized)); + 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()) { @@ -1254,7 +1334,6 @@ public class ClassWith_NumberHandlingOn_Type_WithCustomConverter private class ConverterForMyType : JsonConverter { - [return: MaybeNull] public override ClassWith_NumberHandlingOn_Type_WithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw new NotImplementedException(); @@ -1271,7 +1350,7 @@ public static void CustomConverterOverridesBuiltInLogic() { var options = new JsonSerializerOptions(s_optionReadAndWriteFromStr) { - Converters = { new ConverterForInt32() } + Converters = { new ConverterForInt32(), new ConverterForFloat() } }; string json = @"""32"""; @@ -1281,6 +1360,55 @@ public static void CustomConverterOverridesBuiltInLogic() // 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 544634359a968..6a36e96d3818b 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(); From 7be06ab55ac5459b4f79289e9c040461bdeddf9d Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Mon, 20 Jul 2020 17:42:27 -0700 Subject: [PATCH 4/4] Address review feedback ii --- .../src/System/Text/Json/JsonConstants.cs | 5 +- .../Text/Json/Reader/JsonReaderHelper.cs | 49 ++-------- .../Text/Json/Reader/Utf8JsonReader.TryGet.cs | 4 +- .../Converters/Value/ByteConverter.cs | 7 +- .../Converters/Value/DecimalConverter.cs | 7 +- .../Converters/Value/DoubleConverter.cs | 7 +- .../Converters/Value/Int16Converter.cs | 7 +- .../Converters/Value/Int32Converter.cs | 7 +- .../Converters/Value/Int64Converter.cs | 7 +- .../Converters/Value/NullableConverter.cs | 3 +- .../Converters/Value/SByteConverter.cs | 7 +- .../Converters/Value/SingleConverter.cs | 8 +- .../Converters/Value/UInt16Converter.cs | 7 +- .../Converters/Value/UInt32Converter.cs | 7 +- .../Converters/Value/UInt64Converter.cs | 7 +- .../Text/Json/Serialization/JsonConverter.cs | 2 +- .../Json/Serialization/JsonNumberHandling.cs | 7 +- .../Utf8JsonWriter.WriteValues.Double.cs | 31 +----- .../Utf8JsonWriter.WriteValues.Float.cs | 31 +----- ...Utf8JsonWriter.WriteValues.SignedNumber.cs | 3 - ...f8JsonWriter.WriteValues.UnsignedNumber.cs | 3 - .../Serialization/NumberHandlingTests.cs | 98 +++++++++---------- 22 files changed, 130 insertions(+), 184 deletions(-) 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 0c851cb1aaddf..2e1bb4e16cfe1 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 }; @@ -83,7 +87,6 @@ internal static class JsonConstants (DateTimeParseNumFractionDigits - DateTimeNumFractionDigits)); // Like StandardFormat 'O' for DateTimeOffset, but allowing 9 additional (up to 16) fraction digits. public const int MinimumDateTimeParseLength = 10; // YYYY-MM-DD public const int MaximumEscapedDateTimeOffsetParseLength = MaxExpansionFactorWhileEscaping * MaximumDateTimeOffsetParseLength; - public const int NegativeInfinityLiteralConstantLength = 9; // Character count for -Infinity is 9 internal const char ScientificNotationFormat = 'e'; 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 a879caaf1fa4a..dbea5c0fea0c1 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 @@ -341,7 +341,7 @@ public static bool TryGetFloatingPointConstant(ReadOnlySpan span, out floa { if (span.Length == 3) { - if (ValueIsNan(span)) + if (span.SequenceEqual(JsonConstants.NaNValue)) { value = float.NaN; return true; @@ -349,15 +349,15 @@ public static bool TryGetFloatingPointConstant(ReadOnlySpan span, out floa } else if (span.Length == 8) { - if (ValueIsPositiveInfinity(span)) + if (span.SequenceEqual(JsonConstants.PositiveInfinityValue)) { value = float.PositiveInfinity; return true; } } - else if (span.Length == JsonConstants.NegativeInfinityLiteralConstantLength) + else if (span.Length == 9) { - if (ValueIsNegativeInfinity(span)) + if (span.SequenceEqual(JsonConstants.NegativeInfinityValue)) { value = float.NegativeInfinity; return true; @@ -372,7 +372,7 @@ public static bool TryGetFloatingPointConstant(ReadOnlySpan span, out doub { if (span.Length == 3) { - if (ValueIsNan(span)) + if (span.SequenceEqual(JsonConstants.NaNValue)) { value = double.NaN; return true; @@ -380,15 +380,15 @@ public static bool TryGetFloatingPointConstant(ReadOnlySpan span, out doub } else if (span.Length == 8) { - if (ValueIsPositiveInfinity(span)) + if (span.SequenceEqual(JsonConstants.PositiveInfinityValue)) { value = double.PositiveInfinity; return true; } } - else if (span.Length == JsonConstants.NegativeInfinityLiteralConstantLength) + else if (span.Length == 9) { - if (ValueIsNegativeInfinity(span)) + if (span.SequenceEqual(JsonConstants.NegativeInfinityValue)) { value = double.NegativeInfinity; return true; @@ -398,38 +398,5 @@ public static bool TryGetFloatingPointConstant(ReadOnlySpan span, out doub value = 0; return false; } - - private static bool ValueIsNan(ReadOnlySpan span) - { - Debug.Assert(span.Length == 3); - return span[0] == (byte)'N' && span[1] == (byte)'a' && span[2] == (byte)'N'; - } - - private static bool ValueIsPositiveInfinity(ReadOnlySpan span) - { - Debug.Assert(span.Length == 8); - return span[0] == (byte)'I' && - span[1] == (byte)'n' && - span[2] == (byte)'f' && - span[3] == (byte)'i' && - span[4] == (byte)'n' && - span[5] == (byte)'i' && - span[6] == (byte)'t' && - span[7] == (byte)'y'; - } - - private static bool ValueIsNegativeInfinity(ReadOnlySpan span) - { - Debug.Assert(span.Length == JsonConstants.NegativeInfinityLiteralConstantLength); - return span[0] == (byte)'-' && - span[1] == (byte)'I' && - span[2] == (byte)'n' && - span[3] == (byte)'f' && - span[4] == (byte)'i' && - span[5] == (byte)'n' && - span[6] == (byte)'i' && - span[7] == (byte)'t' && - span[8] == (byte)'y'; - } } } 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 5b338c5091a1d..d08cbcbc28dd0 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 @@ -427,7 +427,7 @@ internal float GetSingleWithQuotes() && 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 implmentation. + // 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)) { @@ -487,7 +487,7 @@ internal double GetDoubleWithQuotes() && 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 implmentation. + // 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)) { 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 71a5330622872..f02b3ac8d70d5 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(); @@ -46,7 +51,5 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, byte writer.WriteNumberValue(value); } } - - internal override bool IsInternalConverterForNumberType => true; } } 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 897555bb6e238..559079eec617a 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(); @@ -47,7 +52,5 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, deci writer.WriteNumberValue(value); } } - - internal override bool IsInternalConverterForNumberType => true; } } 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 6da670fb285c4..7b929b10e3397 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(); @@ -57,7 +62,5 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, doub writer.WriteNumberValue(value); } } - - internal override bool IsInternalConverterForNumberType => true; } } 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 a1a2ecf30fda6..f62da456a3a48 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs @@ -5,6 +5,11 @@ 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(); @@ -49,7 +54,5 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, shor writer.WriteNumberValue((long)value); } } - - internal override bool IsInternalConverterForNumberType => true; } } 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 c95894dffcee2..85d7fb3c6aa90 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(); @@ -49,7 +54,5 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, int writer.WriteNumberValue((long)value); } } - - internal override bool IsInternalConverterForNumberType => true; } } 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 437bd094914cd..48725dccbeecc 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(); @@ -47,7 +52,5 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, long writer.WriteNumberValue(value); } } - - internal override bool IsInternalConverterForNumberType => true; } } 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 2d7b591dc9c81..44e4270a26165 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 @@ -14,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) @@ -69,7 +70,5 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, T? v _converter.WriteNumberWithCustomHandling(writer, value.Value, handling); } } - - internal override bool IsInternalConverterForNumberType => _converter.IsInternalConverterForNumberType; } } 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 e1a9d4a4859c1..0896bf97527ef 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(); @@ -47,7 +52,5 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, sbyt writer.WriteNumberValue(value); } } - - internal override bool IsInternalConverterForNumberType => true; } } 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 bd4fdeed38af3..9b7e6fad328c6 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(); @@ -57,7 +63,5 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, floa writer.WriteNumberValue(value); } } - - internal override bool IsInternalConverterForNumberType => true; } } 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 c59cf5accc486..249d2a059ffa1 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(); @@ -49,7 +54,5 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, usho writer.WriteNumberValue((long)value); } } - - internal override bool IsInternalConverterForNumberType => true; } } 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 fc005fa279632..2c6ea5e07d6b4 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(); @@ -49,7 +54,5 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, uint writer.WriteNumberValue((ulong)value); } } - - internal override bool IsInternalConverterForNumberType => true; } } 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 d88a5c889ce45..3d94f296de13f 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(); @@ -47,7 +52,5 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, ulon writer.WriteNumberValue(value); } } - - internal override bool IsInternalConverterForNumberType => true; } } 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 89ce02d824876..4526db14a36a2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs @@ -52,7 +52,7 @@ internal JsonConverter() { } /// /// Whether the converter is built-in and handles a number type. /// - internal virtual bool IsInternalConverterForNumberType { get; } + internal bool IsInternalConverterForNumberType; /// /// Loosely-typed ReadCore() that forwards to strongly-typed ReadCore(). diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberHandling.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberHandling.cs index 687b4e0dc3482..09be0bfc94c81 100644 --- 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 @@ -23,10 +23,9 @@ public enum JsonNumberHandling /// WriteAsString = 0x2, /// - /// Floating-point constants represented as - /// tokens such as "NaN", "Infinity", "-Infinity", can be read when reading, - /// and such CLR values such as , , - /// will be written as their corresponding JSON string representations. + /// 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/Writer/Utf8JsonWriter.WriteValues.Double.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs index 6fb292637086a..f8a46a31468ca 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 @@ -154,47 +154,22 @@ internal void WriteNumberValueAsString(double value) internal void WriteFloatingPointConstant(double value) { - Span utf8Number = stackalloc byte[JsonConstants.NegativeInfinityLiteralConstantLength]; - int bytesToWrite; if (double.IsNaN(value)) { - utf8Number[0] = (byte)'N'; - utf8Number[1] = (byte)'a'; - utf8Number[2] = (byte)'N'; - bytesToWrite = 3; + WriteNumberValueAsStringUnescaped(JsonConstants.NaNValue); } else if (double.IsPositiveInfinity(value)) { - utf8Number[0] = (byte)'I'; - utf8Number[1] = (byte)'n'; - utf8Number[2] = (byte)'f'; - utf8Number[3] = (byte)'i'; - utf8Number[4] = (byte)'n'; - utf8Number[5] = (byte)'i'; - utf8Number[6] = (byte)'t'; - utf8Number[7] = (byte)'y'; - bytesToWrite = 8; + WriteNumberValueAsStringUnescaped(JsonConstants.PositiveInfinityValue); } else if (double.IsNegativeInfinity(value)) { - utf8Number[0] = (byte)'-'; - utf8Number[1] = (byte)'I'; - utf8Number[2] = (byte)'n'; - utf8Number[3] = (byte)'f'; - utf8Number[4] = (byte)'i'; - utf8Number[5] = (byte)'n'; - utf8Number[6] = (byte)'i'; - utf8Number[7] = (byte)'t'; - utf8Number[8] = (byte)'y'; - bytesToWrite = 9; + WriteNumberValueAsStringUnescaped(JsonConstants.NegativeInfinityValue); } else { WriteNumberValue(value); - return; } - - WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesToWrite)); } } } 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 b0b4906208937..2f046d872f697 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 @@ -154,47 +154,22 @@ internal void WriteNumberValueAsString(float value) internal void WriteFloatingPointConstant(float value) { - Span utf8Number = stackalloc byte[JsonConstants.NegativeInfinityLiteralConstantLength]; - int bytesToWrite; if (float.IsNaN(value)) { - utf8Number[0] = (byte)'N'; - utf8Number[1] = (byte)'a'; - utf8Number[2] = (byte)'N'; - bytesToWrite = 3; + WriteNumberValueAsStringUnescaped(JsonConstants.NaNValue); } else if (float.IsPositiveInfinity(value)) { - utf8Number[0] = (byte)'I'; - utf8Number[1] = (byte)'n'; - utf8Number[2] = (byte)'f'; - utf8Number[3] = (byte)'i'; - utf8Number[4] = (byte)'n'; - utf8Number[5] = (byte)'i'; - utf8Number[6] = (byte)'t'; - utf8Number[7] = (byte)'y'; - bytesToWrite = 8; + WriteNumberValueAsStringUnescaped(JsonConstants.PositiveInfinityValue); } else if (float.IsNegativeInfinity(value)) { - utf8Number[0] = (byte)'-'; - utf8Number[1] = (byte)'I'; - utf8Number[2] = (byte)'n'; - utf8Number[3] = (byte)'f'; - utf8Number[4] = (byte)'i'; - utf8Number[5] = (byte)'n'; - utf8Number[6] = (byte)'i'; - utf8Number[7] = (byte)'t'; - utf8Number[8] = (byte)'y'; - bytesToWrite = 9; + WriteNumberValueAsStringUnescaped(JsonConstants.NegativeInfinityValue); } else { WriteNumberValue(value); - return; } - - WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesToWrite)); } } } 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 ee4576a903193..2d6120a6a8768 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 @@ -107,9 +107,6 @@ private void WriteNumberValueIndented(long value) BytesPending += bytesWritten; } - //internal void WriteNumberValueAsString(int value) - // => WriteNumberValueAsString((long)value); - internal void WriteNumberValueAsString(long value) { Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatInt64Length]; 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 09c2478dea360..d3cf96947db46 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 @@ -109,9 +109,6 @@ private void WriteNumberValueIndented(ulong value) BytesPending += bytesWritten; } - internal void WriteNumberValueAsString(uint value) - => WriteNumberValueAsString((ulong)value); - internal void WriteNumberValueAsString(ulong value) { Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatUInt64Length]; diff --git a/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs index 4832b84d7e42f..60acb54094d6f 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs @@ -8,10 +8,8 @@ using System.Collections.ObjectModel; using System.Globalization; using System.Linq; -using System.Runtime.CompilerServices; using System.Text.Encodings.Web; using System.Text.Json.Tests; -using Microsoft.DotNet.XUnitExtensions; using Xunit; namespace System.Text.Json.Serialization.Tests @@ -400,8 +398,8 @@ private static void AssertDictionaryElements_StringValues(string serialized) } [Fact] - [ActiveIssue("", typeof(PlatformDetection), nameof(PlatformDetection.IsMonoInterpreter))] - public static void DictionariesRoundTripTest() + [ActiveIssue("https://github.com/dotnet/runtime/issues/39674", typeof(PlatformDetection), nameof(PlatformDetection.IsMonoInterpreter))] + public static void DictionariesRoundTrip() { RunAllDictionariessRoundTripTest(JsonNumberTestData.ULongs); RunAllDictionariessRoundTripTest(JsonNumberTestData.Floats); @@ -421,7 +419,6 @@ private static void RunAllDictionariessRoundTripTest(List numbers) jsonBuilder_NumbersAsStrings.Append($"{jsonWithNumberAsString}:"); jsonBuilder_NumbersAsStrings.Append($"{jsonWithNumberAsString},"); - } jsonBuilder_NumbersAsStrings.Remove(jsonBuilder_NumbersAsStrings.Length - 1, 1); @@ -514,7 +511,6 @@ public MyClassWithNumbers(ulong? @ulong, List listOfFloats) } } - [Fact] public static void Number_AsObjectWithParameterizedCtor_PropHasAttribute() { @@ -594,43 +590,35 @@ static void PerformFloatingPointSerialization(string testString) } } - [Fact] - public static void FloatingPointConstants_Fail() - { - // Invalid values - PerformFloatingPointSerialization("naN"); - PerformFloatingPointSerialization("Nan"); - PerformFloatingPointSerialization("NAN"); - - PerformFloatingPointSerialization("+Infinity"); - PerformFloatingPointSerialization("+infinity"); - PerformFloatingPointSerialization("infinity"); - PerformFloatingPointSerialization("infinitY"); - PerformFloatingPointSerialization("INFINITY"); - PerformFloatingPointSerialization("+INFINITY"); - - PerformFloatingPointSerialization("-infinity"); - PerformFloatingPointSerialization("-infinitY"); - PerformFloatingPointSerialization("-INFINITY"); - - PerformFloatingPointSerialization(" NaN"); - PerformFloatingPointSerialization(" Infinity"); - PerformFloatingPointSerialization(" -Infinity"); - PerformFloatingPointSerialization("NaN "); - PerformFloatingPointSerialization("Infinity "); - PerformFloatingPointSerialization("-Infinity "); - PerformFloatingPointSerialization("a-Infinity"); - PerformFloatingPointSerialization("NaNa"); - PerformFloatingPointSerialization("Infinitya"); - PerformFloatingPointSerialization("-Infinitya"); - - static void PerformFloatingPointSerialization(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)); - } + [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] @@ -645,6 +633,17 @@ public static void AllowFloatingPointConstants_WriteAsNumber_IfNotConstant() 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; } @@ -822,9 +821,9 @@ private static void AssertListNumbersEscaped(string json) else { Assert.Equal(JsonTokenType.String, reader.TokenType); - //#if BUILDING_INBOX_LIBRARY - // Assert.True(reader.ValueSpan.Contains((byte)'\\')); - //#else +#if BUILDING_INBOX_LIBRARY + Assert.True(reader.ValueSpan.Contains((byte)'\\')); +#else bool foundBackSlash = false; foreach (byte val in reader.ValueSpan) { @@ -836,7 +835,7 @@ private static void AssertListNumbersEscaped(string json) } Assert.True(foundBackSlash, "Expected escape token."); - //#endif +#endif } } } @@ -1237,11 +1236,11 @@ public static void HandlingOnMemberOverridesHandlingOnType_Enumerable() string json = @"{""List"":[""1""]}"; Assert.Throws(() => JsonSerializer.Deserialize(json)); - var obj1 = new MyCustomListWrapper + var obj = new MyCustomListWrapper { List = new MyCustomList { 1 } }; - Assert.Equal(@"{""List"":[1]}", JsonSerializer.Serialize(obj1)); + Assert.Equal(@"{""List"":[1]}", JsonSerializer.Serialize(obj)); } public class MyCustomListWrapper @@ -1300,6 +1299,7 @@ public static void Attribute_NotAllowed_On_Property_WithCustomConverter() 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); }