diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs
index 0ab83e8b526561..0de194ebde5bca 100644
--- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs
+++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs
@@ -244,6 +244,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults defaults) {
public bool IgnoreReadOnlyFields { get { throw null; } set { } }
public bool IncludeFields { get { throw null; } set { } }
public int MaxDepth { get { throw null; } set { } }
+ public System.Text.Json.Serialization.JsonNumberHandling NumberHandling { get { throw null; } set { } }
public bool PropertyNameCaseInsensitive { get { throw null; } set { } }
public System.Text.Json.JsonNamingPolicy? PropertyNamingPolicy { get { throw null; } set { } }
public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } }
@@ -496,6 +497,14 @@ public enum JsonIgnoreCondition
WhenWritingDefault = 2,
WhenWritingNull = 3,
}
+ [System.FlagsAttribute]
+ public enum JsonNumberHandling
+ {
+ Strict = 0,
+ AllowReadingFromString = 1,
+ WriteAsString = 2,
+ AllowNamedFloatingPointLiterals = 4,
+ }
public abstract partial class JsonAttribute : System.Attribute
{
protected JsonAttribute() { }
@@ -533,6 +542,12 @@ protected internal JsonConverter() { }
public abstract T Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options);
public abstract void Write(System.Text.Json.Utf8JsonWriter writer, T value, System.Text.Json.JsonSerializerOptions options);
}
+ [System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Struct | System.AttributeTargets.Property | System.AttributeTargets.Field, AllowMultiple = false)]
+ public sealed partial class JsonNumberHandlingAttribute : System.Text.Json.Serialization.JsonAttribute
+ {
+ public JsonNumberHandlingAttribute(System.Text.Json.Serialization.JsonNumberHandling handling) { }
+ public System.Text.Json.Serialization.JsonNumberHandling Handling { get { throw null; } }
+ }
[System.AttributeUsageAttribute(System.AttributeTargets.Constructor, AllowMultiple = false)]
public sealed partial class JsonConstructorAttribute : System.Text.Json.Serialization.JsonAttribute
{
diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx
index 41cf9de418c483..d5db3cfc6fb1b0 100644
--- a/src/libraries/System.Text.Json/src/Resources/Strings.resx
+++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx
@@ -536,4 +536,10 @@
The ignore condition 'JsonIgnoreCondition.WhenWritingNull' is not valid on value-type member '{0}' on type '{1}'. Consider using 'JsonIgnoreCondition.WhenWritingDefault'.
+
+ 'JsonNumberHandlingAttribute' cannot be placed on a property, field, or type that is handled by a custom converter. See usage(s) of converter '{0}' on type '{1}'.
+
+
+ When 'JsonNumberHandlingAttribute' is placed on a property or field, the property or field must be a number or a collection. See member '{0}' on type '{1}'.
+
\ No newline at end of file
diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj
index e381a1b381d464..e787defd06be02 100644
--- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj
+++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj
@@ -60,6 +60,7 @@
+
@@ -135,6 +136,7 @@
+
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs
index 2bfc41c6bd912c..2e1bb4e16cfe17 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs
@@ -37,6 +37,10 @@ internal static class JsonConstants
public static ReadOnlySpan FalseValue => new byte[] { (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e' };
public static ReadOnlySpan NullValue => new byte[] { (byte)'n', (byte)'u', (byte)'l', (byte)'l' };
+ public static ReadOnlySpan NaNValue => new byte[] { (byte)'N', (byte)'a', (byte)'N' };
+ public static ReadOnlySpan PositiveInfinityValue => new byte[] { (byte)'I', (byte)'n', (byte)'f', (byte)'i', (byte)'n', (byte)'i', (byte)'t', (byte)'y' };
+ public static ReadOnlySpan NegativeInfinityValue => new byte[] { (byte)'-', (byte)'I', (byte)'n', (byte)'f', (byte)'i', (byte)'n', (byte)'i', (byte)'t', (byte)'y' };
+
// Used to search for the end of a number
public static ReadOnlySpan Delimiters => new byte[] { ListSeparator, CloseBrace, CloseBracket, Space, LineFeed, CarriageReturn, Tab, Slash };
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs
index fcd7f0d3fb8da7..dbea5c0fea0c10 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs
@@ -321,5 +321,82 @@ public static bool TryGetEscapedGuid(ReadOnlySpan source, out Guid value)
value = default;
return false;
}
+
+ public static char GetFloatingPointStandardParseFormat(ReadOnlySpan span)
+ {
+ // Assume that 'e/E' is closer to the end.
+ int startIndex = span.Length - 1;
+ for (int i = startIndex; i >= 0; i--)
+ {
+ byte token = span[i];
+ if (token == 'E' || token == 'e')
+ {
+ return JsonConstants.ScientificNotationFormat;
+ }
+ }
+ return default;
+ }
+
+ public static bool TryGetFloatingPointConstant(ReadOnlySpan span, out float value)
+ {
+ if (span.Length == 3)
+ {
+ if (span.SequenceEqual(JsonConstants.NaNValue))
+ {
+ value = float.NaN;
+ return true;
+ }
+ }
+ else if (span.Length == 8)
+ {
+ if (span.SequenceEqual(JsonConstants.PositiveInfinityValue))
+ {
+ value = float.PositiveInfinity;
+ return true;
+ }
+ }
+ else if (span.Length == 9)
+ {
+ if (span.SequenceEqual(JsonConstants.NegativeInfinityValue))
+ {
+ value = float.NegativeInfinity;
+ return true;
+ }
+ }
+
+ value = 0;
+ return false;
+ }
+
+ public static bool TryGetFloatingPointConstant(ReadOnlySpan span, out double value)
+ {
+ if (span.Length == 3)
+ {
+ if (span.SequenceEqual(JsonConstants.NaNValue))
+ {
+ value = double.NaN;
+ return true;
+ }
+ }
+ else if (span.Length == 8)
+ {
+ if (span.SequenceEqual(JsonConstants.PositiveInfinityValue))
+ {
+ value = double.PositiveInfinity;
+ return true;
+ }
+ }
+ else if (span.Length == 9)
+ {
+ if (span.SequenceEqual(JsonConstants.NegativeInfinityValue))
+ {
+ value = double.NegativeInfinity;
+ return true;
+ }
+ }
+
+ value = 0;
+ return false;
+ }
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs
index dbc52d94b79d57..d08cbcbc28dd06 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs
@@ -416,11 +416,38 @@ public float GetSingle()
internal float GetSingleWithQuotes()
{
ReadOnlySpan span = GetUnescapedSpan();
- if (!TryGetSingleCore(out float value, span))
+
+ if (JsonReaderHelper.TryGetFloatingPointConstant(span, out float value))
{
- throw ThrowHelper.GetFormatException(NumericType.Single);
+ return value;
}
- return value;
+
+ char numberFormat = JsonReaderHelper.GetFloatingPointStandardParseFormat(span);
+ if (Utf8Parser.TryParse(span, out value, out int bytesConsumed, numberFormat)
+ && span.Length == bytesConsumed)
+ {
+ // NETCOREAPP implementation of the TryParse method above permits case-insenstive variants of the
+ // float constants "NaN", "Infinity", "-Infinity". This differs from the NETFRAMEWORK implementation.
+ // The following logic reconciles the two implementations to enforce consistent behavior.
+ if (!float.IsNaN(value) && !float.IsPositiveInfinity(value) && !float.IsNegativeInfinity(value))
+ {
+ return value;
+ }
+ }
+
+ throw ThrowHelper.GetFormatException(NumericType.Single);
+ }
+
+ internal float GetSingleFloatingPointConstant()
+ {
+ ReadOnlySpan span = GetUnescapedSpan();
+
+ if (JsonReaderHelper.TryGetFloatingPointConstant(span, out float value))
+ {
+ return value;
+ }
+
+ throw ThrowHelper.GetFormatException(NumericType.Single);
}
///
@@ -449,11 +476,38 @@ public double GetDouble()
internal double GetDoubleWithQuotes()
{
ReadOnlySpan span = GetUnescapedSpan();
- if (!TryGetDoubleCore(out double value, span))
+
+ if (JsonReaderHelper.TryGetFloatingPointConstant(span, out double value))
{
- throw ThrowHelper.GetFormatException(NumericType.Double);
+ return value;
}
- return value;
+
+ char numberFormat = JsonReaderHelper.GetFloatingPointStandardParseFormat(span);
+ if (Utf8Parser.TryParse(span, out value, out int bytesConsumed, numberFormat)
+ && span.Length == bytesConsumed)
+ {
+ // NETCOREAPP implmentation of the TryParse method above permits case-insenstive variants of the
+ // float constants "NaN", "Infinity", "-Infinity". This differs from the NETFRAMEWORK implementation.
+ // The following logic reconciles the two implementations to enforce consistent behavior.
+ if (!double.IsNaN(value) && !double.IsPositiveInfinity(value) && !double.IsNegativeInfinity(value))
+ {
+ return value;
+ }
+ }
+
+ throw ThrowHelper.GetFormatException(NumericType.Double);
+ }
+
+ internal double GetDoubleFloatingPointConstant()
+ {
+ ReadOnlySpan span = GetUnescapedSpan();
+
+ if (JsonReaderHelper.TryGetFloatingPointConstant(span, out double value))
+ {
+ return value;
+ }
+
+ throw ThrowHelper.GetFormatException(NumericType.Double);
}
///
@@ -482,11 +536,15 @@ public decimal GetDecimal()
internal decimal GetDecimalWithQuotes()
{
ReadOnlySpan span = GetUnescapedSpan();
- if (!TryGetDecimalCore(out decimal value, span))
+
+ char numberFormat = JsonReaderHelper.GetFloatingPointStandardParseFormat(span);
+ if (Utf8Parser.TryParse(span, out decimal value, out int bytesConsumed, numberFormat)
+ && span.Length == bytesConsumed)
{
- throw ThrowHelper.GetFormatException(NumericType.Decimal);
+ return value;
}
- return value;
+
+ throw ThrowHelper.GetFormatException(NumericType.Decimal);
}
///
@@ -919,13 +977,8 @@ public bool TryGetSingle(out float value)
throw ThrowHelper.GetInvalidOperationException_ExpectedNumber(TokenType);
}
- ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
- return TryGetSingleCore(out value, span);
- }
+ ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;;
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal bool TryGetSingleCore(out float value, ReadOnlySpan span)
- {
if (Utf8Parser.TryParse(span, out float tmp, out int bytesConsumed, _numberFormat)
&& span.Length == bytesConsumed)
{
@@ -955,12 +1008,7 @@ public bool TryGetDouble(out double value)
}
ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
- return TryGetDoubleCore(out value, span);
- }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal bool TryGetDoubleCore(out double value, ReadOnlySpan span)
- {
if (Utf8Parser.TryParse(span, out double tmp, out int bytesConsumed, _numberFormat)
&& span.Length == bytesConsumed)
{
@@ -990,12 +1038,7 @@ public bool TryGetDecimal(out decimal value)
}
ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
- return TryGetDecimalCore(out value, span);
- }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal bool TryGetDecimalCore(out decimal value, ReadOnlySpan span)
- {
if (Utf8Parser.TryParse(span, out decimal tmp, out int bytesConsumed, _numberFormat)
&& span.Length == bytesConsumed)
{
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonNumberHandlingAttribute.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonNumberHandlingAttribute.cs
new file mode 100644
index 00000000000000..581d4aacd766e4
--- /dev/null
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonNumberHandlingAttribute.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Text.Json.Serialization
+{
+ ///
+ /// When placed on a type, property, or field, indicates what
+ /// settings should be used when serializing or deserialing numbers.
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
+ public sealed class JsonNumberHandlingAttribute : JsonAttribute
+ {
+ ///
+ /// Indicates what settings should be used when serializing or deserialing numbers.
+ ///
+ public JsonNumberHandling Handling { get; }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public JsonNumberHandlingAttribute(JsonNumberHandling handling)
+ {
+ if (!JsonSerializer.IsValidNumberHandlingValue(handling))
+ {
+ throw new ArgumentOutOfRangeException(nameof(handling));
+ }
+ Handling = handling;
+ }
+ }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs
index 4337037817f050..9147243ef95be1 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs
@@ -38,7 +38,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value,
int index = state.Current.EnumeratorIndex;
JsonConverter elementConverter = GetElementConverter(ref state);
- if (elementConverter.CanUseDirectReadOrWrite)
+ if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null)
{
// Fast path that avoids validation and extra indirection.
for (; index < array.Length; index++)
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs
index cd20a06206db83..36d3acd1606f20 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs
@@ -29,7 +29,9 @@ protected virtual void ConvertCollection(ref ReadStack state, JsonSerializerOpti
///
protected virtual void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state) { }
- internal override Type ElementType => typeof(TValue);
+ private static Type s_valueType = typeof(TValue);
+
+ internal override Type ElementType => s_valueType;
protected Type KeyType = typeof(TKey);
// For string keys we don't use a key converter
@@ -39,9 +41,9 @@ protected virtual void CreateCollection(ref Utf8JsonReader reader, ref ReadStack
protected JsonConverter? _keyConverter;
protected JsonConverter? _valueConverter;
- protected static JsonConverter GetValueConverter(JsonClassInfo classInfo)
+ protected static JsonConverter GetValueConverter(JsonClassInfo elementClassInfo)
{
- JsonConverter converter = (JsonConverter)classInfo.ElementClassInfo!.PropertyInfoForClassInfo.ConverterBase;
+ JsonConverter converter = (JsonConverter)elementClassInfo.PropertyInfoForClassInfo.ConverterBase;
Debug.Assert(converter != null); // It should not be possible to have a null converter at this point.
return converter;
@@ -57,6 +59,8 @@ internal sealed override bool OnTryRead(
ref ReadStack state,
[MaybeNullWhen(false)] out TCollection value)
{
+ JsonClassInfo elementClassInfo = state.Current.JsonClassInfo.ElementClassInfo!;
+
if (state.UseFastPath)
{
// Fast path that avoids maintaining state variables and dealing with preserved references.
@@ -68,8 +72,8 @@ internal sealed override bool OnTryRead(
CreateCollection(ref reader, ref state);
- JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo);
- if (valueConverter.CanUseDirectReadOrWrite)
+ JsonConverter valueConverter = _valueConverter ??= GetValueConverter(elementClassInfo);
+ if (valueConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null)
{
// Process all elements.
while (true)
@@ -89,7 +93,7 @@ internal sealed override bool OnTryRead(
// Read the value and add.
reader.ReadWithVerify();
- TValue element = valueConverter.Read(ref reader, typeof(TValue), options);
+ TValue element = valueConverter.Read(ref reader, s_valueType, options);
Add(key, element!, options, ref state);
}
}
@@ -114,7 +118,7 @@ internal sealed override bool OnTryRead(
reader.ReadWithVerify();
// Get the value from the converter and add it.
- valueConverter.TryRead(ref reader, typeof(TValue), options, ref state, out TValue element);
+ valueConverter.TryRead(ref reader, s_valueType, options, ref state, out TValue element);
Add(key, element!, options, ref state);
}
}
@@ -172,7 +176,7 @@ internal sealed override bool OnTryRead(
}
// Process all elements.
- JsonConverter elementConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo);
+ JsonConverter elementConverter = _valueConverter ??= GetValueConverter(elementClassInfo);
while (true)
{
if (state.Current.PropertyState == StackFramePropertyState.None)
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs
index 10818b7d31eeee..c89e1f9be07ee8 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs
@@ -49,16 +49,18 @@ protected internal override bool OnWriteResume(
enumerator = (Dictionary.Enumerator)state.Current.CollectionEnumerator;
}
+ JsonClassInfo elementClassInfo = state.Current.JsonClassInfo.ElementClassInfo!;
+
JsonConverter keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options);
- JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo);
- if (!state.SupportContinuation && valueConverter.CanUseDirectReadOrWrite)
+ JsonConverter valueConverter = _valueConverter ??= GetValueConverter(elementClassInfo);
+
+ if (!state.SupportContinuation && valueConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null)
{
// Fast path that avoids validation and extra indirection.
do
{
TKey key = enumerator.Current.Key;
keyConverter.WriteWithQuotes(writer, key, options, ref state);
-
valueConverter.Write(writer, enumerator.Current.Value, options);
} while (enumerator.MoveNext());
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs
index d1055df9ec5817..5bfef5de0b1eb7 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs
@@ -71,7 +71,7 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio
enumerator = (IDictionaryEnumerator)state.Current.CollectionEnumerator;
}
- JsonConverter
internal bool IsValueType { get; set; }
+ ///
+ /// Whether the converter is built-in.
+ ///
+ internal bool IsInternalConverter { get; set; }
+
+ ///
+ /// Whether the converter is built-in and handles a number type.
+ ///
+ internal bool IsInternalConverterForNumberType;
+
///
/// Loosely-typed ReadCore() that forwards to strongly-typed ReadCore().
///
@@ -76,7 +86,7 @@ internal bool ShouldFlush(Utf8JsonWriter writer, ref WriteStack state)
internal abstract void WriteWithQuotesAsObject(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state);
// Whether a type (ClassType.Object) is deserialized using a parameterized constructor.
- internal virtual bool ConstructorIsParameterized => false;
+ internal virtual bool ConstructorIsParameterized { get; }
internal ConstructorInfo? ConstructorInfo { get; set; }
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs
index 52dba1df4bb4dc..dfd81ffd871b27 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs
@@ -3,7 +3,6 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
-using System.Runtime.CompilerServices;
namespace System.Text.Json.Serialization
{
@@ -69,11 +68,6 @@ internal override sealed JsonParameterInfo CreateJsonParameterInfo()
///
internal bool CanBeNull { get; }
- ///
- /// Is the converter built-in.
- ///
- internal bool IsInternalConverter { get; set; }
-
// This non-generic API is sealed as it just forwards to the generic version.
internal sealed override bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state)
{
@@ -131,7 +125,14 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali
// For performance, only perform validation on internal converters on debug builds.
if (IsInternalConverter)
{
- value = Read(ref reader, typeToConvert, options);
+ if (IsInternalConverterForNumberType && state.Current.NumberHandling != null)
+ {
+ value = ReadNumberWithCustomHandling(ref reader, state.Current.NumberHandling.Value);
+ }
+ else
+ {
+ value = Read(ref reader, typeToConvert, options);
+ }
}
else
#endif
@@ -140,7 +141,15 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali
int originalPropertyDepth = reader.CurrentDepth;
long originalPropertyBytesConsumed = reader.BytesConsumed;
- value = Read(ref reader, typeToConvert, options);
+ if (IsInternalConverterForNumberType && state.Current.NumberHandling != null)
+ {
+ value = ReadNumberWithCustomHandling(ref reader, state.Current.NumberHandling.Value);
+ }
+ else
+ {
+ value = Read(ref reader, typeToConvert, options);
+ }
+
VerifyRead(
originalPropertyTokenType,
originalPropertyDepth,
@@ -309,7 +318,15 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions
int originalPropertyDepth = writer.CurrentDepth;
- Write(writer, value, options);
+ if (IsInternalConverterForNumberType && state.Current.NumberHandling != null)
+ {
+ WriteNumberWithCustomHandling(writer, value, state.Current.NumberHandling.Value);
+ }
+ else
+ {
+ Write(writer, value, options);
+ }
+
VerifyWrite(originalPropertyDepth, writer);
return true;
}
@@ -452,5 +469,11 @@ internal virtual void WriteWithQuotes(Utf8JsonWriter writer, [DisallowNull] T va
internal sealed override void WriteWithQuotesAsObject(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state)
=> WriteWithQuotes(writer, (T)value, options, ref state);
+
+ internal virtual T ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling)
+ => throw new InvalidOperationException();
+
+ internal virtual void WriteNumberWithCustomHandling(Utf8JsonWriter writer, T value, JsonNumberHandling handling)
+ => throw new InvalidOperationException();
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberHandling.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberHandling.cs
new file mode 100644
index 00000000000000..09be0bfc94c81e
--- /dev/null
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberHandling.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Text.Json.Serialization
+{
+ ///
+ /// Determines how handles numbers when serializing and deserializing.
+ ///
+ [Flags]
+ public enum JsonNumberHandling
+ {
+ ///
+ /// Numbers will only be read from tokens and will only be written as JSON numbers (without quotes).
+ ///
+ Strict = 0x0,
+ ///
+ /// Numbers can be read from tokens.
+ /// Does not prevent numbers from being read from token.
+ ///
+ AllowReadingFromString = 0x1,
+ ///
+ /// Numbers will be written as JSON strings (with quotes), not as JSON numbers.
+ ///
+ WriteAsString = 0x2,
+ ///
+ /// The "NaN", "Infinity", and "-Infinity" tokens can be read as floating-point constants,
+ /// and the , , and
+ /// values will be written as their corresponding JSON string representations.
+ ///
+ AllowNamedFloatingPointLiterals = 0x4
+ }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfo.cs
index 0ccdbeeafda1ef..4687107f7f82be 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfo.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfo.cs
@@ -27,6 +27,8 @@ internal abstract class JsonParameterInfo
// The name of the parameter as UTF-8 bytes.
public byte[] NameAsUtf8Bytes { get; private set; } = null!;
+ public JsonNumberHandling? NumberHandling { get; private set; }
+
// The zero-based position of the parameter in the formal parameter list.
public int Position { get; private set; }
@@ -63,6 +65,7 @@ public virtual void Initialize(
ShouldDeserialize = true;
ConverterBase = matchingProperty.ConverterBase;
IgnoreDefaultValuesOnRead = matchingProperty.IgnoreDefaultValuesOnRead;
+ NumberHandling = matchingProperty.NumberHandling;
}
// Create a parameter that is ignored at run-time. It uses the same type (typeof(sbyte)) to help
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs
index 38adc919f9ed04..10b92e26fac964 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs
@@ -50,6 +50,14 @@ public static JsonPropertyInfo CreateIgnoredPropertyPlaceholder(MemberInfo membe
public Type DeclaredPropertyType { get; private set; } = null!;
+ public virtual void GetPolicies(JsonIgnoreCondition? ignoreCondition, JsonNumberHandling? parentTypeNumberHandling, bool defaultValueIsNull)
+ {
+ DetermineSerializationCapabilities(ignoreCondition);
+ DeterminePropertyName();
+ DetermineIgnoreCondition(ignoreCondition, defaultValueIsNull);
+ DetermineNumberHandling(parentTypeNumberHandling);
+ }
+
private void DeterminePropertyName()
{
if (MemberInfo == null)
@@ -174,6 +182,56 @@ private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition, bool
#pragma warning restore CS0618 // IgnoreNullValues is obsolete
}
+ private void DetermineNumberHandling(JsonNumberHandling? parentTypeNumberHandling)
+ {
+ if (IsForClassInfo)
+ {
+ if (parentTypeNumberHandling != null && !ConverterBase.IsInternalConverter)
+ {
+ ThrowHelper.ThrowInvalidOperationException_NumberHandlingOnPropertyInvalid(this);
+ }
+
+ // Priority 1: Get handling from the type (parent type in this case is the type itself).
+ NumberHandling = parentTypeNumberHandling;
+
+ // Priority 2: Get handling from JsonSerializerOptions instance.
+ if (!NumberHandling.HasValue && Options.NumberHandling != JsonNumberHandling.Strict)
+ {
+ NumberHandling = Options.NumberHandling;
+ }
+ }
+ else
+ {
+ JsonNumberHandling? handling = null;
+
+ // Priority 1: Get handling from attribute on property or field.
+ if (MemberInfo != null)
+ {
+ JsonNumberHandlingAttribute? attribute = GetAttribute(MemberInfo);
+
+ if (attribute != null &&
+ !ConverterBase.IsInternalConverterForNumberType &&
+ ((ClassType.Enumerable | ClassType.Dictionary) & ClassType) == 0)
+ {
+ ThrowHelper.ThrowInvalidOperationException_NumberHandlingOnPropertyInvalid(this);
+ }
+
+ handling = attribute?.Handling;
+ }
+
+ // Priority 2: Get handling from attribute on parent class type.
+ handling ??= parentTypeNumberHandling;
+
+ // Priority 3: Get handling from JsonSerializerOptions instance.
+ if (!handling.HasValue && Options.NumberHandling != JsonNumberHandling.Strict)
+ {
+ handling = Options.NumberHandling;
+ }
+
+ NumberHandling = handling;
+ }
+ }
+
public static TAttribute? GetAttribute(MemberInfo memberInfo) where TAttribute : Attribute
{
return (TAttribute?)memberInfo.GetCustomAttribute(typeof(TAttribute), inherit: false);
@@ -182,13 +240,6 @@ private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition, bool
public abstract bool GetMemberAndWriteJson(object obj, ref WriteStack state, Utf8JsonWriter writer);
public abstract bool GetMemberAndWriteJsonExtensionData(object obj, ref WriteStack state, Utf8JsonWriter writer);
- public virtual void GetPolicies(JsonIgnoreCondition? ignoreCondition, bool defaultValueIsNull)
- {
- DetermineSerializationCapabilities(ignoreCondition);
- DeterminePropertyName();
- DetermineIgnoreCondition(ignoreCondition, defaultValueIsNull);
- }
-
public abstract object? GetValueAsObject(object obj);
public bool HasGetter { get; set; }
@@ -202,6 +253,7 @@ public virtual void Initialize(
MemberInfo? memberInfo,
JsonConverter converter,
JsonIgnoreCondition? ignoreCondition,
+ JsonNumberHandling? parentTypeNumberHandling,
JsonSerializerOptions options)
{
Debug.Assert(converter != null);
@@ -344,5 +396,7 @@ public JsonClassInfo RuntimeClassInfo
public bool ShouldSerialize { get; private set; }
public bool ShouldDeserialize { get; private set; }
public bool IsIgnored { get; private set; }
+
+ public JsonNumberHandling? NumberHandling { get; private set; }
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs
index 83c5ee116372de..85962cdb0bf515 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs
@@ -28,6 +28,7 @@ public override void Initialize(
MemberInfo? memberInfo,
JsonConverter converter,
JsonIgnoreCondition? ignoreCondition,
+ JsonNumberHandling? parentTypeNumberHandling,
JsonSerializerOptions options)
{
base.Initialize(
@@ -38,6 +39,7 @@ public override void Initialize(
memberInfo,
converter,
ignoreCondition,
+ parentTypeNumberHandling,
options);
switch (memberInfo)
@@ -89,7 +91,7 @@ public override void Initialize(
}
}
- GetPolicies(ignoreCondition, defaultValueIsNull: Converter.CanBeNull);
+ GetPolicies(ignoreCondition, parentTypeNumberHandling, defaultValueIsNull: Converter.CanBeNull);
}
public override JsonConverter ConverterBase
@@ -209,13 +211,13 @@ public override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref U
success = true;
}
- else if (Converter.CanUseDirectReadOrWrite)
+ else if (Converter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null)
{
if (!isNullToken || !IgnoreDefaultValuesOnRead || !Converter.CanBeNull)
{
// Optimize for internal converters by avoiding the extra call to TryRead.
- T fastvalue = Converter.Read(ref reader, RuntimePropertyType!, Options);
- Set!(obj, fastvalue!);
+ T fastValue = Converter.Read(ref reader, RuntimePropertyType!, Options);
+ Set!(obj, fastValue!);
}
success = true;
@@ -253,7 +255,7 @@ public override bool ReadJsonAsObject(ref ReadStack state, ref Utf8JsonReader re
else
{
// Optimize for internal converters by avoiding the extra call to TryRead.
- if (Converter.CanUseDirectReadOrWrite)
+ if (Converter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null)
{
value = Converter.Read(ref reader, RuntimePropertyType!, Options);
success = true;
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs
index 04bb332121f9fa..17a0669b2a4ab2 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs
@@ -55,6 +55,7 @@ internal static JsonPropertyInfo LookupProperty(
}
state.Current.JsonPropertyInfo = jsonPropertyInfo;
+ state.Current.NumberHandling = jsonPropertyInfo.NumberHandling;
return jsonPropertyInfo;
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs
index 4e7946872454c4..8b01c199e6e470 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs
@@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
namespace System.Text.Json
@@ -35,5 +36,11 @@ private static TValue ReadCore(JsonConverter jsonConverter, ref Utf8Json
Debug.Assert(value == null || value is TValue);
return (TValue)value!;
}
+
+ internal static bool IsValidNumberHandlingValue(JsonNumberHandling handling)
+ {
+ int handlingValue = (int)handling;
+ return handlingValue >= 0 && handlingValue <= 7;
+ }
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
index 0796280c369dcd..860b49315a404d 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
@@ -325,7 +325,7 @@ private JsonConverter GetConverterFromAttribute(JsonConverterAttribute converter
return GetAttributeThatCanHaveMultiple(attributeType, classType, memberInfo, attributes);
}
- private static Attribute? GetAttributeThatCanHaveMultiple(Type classType, Type attributeType)
+ internal static Attribute? GetAttributeThatCanHaveMultiple(Type classType, Type attributeType)
{
object[] attributes = classType.GetCustomAttributes(attributeType, inherit: false);
return GetAttributeThatCanHaveMultiple(attributeType, classType, null, attributes);
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs
index e26eb9e0176aab..4217f61a61fc84 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs
@@ -33,6 +33,7 @@ public sealed partial class JsonSerializerOptions
private ReferenceHandler? _referenceHandler;
private JavaScriptEncoder? _encoder;
private JsonIgnoreCondition _defaultIgnoreCondition;
+ private JsonNumberHandling _numberHandling;
private int _defaultBufferSize = BufferSizeDefault;
private int _maxDepth;
@@ -74,6 +75,7 @@ public JsonSerializerOptions(JsonSerializerOptions options)
_referenceHandler = options._referenceHandler;
_encoder = options._encoder;
_defaultIgnoreCondition = options._defaultIgnoreCondition;
+ _numberHandling = options._numberHandling;
_defaultBufferSize = options._defaultBufferSize;
_maxDepth = options._maxDepth;
@@ -262,6 +264,27 @@ public JsonIgnoreCondition DefaultIgnoreCondition
}
}
+ ///
+ /// Specifies how number types should be handled when serializing or deserializing.
+ ///
+ ///
+ /// Thrown if this property is set after serialization or deserialization has occurred.
+ ///
+ public JsonNumberHandling NumberHandling
+ {
+ get => _numberHandling;
+ set
+ {
+ VerifyMutable();
+
+ if (!JsonSerializer.IsValidNumberHandlingValue(value))
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ _numberHandling = value;
+ }
+ }
+
///
/// Determines whether read-only properties are ignored during serialization.
/// A property is read-only if it contains a public getter but not a public setter.
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs
index c3b1ba7c38a74e..ac03e3b6adba67 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs
@@ -87,6 +87,8 @@ public void Initialize(Type type, JsonSerializerOptions options, bool supportCon
// The initial JsonPropertyInfo will be used to obtain the converter.
Current.JsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo;
+ Current.NumberHandling = Current.JsonPropertyInfo.NumberHandling;
+
bool preserveReferences = options.ReferenceHandler != null;
if (preserveReferences)
{
@@ -109,6 +111,8 @@ public void Push()
else
{
JsonClassInfo jsonClassInfo;
+ JsonNumberHandling? numberHandling = Current.NumberHandling;
+
if (Current.JsonClassInfo.ClassType == ClassType.Object)
{
if (Current.JsonPropertyInfo != null)
@@ -120,13 +124,14 @@ public void Push()
jsonClassInfo = Current.CtorArgumentState!.JsonParameterInfo!.RuntimeClassInfo;
}
}
- else if ((Current.JsonClassInfo.ClassType & (ClassType.Value | ClassType.NewValue)) != 0)
+ else if (((ClassType.Value | ClassType.NewValue) & Current.JsonClassInfo.ClassType) != 0)
{
// Although ClassType.Value doesn't push, a custom custom converter may re-enter serialization.
jsonClassInfo = Current.JsonPropertyInfo!.RuntimeClassInfo;
}
else
{
+ Debug.Assert(((ClassType.Enumerable | ClassType.Dictionary) & Current.JsonClassInfo.ClassType) != 0);
jsonClassInfo = Current.JsonClassInfo.ElementClassInfo!;
}
@@ -135,6 +140,8 @@ public void Push()
Current.JsonClassInfo = jsonClassInfo;
Current.JsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo;
+ // Allow number handling on property to win over handling on type.
+ Current.NumberHandling = numberHandling ?? Current.JsonPropertyInfo.NumberHandling;
}
}
else if (_continuationCount == 1)
@@ -159,7 +166,7 @@ public void Push()
}
}
- SetConstrutorArgumentState();
+ SetConstructorArgumentState();
}
public void Pop(bool success)
@@ -210,7 +217,7 @@ public void Pop(bool success)
Current = _previous[--_count -1];
}
- SetConstrutorArgumentState();
+ SetConstructorArgumentState();
}
// Return a JSONPath using simple dot-notation when possible. When special characters are present, bracket-notation is used:
@@ -328,7 +335,7 @@ static void AppendPropertyName(StringBuilder sb, string? propertyName)
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void SetConstrutorArgumentState()
+ private void SetConstructorArgumentState()
{
if (Current.JsonClassInfo.ParameterCount > 0)
{
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs
index 650c1c27c8c14a..75dba157be5eeb 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Diagnostics;
+using System.Text.Json.Serialization;
namespace System.Text.Json
{
@@ -46,6 +47,9 @@ internal struct ReadStackFrame
public int CtorArgumentStateIndex;
public ArgumentState? CtorArgumentState;
+ // Whether to use custom number handling.
+ public JsonNumberHandling? NumberHandling;
+
public void EndConstructorParameter()
{
CtorArgumentState!.JsonParameterInfo = null;
@@ -62,6 +66,7 @@ public void EndProperty()
MetadataId = null;
// No need to clear these since they are overwritten each time:
+ // NumberHandling
// UseExtensionProperty
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs
index ec90b0c75e0218..e3378711eb1eb1 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs
@@ -68,12 +68,10 @@ private void AddCurrent()
public JsonConverter Initialize(Type type, JsonSerializerOptions options, bool supportContinuation)
{
JsonClassInfo jsonClassInfo = options.GetOrAddClassForRootType(type);
- Current.JsonClassInfo = jsonClassInfo;
- if ((jsonClassInfo.ClassType & (ClassType.Enumerable | ClassType.Dictionary)) == 0)
- {
- Current.DeclaredJsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo;
- }
+ Current.JsonClassInfo = jsonClassInfo;
+ Current.DeclaredJsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo;
+ Current.NumberHandling = Current.DeclaredJsonPropertyInfo.NumberHandling;
if (options.ReferenceHandler != null)
{
@@ -97,12 +95,15 @@ public void Push()
else
{
JsonClassInfo jsonClassInfo = Current.GetPolymorphicJsonPropertyInfo().RuntimeClassInfo;
+ JsonNumberHandling? numberHandling = Current.NumberHandling;
AddCurrent();
Current.Reset();
Current.JsonClassInfo = jsonClassInfo;
Current.DeclaredJsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo;
+ // Allow number handling on property to win over handling on type.
+ Current.NumberHandling = numberHandling ?? Current.DeclaredJsonPropertyInfo.NumberHandling;
}
}
else if (_continuationCount == 1)
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs
index 9689568809b24d..d5a671dd4794f3 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs
@@ -68,6 +68,9 @@ internal struct WriteStackFrame
///
public JsonPropertyInfo? PolymorphicJsonPropertyInfo;
+ // Whether to use custom number handling.
+ public JsonNumberHandling? NumberHandling;
+
public void EndDictionaryElement()
{
PropertyState = StackFramePropertyState.None;
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
index 2194ab03fab145..58ea337c53bb8f 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
@@ -224,6 +224,28 @@ public static void ThrowInvalidOperationException_IgnoreConditionOnValueTypeInva
throw new InvalidOperationException(SR.Format(SR.IgnoreConditionOnValueTypeInvalid, memberInfo.Name, memberInfo.DeclaringType));
}
+ [DoesNotReturn]
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static void ThrowInvalidOperationException_NumberHandlingOnPropertyInvalid(JsonPropertyInfo jsonPropertyInfo)
+ {
+ MemberInfo? memberInfo = jsonPropertyInfo.MemberInfo;
+
+ if (!jsonPropertyInfo.ConverterBase.IsInternalConverter)
+ {
+ throw new InvalidOperationException(SR.Format(
+ SR.NumberHandlingConverterMustBeBuiltIn,
+ jsonPropertyInfo.ConverterBase.GetType(),
+ jsonPropertyInfo.IsForClassInfo ? jsonPropertyInfo.DeclaredPropertyType : memberInfo!.DeclaringType));
+ }
+
+ // This exception is only thrown for object properties.
+ Debug.Assert(!jsonPropertyInfo.IsForClassInfo && memberInfo != null);
+ throw new InvalidOperationException(SR.Format(
+ SR.NumberHandlingOnPropertyTypeMustBeNumberOrCollection,
+ memberInfo.Name,
+ memberInfo.DeclaringType));
+ }
+
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotHonored(
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Decimal.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Decimal.cs
index ca5b059be50bbd..a30af2e6546ad1 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Decimal.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Decimal.cs
@@ -93,5 +93,13 @@ private void WriteNumberValueIndented(decimal value)
Debug.Assert(result);
BytesPending += bytesWritten;
}
+
+ internal void WriteNumberValueAsString(decimal value)
+ {
+ Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatDecimalLength];
+ bool result = Utf8Formatter.TryFormat(value, utf8Number, out int bytesWritten);
+ Debug.Assert(result);
+ WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesWritten));
+ }
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs
index cf8732bc44ec8a..f8a46a31468ca9 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs
@@ -143,5 +143,33 @@ private static bool TryFormatDouble(double value, Span destination, out in
}
#endif
}
+
+ internal void WriteNumberValueAsString(double value)
+ {
+ Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatDoubleLength];
+ bool result = TryFormatDouble(value, utf8Number, out int bytesWritten);
+ Debug.Assert(result);
+ WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesWritten));
+ }
+
+ internal void WriteFloatingPointConstant(double value)
+ {
+ if (double.IsNaN(value))
+ {
+ WriteNumberValueAsStringUnescaped(JsonConstants.NaNValue);
+ }
+ else if (double.IsPositiveInfinity(value))
+ {
+ WriteNumberValueAsStringUnescaped(JsonConstants.PositiveInfinityValue);
+ }
+ else if (double.IsNegativeInfinity(value))
+ {
+ WriteNumberValueAsStringUnescaped(JsonConstants.NegativeInfinityValue);
+ }
+ else
+ {
+ WriteNumberValue(value);
+ }
+ }
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs
index f120a09373c479..2f046d872f6972 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs
@@ -143,5 +143,33 @@ private static bool TryFormatSingle(float value, Span destination, out int
}
#endif
}
+
+ internal void WriteNumberValueAsString(float value)
+ {
+ Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatSingleLength];
+ bool result = TryFormatSingle(value, utf8Number, out int bytesWritten);
+ Debug.Assert(result);
+ WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesWritten));
+ }
+
+ internal void WriteFloatingPointConstant(float value)
+ {
+ if (float.IsNaN(value))
+ {
+ WriteNumberValueAsStringUnescaped(JsonConstants.NaNValue);
+ }
+ else if (float.IsPositiveInfinity(value))
+ {
+ WriteNumberValueAsStringUnescaped(JsonConstants.PositiveInfinityValue);
+ }
+ else if (float.IsNegativeInfinity(value))
+ {
+ WriteNumberValueAsStringUnescaped(JsonConstants.NegativeInfinityValue);
+ }
+ else
+ {
+ WriteNumberValue(value);
+ }
+ }
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.SignedNumber.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.SignedNumber.cs
index 957573ddb7a7f4..2d6120a6a87680 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.SignedNumber.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.SignedNumber.cs
@@ -106,5 +106,13 @@ private void WriteNumberValueIndented(long value)
Debug.Assert(result);
BytesPending += bytesWritten;
}
+
+ internal void WriteNumberValueAsString(long value)
+ {
+ Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatInt64Length];
+ bool result = Utf8Formatter.TryFormat(value, utf8Number, out int bytesWritten);
+ Debug.Assert(result);
+ WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesWritten));
+ }
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.String.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.String.cs
index c86a8af22f1628..15cb0b8d1af5ec 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.String.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.String.cs
@@ -349,5 +349,19 @@ private void WriteStringEscapeValue(ReadOnlySpan utf8Value, int firstEscap
ArrayPool.Shared.Return(valueArray);
}
}
+
+ ///
+ /// Writes a number as a JSON string. The string value is not escaped.
+ ///
+ ///
+ internal void WriteNumberValueAsStringUnescaped(ReadOnlySpan utf8Value)
+ {
+ // The value has been validated prior to calling this method.
+
+ WriteStringByOptions(utf8Value);
+
+ SetFlagToAddListSeparatorBeforeNextItem();
+ _tokenType = JsonTokenType.String;
+ }
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.UnsignedNumber.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.UnsignedNumber.cs
index 2c15441d432593..d3cf96947db468 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.UnsignedNumber.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.UnsignedNumber.cs
@@ -108,5 +108,13 @@ private void WriteNumberValueIndented(ulong value)
Debug.Assert(result);
BytesPending += bytesWritten;
}
+
+ internal void WriteNumberValueAsString(ulong value)
+ {
+ Span utf8Number = stackalloc byte[JsonConstants.MaximumFormatUInt64Length];
+ bool result = Utf8Formatter.TryFormat(value, utf8Number, out int bytesWritten);
+ Debug.Assert(result);
+ WriteNumberValueAsStringUnescaped(utf8Number.Slice(0, bytesWritten));
+ }
}
}
diff --git a/src/libraries/System.Text.Json/tests/JsonNumberTestData.cs b/src/libraries/System.Text.Json/tests/JsonNumberTestData.cs
index d07064a3e487be..384488485c9806 100644
--- a/src/libraries/System.Text.Json/tests/JsonNumberTestData.cs
+++ b/src/libraries/System.Text.Json/tests/JsonNumberTestData.cs
@@ -3,8 +3,7 @@
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
-using Newtonsoft.Json;
+using System.Linq;
namespace System.Text.Json.Tests
{
@@ -21,6 +20,19 @@ internal class JsonNumberTestData
public static List Floats { get; set; }
public static List Doubles { get; set; }
public static List Decimals { get; set; }
+
+ public static List NullableBytes { get; set; }
+ public static List NullableSBytes { get; set; }
+ public static List NullableShorts { get; set; }
+ public static List NullableInts { get; set; }
+ public static List NullableLongs { get; set; }
+ public static List NullableUShorts { get; set; }
+ public static List NullableUInts { get; set; }
+ public static List NullableULongs { get; set; }
+ public static List NullableFloats { get; set; }
+ public static List NullableDoubles { get; set; }
+ public static List NullableDecimals { get; set; }
+
public static byte[] JsonData { get; set; }
static JsonNumberTestData()
@@ -295,6 +307,19 @@ static JsonNumberTestData()
builder.Append("\"intEnd\": 0}");
#endregion
+ // Make collections of nullable numbers.
+ NullableBytes = new List(Bytes.Select(num => (byte?)num));
+ NullableSBytes = new List(SBytes.Select(num => (sbyte?)num));
+ NullableShorts = new List(Shorts.Select(num => (short?)num));
+ NullableInts = new List(Ints.Select(num => (int?)num));
+ NullableLongs = new List(Longs.Select(num => (long?)num));
+ NullableUShorts = new List(UShorts.Select(num => (ushort?)num));
+ NullableUInts = new List(UInts.Select(num => (uint?)num));
+ NullableULongs = new List(ULongs.Select(num => (ulong?)num));
+ NullableFloats = new List(Floats.Select(num => (float?)num));
+ NullableDoubles = new List(Doubles.Select(num => (double?)num));
+ NullableDecimals = new List(Decimals.Select(num => (decimal?)num));
+
string jsonString = builder.ToString();
JsonData = Encoding.UTF8.GetBytes(jsonString);
}
diff --git a/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs
new file mode 100644
index 00000000000000..60acb54094d6f9
--- /dev/null
+++ b/src/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs
@@ -0,0 +1,1414 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using System.Text.Encodings.Web;
+using System.Text.Json.Tests;
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+ public static partial class NumberHandlingTests
+ {
+ private static readonly JsonSerializerOptions s_optionReadFromStr = new JsonSerializerOptions
+ {
+ NumberHandling = JsonNumberHandling.AllowReadingFromString
+ };
+
+ private static readonly JsonSerializerOptions s_optionWriteAsStr = new JsonSerializerOptions
+ {
+ NumberHandling = JsonNumberHandling.WriteAsString
+ };
+
+ private static readonly JsonSerializerOptions s_optionReadAndWriteFromStr = new JsonSerializerOptions
+ {
+ NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString
+ };
+
+ private static readonly JsonSerializerOptions s_optionsAllowFloatConstants = new JsonSerializerOptions
+ {
+ NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
+ };
+
+ private static readonly JsonSerializerOptions s_optionReadFromStrAllowFloatConstants = new JsonSerializerOptions
+ {
+ NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals
+ };
+
+ private static readonly JsonSerializerOptions s_optionWriteAsStrAllowFloatConstants = new JsonSerializerOptions
+ {
+ NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowNamedFloatingPointLiterals
+ };
+
+ [Fact]
+ public static void Number_AsRootType_RoundTrip()
+ {
+ RunAsRootTypeTest(JsonNumberTestData.Bytes);
+ RunAsRootTypeTest(JsonNumberTestData.SBytes);
+ RunAsRootTypeTest(JsonNumberTestData.Shorts);
+ RunAsRootTypeTest(JsonNumberTestData.Ints);
+ RunAsRootTypeTest(JsonNumberTestData.Longs);
+ RunAsRootTypeTest(JsonNumberTestData.UShorts);
+ RunAsRootTypeTest(JsonNumberTestData.UInts);
+ RunAsRootTypeTest(JsonNumberTestData.ULongs);
+ RunAsRootTypeTest(JsonNumberTestData.Floats);
+ RunAsRootTypeTest(JsonNumberTestData.Doubles);
+ RunAsRootTypeTest(JsonNumberTestData.Decimals);
+ RunAsRootTypeTest(JsonNumberTestData.NullableBytes);
+ RunAsRootTypeTest(JsonNumberTestData.NullableSBytes);
+ RunAsRootTypeTest(JsonNumberTestData.NullableShorts);
+ RunAsRootTypeTest(JsonNumberTestData.NullableInts);
+ RunAsRootTypeTest(JsonNumberTestData.NullableLongs);
+ RunAsRootTypeTest(JsonNumberTestData.NullableUShorts);
+ RunAsRootTypeTest(JsonNumberTestData.NullableUInts);
+ RunAsRootTypeTest(JsonNumberTestData.NullableULongs);
+ RunAsRootTypeTest(JsonNumberTestData.NullableFloats);
+ RunAsRootTypeTest(JsonNumberTestData.NullableDoubles);
+ RunAsRootTypeTest(JsonNumberTestData.NullableDecimals);
+ }
+
+ private static void RunAsRootTypeTest(List numbers)
+ {
+ foreach (T number in numbers)
+ {
+ string numberAsString = GetNumberAsString(number);
+ string json = $"{numberAsString}";
+ string jsonWithNumberAsString = @$"""{numberAsString}""";
+ PerformAsRootTypeSerialization(number, json, jsonWithNumberAsString);
+ }
+ }
+
+ private static string GetNumberAsString(T number)
+ {
+ return number switch
+ {
+ double @double => @double.ToString(JsonTestHelper.DoubleFormatString, CultureInfo.InvariantCulture),
+ float @float => @float.ToString(JsonTestHelper.SingleFormatString, CultureInfo.InvariantCulture),
+ decimal @decimal => @decimal.ToString(CultureInfo.InvariantCulture),
+ _ => number.ToString()
+ };
+ }
+
+ private static void PerformAsRootTypeSerialization(T number, string jsonWithNumberAsNumber, string jsonWithNumberAsString)
+ {
+ // Option: read from string
+
+ // Deserialize
+ Assert.Equal(number, JsonSerializer.Deserialize(jsonWithNumberAsNumber, s_optionReadFromStr));
+ Assert.Equal(number, JsonSerializer.Deserialize(jsonWithNumberAsString, s_optionReadFromStr));
+
+ // Serialize
+ Assert.Equal(jsonWithNumberAsNumber, JsonSerializer.Serialize(number, s_optionReadFromStr));
+
+ // Option: write as string
+
+ // Deserialize
+ Assert.Equal(number, JsonSerializer.Deserialize(jsonWithNumberAsNumber, s_optionWriteAsStr));
+ Assert.Throws(() => JsonSerializer.Deserialize(jsonWithNumberAsString, s_optionWriteAsStr));
+
+ // Serialize
+ Assert.Equal(jsonWithNumberAsString, JsonSerializer.Serialize(number, s_optionWriteAsStr));
+
+ // Option: read and write from/to string
+
+ // Deserialize
+ Assert.Equal(number, JsonSerializer.Deserialize(jsonWithNumberAsNumber, s_optionReadAndWriteFromStr));
+ Assert.Equal(number, JsonSerializer.Deserialize(jsonWithNumberAsString, s_optionReadAndWriteFromStr));
+
+ // Serialize
+ Assert.Equal(jsonWithNumberAsString, JsonSerializer.Serialize(number, s_optionReadAndWriteFromStr));
+ }
+
+ [Fact]
+ public static void Number_AsBoxedRootType()
+ {
+ string numberAsString = @"""2""";
+
+ int @int = 2;
+ float @float = 2;
+ int? nullableInt = 2;
+ float? nullableFloat = 2;
+
+ Assert.Equal(numberAsString, JsonSerializer.Serialize((object)@int, s_optionReadAndWriteFromStr));
+ Assert.Equal(numberAsString, JsonSerializer.Serialize((object)@float, s_optionReadAndWriteFromStr));
+ Assert.Equal(numberAsString, JsonSerializer.Serialize((object)nullableInt, s_optionReadAndWriteFromStr));
+ Assert.Equal(numberAsString, JsonSerializer.Serialize((object)nullableFloat, s_optionReadAndWriteFromStr));
+
+ Assert.Equal(2, (int)JsonSerializer.Deserialize(numberAsString, typeof(int), s_optionReadAndWriteFromStr));
+ Assert.Equal(2, (float)JsonSerializer.Deserialize(numberAsString, typeof(float), s_optionReadAndWriteFromStr));
+ Assert.Equal(2, (int?)JsonSerializer.Deserialize(numberAsString, typeof(int?), s_optionReadAndWriteFromStr));
+ Assert.Equal(2, (float?)JsonSerializer.Deserialize(numberAsString, typeof(float?), s_optionReadAndWriteFromStr));
+ }
+
+ [Fact]
+ public static void Number_AsCollectionElement_RoundTrip()
+ {
+ RunAsCollectionElementTest(JsonNumberTestData.Bytes);
+ RunAsCollectionElementTest(JsonNumberTestData.SBytes);
+ RunAsCollectionElementTest(JsonNumberTestData.Shorts);
+ RunAsCollectionElementTest(JsonNumberTestData.Ints);
+ RunAsCollectionElementTest(JsonNumberTestData.Longs);
+ RunAsCollectionElementTest(JsonNumberTestData.UShorts);
+ RunAsCollectionElementTest(JsonNumberTestData.UInts);
+ RunAsCollectionElementTest(JsonNumberTestData.ULongs);
+ RunAsCollectionElementTest(JsonNumberTestData.Floats);
+ RunAsCollectionElementTest(JsonNumberTestData.Doubles);
+ RunAsCollectionElementTest(JsonNumberTestData.Decimals);
+ RunAsCollectionElementTest(JsonNumberTestData.NullableBytes);
+ RunAsCollectionElementTest(JsonNumberTestData.NullableSBytes);
+ RunAsCollectionElementTest(JsonNumberTestData.NullableShorts);
+ RunAsCollectionElementTest(JsonNumberTestData.NullableInts);
+ RunAsCollectionElementTest(JsonNumberTestData.NullableLongs);
+ RunAsCollectionElementTest(JsonNumberTestData.NullableUShorts);
+ RunAsCollectionElementTest(JsonNumberTestData.NullableUInts);
+ RunAsCollectionElementTest(JsonNumberTestData.NullableULongs);
+ RunAsCollectionElementTest(JsonNumberTestData.NullableFloats);
+ RunAsCollectionElementTest(JsonNumberTestData.NullableDoubles);
+ RunAsCollectionElementTest(JsonNumberTestData.NullableDecimals);
+ }
+
+ private static void RunAsCollectionElementTest(List numbers)
+ {
+ StringBuilder jsonBuilder_NumbersAsNumbers = new StringBuilder();
+ StringBuilder jsonBuilder_NumbersAsStrings = new StringBuilder();
+ StringBuilder jsonBuilder_NumbersAsNumbersAndStrings = new StringBuilder();
+ StringBuilder jsonBuilder_NumbersAsNumbersAndStrings_Alternate = new StringBuilder();
+ bool asNumber = false;
+
+ jsonBuilder_NumbersAsNumbers.Append("[");
+ jsonBuilder_NumbersAsStrings.Append("[");
+ jsonBuilder_NumbersAsNumbersAndStrings.Append("[");
+ jsonBuilder_NumbersAsNumbersAndStrings_Alternate.Append("[");
+
+ foreach (T number in numbers)
+ {
+ string numberAsString = GetNumberAsString(number);
+
+ string jsonWithNumberAsString = @$"""{numberAsString}""";
+
+ jsonBuilder_NumbersAsNumbers.Append($"{numberAsString},");
+ jsonBuilder_NumbersAsStrings.Append($"{jsonWithNumberAsString},");
+ jsonBuilder_NumbersAsNumbersAndStrings.Append(asNumber
+ ? $"{numberAsString},"
+ : $"{jsonWithNumberAsString},");
+ jsonBuilder_NumbersAsNumbersAndStrings_Alternate.Append(!asNumber
+ ? $"{numberAsString},"
+ : $"{jsonWithNumberAsString},");
+
+ asNumber = !asNumber;
+ }
+
+ jsonBuilder_NumbersAsNumbers.Remove(jsonBuilder_NumbersAsNumbers.Length - 1, 1);
+ jsonBuilder_NumbersAsStrings.Remove(jsonBuilder_NumbersAsStrings.Length - 1, 1);
+ jsonBuilder_NumbersAsNumbersAndStrings.Remove(jsonBuilder_NumbersAsNumbersAndStrings.Length - 1, 1);
+ jsonBuilder_NumbersAsNumbersAndStrings_Alternate.Remove(jsonBuilder_NumbersAsNumbersAndStrings_Alternate.Length - 1, 1);
+
+ jsonBuilder_NumbersAsNumbers.Append("]");
+ jsonBuilder_NumbersAsStrings.Append("]");
+ jsonBuilder_NumbersAsNumbersAndStrings.Append("]");
+ jsonBuilder_NumbersAsNumbersAndStrings_Alternate.Append("]");
+
+ string jsonNumbersAsStrings = jsonBuilder_NumbersAsStrings.ToString();
+
+ PerformAsCollectionElementSerialization(
+ numbers,
+ jsonBuilder_NumbersAsNumbers.ToString(),
+ jsonNumbersAsStrings,
+ jsonBuilder_NumbersAsNumbersAndStrings.ToString(),
+ jsonBuilder_NumbersAsNumbersAndStrings_Alternate.ToString());
+
+ // Reflection based tests for every collection type.
+ RunAllCollectionsRoundTripTest(jsonNumbersAsStrings);
+ }
+
+ private static void PerformAsCollectionElementSerialization(
+ List numbers,
+ string json_NumbersAsNumbers,
+ string json_NumbersAsStrings,
+ string json_NumbersAsNumbersAndStrings,
+ string json_NumbersAsNumbersAndStrings_Alternate)
+ {
+ List deserialized;
+
+ // Option: read from string
+
+ // Deserialize
+ deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbers, s_optionReadFromStr);
+ AssertIEnumerableEqual(numbers, deserialized);
+
+ deserialized = JsonSerializer.Deserialize>(json_NumbersAsStrings, s_optionReadFromStr);
+ AssertIEnumerableEqual(numbers, deserialized);
+
+ deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings, s_optionReadFromStr);
+ AssertIEnumerableEqual(numbers, deserialized);
+
+ deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings_Alternate, s_optionReadFromStr);
+ AssertIEnumerableEqual(numbers, deserialized);
+
+ // Serialize
+ Assert.Equal(json_NumbersAsNumbers, JsonSerializer.Serialize(numbers, s_optionReadFromStr));
+
+ // Option: write as string
+
+ // Deserialize
+ deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbers, s_optionWriteAsStr);
+ AssertIEnumerableEqual(numbers, deserialized);
+
+ Assert.Throws(() => JsonSerializer.Deserialize>(json_NumbersAsStrings, s_optionWriteAsStr));
+
+ Assert.Throws(() => JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings, s_optionWriteAsStr));
+
+ Assert.Throws(() => JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings_Alternate, s_optionWriteAsStr));
+
+ // Serialize
+ Assert.Equal(json_NumbersAsStrings, JsonSerializer.Serialize(numbers, s_optionWriteAsStr));
+
+ // Option: read and write from/to string
+
+ // Deserialize
+ deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbers, s_optionReadAndWriteFromStr);
+ AssertIEnumerableEqual(numbers, deserialized);
+
+ deserialized = JsonSerializer.Deserialize>(json_NumbersAsStrings, s_optionReadAndWriteFromStr);
+ AssertIEnumerableEqual(numbers, deserialized);
+
+ deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings, s_optionReadAndWriteFromStr);
+ AssertIEnumerableEqual(numbers, deserialized);
+
+ deserialized = JsonSerializer.Deserialize>(json_NumbersAsNumbersAndStrings_Alternate, s_optionReadAndWriteFromStr);
+ AssertIEnumerableEqual(numbers, deserialized);
+
+ // Serialize
+ Assert.Equal(json_NumbersAsStrings, JsonSerializer.Serialize(numbers, s_optionReadAndWriteFromStr));
+ }
+
+ private static void AssertIEnumerableEqual(IEnumerable list1, IEnumerable list2)
+ {
+ IEnumerator enumerator1 = list1.GetEnumerator();
+ IEnumerator enumerator2 = list2.GetEnumerator();
+
+ while (enumerator1.MoveNext())
+ {
+ enumerator2.MoveNext();
+ Assert.Equal(enumerator1.Current, enumerator2.Current);
+ }
+
+ Assert.False(enumerator2.MoveNext());
+ }
+
+ private static void RunAllCollectionsRoundTripTest(string json)
+ {
+ foreach (Type type in CollectionTestTypes.DeserializableGenericEnumerableTypes())
+ {
+ if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(HashSet<>))
+ {
+ HashSet obj1 = (HashSet)JsonSerializer.Deserialize(json, type, s_optionReadAndWriteFromStr);
+ string serialized = JsonSerializer.Serialize(obj1, s_optionReadAndWriteFromStr);
+
+ HashSet obj2 = (HashSet)JsonSerializer.Deserialize(serialized, type, s_optionReadAndWriteFromStr);
+
+ Assert.Equal(obj1.Count, obj2.Count);
+ foreach (T element in obj1)
+ {
+ Assert.True(obj2.Contains(element));
+ }
+ }
+ else if (type != typeof(byte[]))
+ {
+ object obj = JsonSerializer.Deserialize(json, type, s_optionReadAndWriteFromStr);
+ string serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr);
+ Assert.Equal(json, serialized);
+ }
+ }
+
+ foreach (Type type in CollectionTestTypes.DeserializableNonGenericEnumerableTypes())
+ {
+ // Deserialized as collection of JsonElements.
+ object obj = JsonSerializer.Deserialize(json, type, s_optionReadAndWriteFromStr);
+ // Serialized as strings with escaping.
+ string serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr);
+
+ // Ensure escaped values were serialized accurately
+ List list = JsonSerializer.Deserialize>(serialized, s_optionReadAndWriteFromStr);
+ serialized = JsonSerializer.Serialize(list, s_optionReadAndWriteFromStr);
+ Assert.Equal(json, serialized);
+
+ // Serialize instance which is a collection of numbers (not JsonElements).
+ obj = Activator.CreateInstance(type, new[] { list });
+ serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr);
+ Assert.Equal(json, serialized);
+ }
+ }
+
+ [Fact]
+ public static void Number_AsDictionaryElement_RoundTrip()
+ {
+ var dict = new Dictionary();
+ for (int i = 0; i < 10; i++)
+ {
+ dict[JsonNumberTestData.Ints[i]] = JsonNumberTestData.Floats[i];
+ }
+
+ // Serialize
+ string serialized = JsonSerializer.Serialize(dict, s_optionReadAndWriteFromStr);
+ AssertDictionaryElements_StringValues(serialized);
+
+ // Deserialize
+ dict = JsonSerializer.Deserialize>(serialized, s_optionReadAndWriteFromStr);
+
+ // Test roundtrip
+ JsonTestHelper.AssertJsonEqual(serialized, JsonSerializer.Serialize(dict, s_optionReadAndWriteFromStr));
+ }
+
+ private static void AssertDictionaryElements_StringValues(string serialized)
+ {
+ var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(serialized));
+ reader.Read();
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndObject)
+ {
+ break;
+ }
+ else if (reader.TokenType == JsonTokenType.String)
+ {
+#if BUILDING_INBOX_LIBRARY
+ Assert.False(reader.ValueSpan.Contains((byte)'\\'));
+#else
+ foreach (byte val in reader.ValueSpan)
+ {
+ if (val == (byte)'\\')
+ {
+ Assert.True(false, "Unexpected escape token.");
+ }
+ }
+#endif
+ }
+ else
+ {
+ Assert.Equal(JsonTokenType.PropertyName, reader.TokenType);
+ }
+ }
+ }
+
+ [Fact]
+ [ActiveIssue("https://github.com/dotnet/runtime/issues/39674", typeof(PlatformDetection), nameof(PlatformDetection.IsMonoInterpreter))]
+ public static void DictionariesRoundTrip()
+ {
+ RunAllDictionariessRoundTripTest(JsonNumberTestData.ULongs);
+ RunAllDictionariessRoundTripTest(JsonNumberTestData.Floats);
+ RunAllDictionariessRoundTripTest(JsonNumberTestData.Doubles);
+ }
+
+ private static void RunAllDictionariessRoundTripTest(List numbers)
+ {
+ StringBuilder jsonBuilder_NumbersAsStrings = new StringBuilder();
+
+ jsonBuilder_NumbersAsStrings.Append("{");
+
+ foreach (T number in numbers)
+ {
+ string numberAsString = GetNumberAsString(number);
+ string jsonWithNumberAsString = @$"""{numberAsString}""";
+
+ jsonBuilder_NumbersAsStrings.Append($"{jsonWithNumberAsString}:");
+ jsonBuilder_NumbersAsStrings.Append($"{jsonWithNumberAsString},");
+ }
+
+ jsonBuilder_NumbersAsStrings.Remove(jsonBuilder_NumbersAsStrings.Length - 1, 1);
+ jsonBuilder_NumbersAsStrings.Append("}");
+
+ string jsonNumbersAsStrings = jsonBuilder_NumbersAsStrings.ToString();
+
+ foreach (Type type in CollectionTestTypes.DeserializableDictionaryTypes())
+ {
+ object obj = JsonSerializer.Deserialize(jsonNumbersAsStrings, type, s_optionReadAndWriteFromStr);
+ JsonTestHelper.AssertJsonEqual(jsonNumbersAsStrings, JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr));
+ }
+
+ foreach (Type type in CollectionTestTypes.DeserializableNonDictionaryTypes())
+ {
+ Dictionary dict = JsonSerializer.Deserialize>(jsonNumbersAsStrings, s_optionReadAndWriteFromStr);
+
+ // Serialize instance which is a dictionary of numbers (not JsonElements).
+ object obj = Activator.CreateInstance(type, new[] { dict });
+ string serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr);
+ JsonTestHelper.AssertJsonEqual(jsonNumbersAsStrings, serialized);
+ }
+ }
+
+ [Fact]
+ public static void Number_AsPropertyValue_RoundTrip()
+ {
+ var obj = new Class_With_NullableUInt64_And_Float()
+ {
+ NullableUInt64Number = JsonNumberTestData.NullableULongs.LastOrDefault(),
+ FloatNumbers = JsonNumberTestData.Floats
+ };
+
+ // Serialize
+ string serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr);
+
+ // Deserialize
+ obj = JsonSerializer.Deserialize(serialized, s_optionReadAndWriteFromStr);
+
+ // Test roundtrip
+ JsonTestHelper.AssertJsonEqual(serialized, JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr));
+ }
+
+ private class Class_With_NullableUInt64_And_Float
+ {
+ public ulong? NullableUInt64Number { get; set; }
+ [JsonInclude]
+ public List FloatNumbers;
+ }
+
+ [Fact]
+ public static void Number_AsKeyValuePairValue_RoundTrip()
+ {
+ var obj = new KeyValuePair>(JsonNumberTestData.NullableULongs.LastOrDefault(), JsonNumberTestData.Floats);
+
+ // Serialize
+ string serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr);
+
+ // Deserialize
+ obj = JsonSerializer.Deserialize>>(serialized, s_optionReadAndWriteFromStr);
+
+ // Test roundtrip
+ JsonTestHelper.AssertJsonEqual(serialized, JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr));
+ }
+
+ [Fact]
+ public static void Number_AsObjectWithParameterizedCtor_RoundTrip()
+ {
+ var obj = new MyClassWithNumbers(JsonNumberTestData.NullableULongs.LastOrDefault(), JsonNumberTestData.Floats);
+
+ // Serialize
+ string serialized = JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr);
+
+ // Deserialize
+ obj = JsonSerializer.Deserialize(serialized, s_optionReadAndWriteFromStr);
+
+ // Test roundtrip
+ JsonTestHelper.AssertJsonEqual(serialized, JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr));
+ }
+
+ private class MyClassWithNumbers
+ {
+ public ulong? Ulong { get; }
+ public List ListOfFloats { get; }
+
+ public MyClassWithNumbers(ulong? @ulong, List listOfFloats)
+ {
+ Ulong = @ulong;
+ ListOfFloats = listOfFloats;
+ }
+ }
+
+ [Fact]
+ public static void Number_AsObjectWithParameterizedCtor_PropHasAttribute()
+ {
+ string json = @"{""ListOfFloats"":[""1""]}";
+ // Strict handling on property overrides loose global policy.
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionReadFromStr));
+
+ // Serialize
+ json = @"{""ListOfFloats"":[1]}";
+ MyClassWithNumbers_PropsHasAttribute obj = JsonSerializer.Deserialize(json);
+
+ // Number serialized as JSON number due to strict handling on property which overrides loose global policy.
+ Assert.Equal(json, JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr));
+ }
+
+ private class MyClassWithNumbers_PropsHasAttribute
+ {
+ [JsonNumberHandling(JsonNumberHandling.Strict)]
+ public List ListOfFloats { get; }
+
+ public MyClassWithNumbers_PropsHasAttribute(List listOfFloats)
+ {
+ ListOfFloats = listOfFloats;
+ }
+ }
+
+ [Fact]
+ public static void FloatingPointConstants_Pass()
+ {
+ // Valid values
+ PerformFloatingPointSerialization("NaN");
+ PerformFloatingPointSerialization("Infinity");
+ PerformFloatingPointSerialization("-Infinity");
+
+ static void PerformFloatingPointSerialization(string testString)
+ {
+ string testStringAsJson = $@"""{testString}""";
+ string testJson = @$"{{""FloatNumber"":{testStringAsJson},""DoubleNumber"":{testStringAsJson}}}";
+
+ StructWithNumbers obj;
+ switch (testString)
+ {
+ case "NaN":
+ obj = JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants);
+ Assert.Equal(float.NaN, obj.FloatNumber);
+ Assert.Equal(double.NaN, obj.DoubleNumber);
+
+ obj = JsonSerializer.Deserialize(testJson, s_optionReadFromStr);
+ Assert.Equal(float.NaN, obj.FloatNumber);
+ Assert.Equal(double.NaN, obj.DoubleNumber);
+ break;
+ case "Infinity":
+ obj = JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants);
+ Assert.Equal(float.PositiveInfinity, obj.FloatNumber);
+ Assert.Equal(double.PositiveInfinity, obj.DoubleNumber);
+
+ obj = JsonSerializer.Deserialize(testJson, s_optionReadFromStr);
+ Assert.Equal(float.PositiveInfinity, obj.FloatNumber);
+ Assert.Equal(double.PositiveInfinity, obj.DoubleNumber);
+ break;
+ case "-Infinity":
+ obj = JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants);
+ Assert.Equal(float.NegativeInfinity, obj.FloatNumber);
+ Assert.Equal(double.NegativeInfinity, obj.DoubleNumber);
+
+ obj = JsonSerializer.Deserialize(testJson, s_optionReadFromStr);
+ Assert.Equal(float.NegativeInfinity, obj.FloatNumber);
+ Assert.Equal(double.NegativeInfinity, obj.DoubleNumber);
+ break;
+ default:
+ Assert.Throws(() => JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants));
+ return;
+ }
+
+ JsonTestHelper.AssertJsonEqual(testJson, JsonSerializer.Serialize(obj, s_optionsAllowFloatConstants));
+ JsonTestHelper.AssertJsonEqual(testJson, JsonSerializer.Serialize(obj, s_optionWriteAsStr));
+ }
+ }
+
+ [Theory]
+ [InlineData("naN")]
+ [InlineData("Nan")]
+ [InlineData("NAN")]
+ [InlineData("+Infinity")]
+ [InlineData("+infinity")]
+ [InlineData("infinity")]
+ [InlineData("infinitY")]
+ [InlineData("INFINITY")]
+ [InlineData("+INFINITY")]
+ [InlineData("-infinity")]
+ [InlineData("-infinitY")]
+ [InlineData("-INFINITY")]
+ [InlineData(" NaN")]
+ [InlineData(" Infinity")]
+ [InlineData(" -Infinity")]
+ [InlineData("NaN ")]
+ [InlineData("Infinity ")]
+ [InlineData("-Infinity ")]
+ [InlineData("a-Infinity")]
+ [InlineData("NaNa")]
+ [InlineData("Infinitya")]
+ [InlineData("-Infinitya")]
+ public static void FloatingPointConstants_Fail(string testString)
+ {
+ string testStringAsJson = $@"""{testString}""";
+ string testJson = @$"{{""FloatNumber"":{testStringAsJson},""DoubleNumber"":{testStringAsJson}}}";
+ Assert.Throws(() => JsonSerializer.Deserialize(testJson, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(testJson, s_optionReadFromStr));
+ }
+
+ [Fact]
+ public static void AllowFloatingPointConstants_WriteAsNumber_IfNotConstant()
+ {
+ float @float = 1;
+ // Not written as "1"
+ Assert.Equal("1", JsonSerializer.Serialize(@float, s_optionsAllowFloatConstants));
+
+ double @double = 1;
+ // Not written as "1"
+ Assert.Equal("1", JsonSerializer.Serialize(@double, s_optionsAllowFloatConstants));
+ }
+
+ [Theory]
+ [InlineData("NaN")]
+ [InlineData("Infinity")]
+ [InlineData("-Infinity")]
+ public static void Unquoted_FloatingPointConstants_Read_Fail(string testString)
+ {
+ Assert.Throws(() => JsonSerializer.Deserialize(testString, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(testString, s_optionReadFromStr));
+ Assert.Throws(() => JsonSerializer.Deserialize(testString, s_optionReadFromStrAllowFloatConstants));
+ }
+
+ private struct StructWithNumbers
+ {
+ public float FloatNumber { get; set; }
+ public double DoubleNumber { get; set; }
+ }
+
+ [Fact]
+ public static void ReadFromString_AllowFloatingPoint()
+ {
+ string json = @"{""IntNumber"":""1"",""FloatNumber"":""NaN""}";
+ ClassWithNumbers obj = JsonSerializer.Deserialize(json, s_optionReadFromStrAllowFloatConstants);
+
+ Assert.Equal(1, obj.IntNumber);
+ Assert.Equal(float.NaN, obj.FloatNumber);
+
+ JsonTestHelper.AssertJsonEqual(@"{""IntNumber"":1,""FloatNumber"":""NaN""}", JsonSerializer.Serialize(obj, s_optionReadFromStrAllowFloatConstants));
+ }
+
+ [Fact]
+ public static void WriteAsString_AllowFloatingPoint()
+ {
+ string json = @"{""IntNumber"":""1"",""FloatNumber"":""NaN""}";
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionWriteAsStrAllowFloatConstants));
+
+ var obj = new ClassWithNumbers
+ {
+ IntNumber = 1,
+ FloatNumber = float.NaN
+ };
+
+ JsonTestHelper.AssertJsonEqual(json, JsonSerializer.Serialize(obj, s_optionWriteAsStrAllowFloatConstants));
+ }
+
+ public class ClassWithNumbers
+ {
+ public int IntNumber { get; set; }
+ public float FloatNumber { get; set; }
+ }
+
+ [Fact]
+ public static void FloatingPointConstants_IncompatibleNumber()
+ {
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ AssertFloatingPointIncompatible_Fails();
+ }
+
+ private static void AssertFloatingPointIncompatible_Fails()
+ {
+ string[] testCases = new[]
+ {
+ @"""NaN""",
+ @"""Infinity""",
+ @"""-Infinity""",
+ };
+
+ foreach (string test in testCases)
+ {
+ Assert.Throws(() => JsonSerializer.Deserialize(test, s_optionReadFromStrAllowFloatConstants));
+ }
+ }
+
+ [Fact]
+ public static void UnsupportedFormats()
+ {
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ AssertUnsupportedFormatThrows();
+ }
+
+ private static void AssertUnsupportedFormatThrows()
+ {
+ string[] testCases = new[]
+ {
+ "$123.46", // Currency
+ "100.00 %", // Percent
+ "1234,57", // Fixed point
+ "00FF", // Hexadecimal
+ };
+
+ foreach (string test in testCases)
+ {
+ Assert.Throws(() => JsonSerializer.Deserialize(test, s_optionReadFromStr));
+ }
+ }
+
+ [Fact]
+ public static void EscapingTest()
+ {
+ // Cause all characters to be escaped.
+ var encoderSettings = new TextEncoderSettings();
+ encoderSettings.ForbidCharacters('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '+', '-', 'e', 'E');
+
+ JavaScriptEncoder encoder = JavaScriptEncoder.Create(encoderSettings);
+ var options = new JsonSerializerOptions(s_optionReadAndWriteFromStr)
+ {
+ Encoder = encoder
+ };
+
+ PerformEscapingTest(JsonNumberTestData.Bytes, options);
+ PerformEscapingTest(JsonNumberTestData.SBytes, options);
+ PerformEscapingTest(JsonNumberTestData.Shorts, options);
+ PerformEscapingTest(JsonNumberTestData.Ints, options);
+ PerformEscapingTest(JsonNumberTestData.Longs, options);
+ PerformEscapingTest(JsonNumberTestData.UShorts, options);
+ PerformEscapingTest(JsonNumberTestData.UInts, options);
+ PerformEscapingTest(JsonNumberTestData.ULongs, options);
+ PerformEscapingTest(JsonNumberTestData.Floats, options);
+ PerformEscapingTest(JsonNumberTestData.Doubles, options);
+ PerformEscapingTest(JsonNumberTestData.Decimals, options);
+ }
+
+ private static void PerformEscapingTest(List numbers, JsonSerializerOptions options)
+ {
+ // All input characters are escaped
+ IEnumerable numbersAsStrings = numbers.Select(num => GetNumberAsString(num));
+ string input = JsonSerializer.Serialize(numbersAsStrings, options);
+ AssertListNumbersEscaped(input);
+
+ // Unescaping works
+ List deserialized = JsonSerializer.Deserialize>(input, options);
+ Assert.Equal(numbers.Count, deserialized.Count);
+ for (int i = 0; i < numbers.Count; i++)
+ {
+ Assert.Equal(numbers[i], deserialized[i]);
+ }
+
+ // Every number is written as a string, and custom escaping is not honored.
+ string serialized = JsonSerializer.Serialize(deserialized, options);
+ AssertListNumbersUnescaped(serialized);
+ }
+
+ private static void AssertListNumbersEscaped(string json)
+ {
+ var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
+ reader.Read();
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndArray)
+ {
+ break;
+ }
+ else
+ {
+ Assert.Equal(JsonTokenType.String, reader.TokenType);
+#if BUILDING_INBOX_LIBRARY
+ Assert.True(reader.ValueSpan.Contains((byte)'\\'));
+#else
+ bool foundBackSlash = false;
+ foreach (byte val in reader.ValueSpan)
+ {
+ if (val == (byte)'\\')
+ {
+ foundBackSlash = true;
+ break;
+ }
+ }
+
+ Assert.True(foundBackSlash, "Expected escape token.");
+#endif
+ }
+ }
+ }
+
+ private static void AssertListNumbersUnescaped(string json)
+ {
+ var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
+ reader.Read();
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndArray)
+ {
+ break;
+ }
+ else
+ {
+ Assert.Equal(JsonTokenType.String, reader.TokenType);
+#if BUILDING_INBOX_LIBRARY
+ Assert.False(reader.ValueSpan.Contains((byte)'\\'));
+#else
+ foreach (byte val in reader.ValueSpan)
+ {
+ if (val == (byte)'\\')
+ {
+ Assert.True(false, "Unexpected escape token.");
+ }
+ }
+#endif
+ }
+ }
+ }
+
+ [Fact]
+ public static void Number_RoundtripNull()
+ {
+ Perform_Number_RoundTripNull_Test();
+ Perform_Number_RoundTripNull_Test();
+ Perform_Number_RoundTripNull_Test();
+ Perform_Number_RoundTripNull_Test();
+ Perform_Number_RoundTripNull_Test();
+ Perform_Number_RoundTripNull_Test();
+ Perform_Number_RoundTripNull_Test();
+ Perform_Number_RoundTripNull_Test();
+ Perform_Number_RoundTripNull_Test();
+ Perform_Number_RoundTripNull_Test();
+ }
+
+ private static void Perform_Number_RoundTripNull_Test()
+ {
+ string nullAsJson = "null";
+ string nullAsQuotedJson = $@"""{nullAsJson}""";
+
+ Assert.Throws(() => JsonSerializer.Deserialize(nullAsJson, s_optionReadAndWriteFromStr));
+ Assert.Equal("0", JsonSerializer.Serialize(default(T)));
+ Assert.Throws(() => JsonSerializer.Deserialize(nullAsQuotedJson, s_optionReadAndWriteFromStr));
+ }
+
+ [Fact]
+ public static void NullableNumber_RoundtripNull()
+ {
+ Perform_NullableNumber_RoundTripNull_Test();
+ Perform_NullableNumber_RoundTripNull_Test();
+ Perform_NullableNumber_RoundTripNull_Test();
+ Perform_NullableNumber_RoundTripNull_Test();
+ Perform_NullableNumber_RoundTripNull_Test();
+ Perform_NullableNumber_RoundTripNull_Test();
+ Perform_NullableNumber_RoundTripNull_Test();
+ Perform_NullableNumber_RoundTripNull_Test();
+ Perform_NullableNumber_RoundTripNull_Test();
+ Perform_NullableNumber_RoundTripNull_Test();
+ }
+
+ private static void Perform_NullableNumber_RoundTripNull_Test()
+ {
+ string nullAsJson = "null";
+ string nullAsQuotedJson = $@"""{nullAsJson}""";
+
+ Assert.Null(JsonSerializer.Deserialize(nullAsJson, s_optionReadAndWriteFromStr));
+ Assert.Equal(nullAsJson, JsonSerializer.Serialize(default(T)));
+ Assert.Throws(() => JsonSerializer.Deserialize(nullAsQuotedJson, s_optionReadAndWriteFromStr));
+ }
+
+ [Fact]
+ public static void Disallow_ArbritaryStrings_On_AllowFloatingPointConstants()
+ {
+ string json = @"""12345""";
+
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsAllowFloatConstants));
+ Assert.Throws(() => JsonSerializer.Deserialize