Skip to content

Commit

Permalink
Add JsonNumberHandling(Attribute) & support for (de)serializing numbe…
Browse files Browse the repository at this point in the history
…rs from/to string (#39363)

* Add JsonNumberHandling & support for (de)serializing numbers from/to string

* Add JsonNumberHandlingAttribute

* Address review feedback + fixups

* Address review feedback ii
  • Loading branch information
layomia authored Jul 21, 2020
1 parent 82d7d5f commit 650f28f
Show file tree
Hide file tree
Showing 64 changed files with 2,502 additions and 147 deletions.
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)]
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' };

// 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;
}
}
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))
{
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);
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<byte> span = GetUnescapedSpan();

if (JsonReaderHelper.TryGetFloatingPointConstant(span, out float value))
{
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;;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool TryGetSingleCore(out float value, ReadOnlySpan<byte> span)
{
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.
/// </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.
/// </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

0 comments on commit 650f28f

Please sign in to comment.