Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JsonNumberHandling(Attribute) & support for (de)serializing numbers from/to string #39363

Merged
merged 4 commits into from
Jul 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 { } }
Expand Down Expand Up @@ -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() { }
Expand Down Expand Up @@ -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)]
layomia marked this conversation as resolved.
Show resolved Hide resolved
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
{
Expand Down
6 changes: 6 additions & 0 deletions src/libraries/System.Text.Json/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -536,4 +536,10 @@
<data name="IgnoreConditionOnValueTypeInvalid" xml:space="preserve">
<value>The ignore condition 'JsonIgnoreCondition.WhenWritingNull' is not valid on value-type member '{0}' on type '{1}'. Consider using 'JsonIgnoreCondition.WhenWritingDefault'.</value>
</data>
<data name="NumberHandlingConverterMustBeBuiltIn" xml:space="preserve">
<value>'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}'.</value>
</data>
<data name="NumberHandlingOnPropertyTypeMustBeNumberOrCollection" xml:space="preserve">
<value>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}'.</value>
</data>
</root>
2 changes: 2 additions & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<Compile Include="System\Text\Json\Serialization\Attributes\JsonExtensionDataAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Attributes\JsonIgnoreAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Attributes\JsonIncludeAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Attributes\JsonNumberHandlingAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Attributes\JsonPropertyNameAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\ClassType.cs" />
<Compile Include="System\Text\Json\Serialization\ConverterList.cs" />
Expand Down Expand Up @@ -135,6 +136,7 @@
<Compile Include="System\Text\Json\Serialization\JsonDefaultNamingPolicy.cs" />
<Compile Include="System\Text\Json\Serialization\JsonIgnoreCondition.cs" />
<Compile Include="System\Text\Json\Serialization\JsonNamingPolicy.cs" />
<Compile Include="System\Text\Json\Serialization\JsonNumberHandling.cs" />
<Compile Include="System\Text\Json\Serialization\JsonParameterInfo.cs" />
<Compile Include="System\Text\Json\Serialization\JsonParameterInfoOfT.cs" />
<Compile Include="System\Text\Json\Serialization\JsonPropertyInfo.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ internal static class JsonConstants
public static ReadOnlySpan<byte> FalseValue => new byte[] { (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e' };
public static ReadOnlySpan<byte> NullValue => new byte[] { (byte)'n', (byte)'u', (byte)'l', (byte)'l' };

public static ReadOnlySpan<byte> NaNValue => new byte[] { (byte)'N', (byte)'a', (byte)'N' };
public static ReadOnlySpan<byte> PositiveInfinityValue => new byte[] { (byte)'I', (byte)'n', (byte)'f', (byte)'i', (byte)'n', (byte)'i', (byte)'t', (byte)'y' };
public static ReadOnlySpan<byte> NegativeInfinityValue => new byte[] { (byte)'-', (byte)'I', (byte)'n', (byte)'f', (byte)'i', (byte)'n', (byte)'i', (byte)'t', (byte)'y' };

layomia marked this conversation as resolved.
Show resolved Hide resolved
// Used to search for the end of a number
public static ReadOnlySpan<byte> Delimiters => new byte[] { ListSeparator, CloseBrace, CloseBracket, Space, LineFeed, CarriageReturn, Tab, Slash };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,5 +321,82 @@ public static bool TryGetEscapedGuid(ReadOnlySpan<byte> source, out Guid value)
value = default;
return false;
}

public static char GetFloatingPointStandardParseFormat(ReadOnlySpan<byte> 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;
}
layomia marked this conversation as resolved.
Show resolved Hide resolved
}
return default;
}

public static bool TryGetFloatingPointConstant(ReadOnlySpan<byte> 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))
layomia marked this conversation as resolved.
Show resolved Hide resolved
{
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<byte> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -416,11 +416,38 @@ public float GetSingle()
internal float GetSingleWithQuotes()
{
ReadOnlySpan<byte> 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);
layomia marked this conversation as resolved.
Show resolved Hide resolved
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))
layomia marked this conversation as resolved.
Show resolved Hide resolved
{
return value;
}
}

throw ThrowHelper.GetFormatException(NumericType.Single);
}

internal float GetSingleFloatingPointConstant()
{
ReadOnlySpan<byte> span = GetUnescapedSpan();

if (JsonReaderHelper.TryGetFloatingPointConstant(span, out float value))
layomia marked this conversation as resolved.
Show resolved Hide resolved
{
return value;
}

throw ThrowHelper.GetFormatException(NumericType.Single);
}

/// <summary>
Expand Down Expand Up @@ -449,11 +476,38 @@ public double GetDouble()
internal double GetDoubleWithQuotes()
{
ReadOnlySpan<byte> 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<byte> span = GetUnescapedSpan();

if (JsonReaderHelper.TryGetFloatingPointConstant(span, out double value))
{
return value;
}

throw ThrowHelper.GetFormatException(NumericType.Double);
}

/// <summary>
Expand Down Expand Up @@ -482,11 +536,15 @@ public decimal GetDecimal()
internal decimal GetDecimalWithQuotes()
{
ReadOnlySpan<byte> 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);
}

/// <summary>
Expand Down Expand Up @@ -919,13 +977,8 @@ public bool TryGetSingle(out float value)
throw ThrowHelper.GetInvalidOperationException_ExpectedNumber(TokenType);
}

ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
return TryGetSingleCore(out value, span);
}
ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;;
layomia marked this conversation as resolved.
Show resolved Hide resolved

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool TryGetSingleCore(out float value, ReadOnlySpan<byte> span)
{
layomia marked this conversation as resolved.
Show resolved Hide resolved
if (Utf8Parser.TryParse(span, out float tmp, out int bytesConsumed, _numberFormat)
&& span.Length == bytesConsumed)
{
Expand Down Expand Up @@ -955,12 +1008,7 @@ public bool TryGetDouble(out double value)
}

ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
return TryGetDoubleCore(out value, span);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool TryGetDoubleCore(out double value, ReadOnlySpan<byte> span)
{
if (Utf8Parser.TryParse(span, out double tmp, out int bytesConsumed, _numberFormat)
&& span.Length == bytesConsumed)
{
Expand Down Expand Up @@ -990,12 +1038,7 @@ public bool TryGetDecimal(out decimal value)
}

ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
return TryGetDecimalCore(out value, span);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool TryGetDecimalCore(out decimal value, ReadOnlySpan<byte> span)
{
if (Utf8Parser.TryParse(span, out decimal tmp, out int bytesConsumed, _numberFormat)
&& span.Length == bytesConsumed)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// When placed on a type, property, or field, indicates what <see cref="JsonNumberHandling"/>
/// settings should be used when serializing or deserialing numbers.
layomia marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class JsonNumberHandlingAttribute : JsonAttribute
{
/// <summary>
/// Indicates what settings should be used when serializing or deserialing numbers.
layomia marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public JsonNumberHandling Handling { get; }

/// <summary>
/// Initializes a new instance of <see cref="JsonNumberHandlingAttribute"/>.
/// </summary>
public JsonNumberHandlingAttribute(JsonNumberHandling handling)
{
if (!JsonSerializer.IsValidNumberHandlingValue(handling))
{
throw new ArgumentOutOfRangeException(nameof(handling));
}
Handling = handling;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value,
int index = state.Current.EnumeratorIndex;

JsonConverter<TElement> 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++)
Expand Down
Loading