Skip to content

Commit 44446b1

Browse files
authored
Add JsonNumberHandling & support for (de)serializing numbers from/to string (dotnet#39685)
1 parent e637fc5 commit 44446b1

File tree

64 files changed

+2502
-147
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+2502
-147
lines changed

src/libraries/System.Text.Json/ref/System.Text.Json.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults defaults) {
244244
public bool IgnoreReadOnlyFields { get { throw null; } set { } }
245245
public bool IncludeFields { get { throw null; } set { } }
246246
public int MaxDepth { get { throw null; } set { } }
247+
public System.Text.Json.Serialization.JsonNumberHandling NumberHandling { get { throw null; } set { } }
247248
public bool PropertyNameCaseInsensitive { get { throw null; } set { } }
248249
public System.Text.Json.JsonNamingPolicy? PropertyNamingPolicy { get { throw null; } set { } }
249250
public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } }
@@ -496,6 +497,14 @@ public enum JsonIgnoreCondition
496497
WhenWritingDefault = 2,
497498
WhenWritingNull = 3,
498499
}
500+
[System.FlagsAttribute]
501+
public enum JsonNumberHandling
502+
{
503+
Strict = 0,
504+
AllowReadingFromString = 1,
505+
WriteAsString = 2,
506+
AllowNamedFloatingPointLiterals = 4,
507+
}
499508
public abstract partial class JsonAttribute : System.Attribute
500509
{
501510
protected JsonAttribute() { }
@@ -533,6 +542,12 @@ protected internal JsonConverter() { }
533542
public abstract T Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options);
534543
public abstract void Write(System.Text.Json.Utf8JsonWriter writer, T value, System.Text.Json.JsonSerializerOptions options);
535544
}
545+
[System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Struct | System.AttributeTargets.Property | System.AttributeTargets.Field, AllowMultiple = false)]
546+
public sealed partial class JsonNumberHandlingAttribute : System.Text.Json.Serialization.JsonAttribute
547+
{
548+
public JsonNumberHandlingAttribute(System.Text.Json.Serialization.JsonNumberHandling handling) { }
549+
public System.Text.Json.Serialization.JsonNumberHandling Handling { get { throw null; } }
550+
}
536551
[System.AttributeUsageAttribute(System.AttributeTargets.Constructor, AllowMultiple = false)]
537552
public sealed partial class JsonConstructorAttribute : System.Text.Json.Serialization.JsonAttribute
538553
{

src/libraries/System.Text.Json/src/Resources/Strings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,4 +536,10 @@
536536
<data name="IgnoreConditionOnValueTypeInvalid" xml:space="preserve">
537537
<value>The ignore condition 'JsonIgnoreCondition.WhenWritingNull' is not valid on value-type member '{0}' on type '{1}'. Consider using 'JsonIgnoreCondition.WhenWritingDefault'.</value>
538538
</data>
539+
<data name="NumberHandlingConverterMustBeBuiltIn" xml:space="preserve">
540+
<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>
541+
</data>
542+
<data name="NumberHandlingOnPropertyTypeMustBeNumberOrCollection" xml:space="preserve">
543+
<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>
544+
</data>
539545
</root>

src/libraries/System.Text.Json/src/System.Text.Json.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
<Compile Include="System\Text\Json\Serialization\Attributes\JsonExtensionDataAttribute.cs" />
6161
<Compile Include="System\Text\Json\Serialization\Attributes\JsonIgnoreAttribute.cs" />
6262
<Compile Include="System\Text\Json\Serialization\Attributes\JsonIncludeAttribute.cs" />
63+
<Compile Include="System\Text\Json\Serialization\Attributes\JsonNumberHandlingAttribute.cs" />
6364
<Compile Include="System\Text\Json\Serialization\Attributes\JsonPropertyNameAttribute.cs" />
6465
<Compile Include="System\Text\Json\Serialization\ClassType.cs" />
6566
<Compile Include="System\Text\Json\Serialization\ConverterList.cs" />
@@ -135,6 +136,7 @@
135136
<Compile Include="System\Text\Json\Serialization\JsonDefaultNamingPolicy.cs" />
136137
<Compile Include="System\Text\Json\Serialization\JsonIgnoreCondition.cs" />
137138
<Compile Include="System\Text\Json\Serialization\JsonNamingPolicy.cs" />
139+
<Compile Include="System\Text\Json\Serialization\JsonNumberHandling.cs" />
138140
<Compile Include="System\Text\Json\Serialization\JsonParameterInfo.cs" />
139141
<Compile Include="System\Text\Json\Serialization\JsonParameterInfoOfT.cs" />
140142
<Compile Include="System\Text\Json\Serialization\JsonPropertyInfo.cs" />

src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ internal static class JsonConstants
3737
public static ReadOnlySpan<byte> FalseValue => new byte[] { (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e' };
3838
public static ReadOnlySpan<byte> NullValue => new byte[] { (byte)'n', (byte)'u', (byte)'l', (byte)'l' };
3939

40+
public static ReadOnlySpan<byte> NaNValue => new byte[] { (byte)'N', (byte)'a', (byte)'N' };
41+
public static ReadOnlySpan<byte> PositiveInfinityValue => new byte[] { (byte)'I', (byte)'n', (byte)'f', (byte)'i', (byte)'n', (byte)'i', (byte)'t', (byte)'y' };
42+
public static ReadOnlySpan<byte> NegativeInfinityValue => new byte[] { (byte)'-', (byte)'I', (byte)'n', (byte)'f', (byte)'i', (byte)'n', (byte)'i', (byte)'t', (byte)'y' };
43+
4044
// Used to search for the end of a number
4145
public static ReadOnlySpan<byte> Delimiters => new byte[] { ListSeparator, CloseBrace, CloseBracket, Space, LineFeed, CarriageReturn, Tab, Slash };
4246

src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,5 +321,82 @@ public static bool TryGetEscapedGuid(ReadOnlySpan<byte> source, out Guid value)
321321
value = default;
322322
return false;
323323
}
324+
325+
public static char GetFloatingPointStandardParseFormat(ReadOnlySpan<byte> span)
326+
{
327+
// Assume that 'e/E' is closer to the end.
328+
int startIndex = span.Length - 1;
329+
for (int i = startIndex; i >= 0; i--)
330+
{
331+
byte token = span[i];
332+
if (token == 'E' || token == 'e')
333+
{
334+
return JsonConstants.ScientificNotationFormat;
335+
}
336+
}
337+
return default;
338+
}
339+
340+
public static bool TryGetFloatingPointConstant(ReadOnlySpan<byte> span, out float value)
341+
{
342+
if (span.Length == 3)
343+
{
344+
if (span.SequenceEqual(JsonConstants.NaNValue))
345+
{
346+
value = float.NaN;
347+
return true;
348+
}
349+
}
350+
else if (span.Length == 8)
351+
{
352+
if (span.SequenceEqual(JsonConstants.PositiveInfinityValue))
353+
{
354+
value = float.PositiveInfinity;
355+
return true;
356+
}
357+
}
358+
else if (span.Length == 9)
359+
{
360+
if (span.SequenceEqual(JsonConstants.NegativeInfinityValue))
361+
{
362+
value = float.NegativeInfinity;
363+
return true;
364+
}
365+
}
366+
367+
value = 0;
368+
return false;
369+
}
370+
371+
public static bool TryGetFloatingPointConstant(ReadOnlySpan<byte> span, out double value)
372+
{
373+
if (span.Length == 3)
374+
{
375+
if (span.SequenceEqual(JsonConstants.NaNValue))
376+
{
377+
value = double.NaN;
378+
return true;
379+
}
380+
}
381+
else if (span.Length == 8)
382+
{
383+
if (span.SequenceEqual(JsonConstants.PositiveInfinityValue))
384+
{
385+
value = double.PositiveInfinity;
386+
return true;
387+
}
388+
}
389+
else if (span.Length == 9)
390+
{
391+
if (span.SequenceEqual(JsonConstants.NegativeInfinityValue))
392+
{
393+
value = double.NegativeInfinity;
394+
return true;
395+
}
396+
}
397+
398+
value = 0;
399+
return false;
400+
}
324401
}
325402
}

src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -416,11 +416,38 @@ public float GetSingle()
416416
internal float GetSingleWithQuotes()
417417
{
418418
ReadOnlySpan<byte> span = GetUnescapedSpan();
419-
if (!TryGetSingleCore(out float value, span))
419+
420+
if (JsonReaderHelper.TryGetFloatingPointConstant(span, out float value))
420421
{
421-
throw ThrowHelper.GetFormatException(NumericType.Single);
422+
return value;
422423
}
423-
return value;
424+
425+
char numberFormat = JsonReaderHelper.GetFloatingPointStandardParseFormat(span);
426+
if (Utf8Parser.TryParse(span, out value, out int bytesConsumed, numberFormat)
427+
&& span.Length == bytesConsumed)
428+
{
429+
// NETCOREAPP implementation of the TryParse method above permits case-insenstive variants of the
430+
// float constants "NaN", "Infinity", "-Infinity". This differs from the NETFRAMEWORK implementation.
431+
// The following logic reconciles the two implementations to enforce consistent behavior.
432+
if (!float.IsNaN(value) && !float.IsPositiveInfinity(value) && !float.IsNegativeInfinity(value))
433+
{
434+
return value;
435+
}
436+
}
437+
438+
throw ThrowHelper.GetFormatException(NumericType.Single);
439+
}
440+
441+
internal float GetSingleFloatingPointConstant()
442+
{
443+
ReadOnlySpan<byte> span = GetUnescapedSpan();
444+
445+
if (JsonReaderHelper.TryGetFloatingPointConstant(span, out float value))
446+
{
447+
return value;
448+
}
449+
450+
throw ThrowHelper.GetFormatException(NumericType.Single);
424451
}
425452

426453
/// <summary>
@@ -449,11 +476,38 @@ public double GetDouble()
449476
internal double GetDoubleWithQuotes()
450477
{
451478
ReadOnlySpan<byte> span = GetUnescapedSpan();
452-
if (!TryGetDoubleCore(out double value, span))
479+
480+
if (JsonReaderHelper.TryGetFloatingPointConstant(span, out double value))
453481
{
454-
throw ThrowHelper.GetFormatException(NumericType.Double);
482+
return value;
455483
}
456-
return value;
484+
485+
char numberFormat = JsonReaderHelper.GetFloatingPointStandardParseFormat(span);
486+
if (Utf8Parser.TryParse(span, out value, out int bytesConsumed, numberFormat)
487+
&& span.Length == bytesConsumed)
488+
{
489+
// NETCOREAPP implmentation of the TryParse method above permits case-insenstive variants of the
490+
// float constants "NaN", "Infinity", "-Infinity". This differs from the NETFRAMEWORK implementation.
491+
// The following logic reconciles the two implementations to enforce consistent behavior.
492+
if (!double.IsNaN(value) && !double.IsPositiveInfinity(value) && !double.IsNegativeInfinity(value))
493+
{
494+
return value;
495+
}
496+
}
497+
498+
throw ThrowHelper.GetFormatException(NumericType.Double);
499+
}
500+
501+
internal double GetDoubleFloatingPointConstant()
502+
{
503+
ReadOnlySpan<byte> span = GetUnescapedSpan();
504+
505+
if (JsonReaderHelper.TryGetFloatingPointConstant(span, out double value))
506+
{
507+
return value;
508+
}
509+
510+
throw ThrowHelper.GetFormatException(NumericType.Double);
457511
}
458512

459513
/// <summary>
@@ -482,11 +536,15 @@ public decimal GetDecimal()
482536
internal decimal GetDecimalWithQuotes()
483537
{
484538
ReadOnlySpan<byte> span = GetUnescapedSpan();
485-
if (!TryGetDecimalCore(out decimal value, span))
539+
540+
char numberFormat = JsonReaderHelper.GetFloatingPointStandardParseFormat(span);
541+
if (Utf8Parser.TryParse(span, out decimal value, out int bytesConsumed, numberFormat)
542+
&& span.Length == bytesConsumed)
486543
{
487-
throw ThrowHelper.GetFormatException(NumericType.Decimal);
544+
return value;
488545
}
489-
return value;
546+
547+
throw ThrowHelper.GetFormatException(NumericType.Decimal);
490548
}
491549

492550
/// <summary>
@@ -919,13 +977,8 @@ public bool TryGetSingle(out float value)
919977
throw ThrowHelper.GetInvalidOperationException_ExpectedNumber(TokenType);
920978
}
921979

922-
ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
923-
return TryGetSingleCore(out value, span);
924-
}
980+
ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;;
925981

926-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
927-
internal bool TryGetSingleCore(out float value, ReadOnlySpan<byte> span)
928-
{
929982
if (Utf8Parser.TryParse(span, out float tmp, out int bytesConsumed, _numberFormat)
930983
&& span.Length == bytesConsumed)
931984
{
@@ -955,12 +1008,7 @@ public bool TryGetDouble(out double value)
9551008
}
9561009

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

961-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
962-
internal bool TryGetDoubleCore(out double value, ReadOnlySpan<byte> span)
963-
{
9641012
if (Utf8Parser.TryParse(span, out double tmp, out int bytesConsumed, _numberFormat)
9651013
&& span.Length == bytesConsumed)
9661014
{
@@ -990,12 +1038,7 @@ public bool TryGetDecimal(out decimal value)
9901038
}
9911039

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

996-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
997-
internal bool TryGetDecimalCore(out decimal value, ReadOnlySpan<byte> span)
998-
{
9991042
if (Utf8Parser.TryParse(span, out decimal tmp, out int bytesConsumed, _numberFormat)
10001043
&& span.Length == bytesConsumed)
10011044
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System.Text.Json.Serialization
5+
{
6+
/// <summary>
7+
/// When placed on a type, property, or field, indicates what <see cref="JsonNumberHandling"/>
8+
/// settings should be used when serializing or deserialing numbers.
9+
/// </summary>
10+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
11+
public sealed class JsonNumberHandlingAttribute : JsonAttribute
12+
{
13+
/// <summary>
14+
/// Indicates what settings should be used when serializing or deserialing numbers.
15+
/// </summary>
16+
public JsonNumberHandling Handling { get; }
17+
18+
/// <summary>
19+
/// Initializes a new instance of <see cref="JsonNumberHandlingAttribute"/>.
20+
/// </summary>
21+
public JsonNumberHandlingAttribute(JsonNumberHandling handling)
22+
{
23+
if (!JsonSerializer.IsValidNumberHandlingValue(handling))
24+
{
25+
throw new ArgumentOutOfRangeException(nameof(handling));
26+
}
27+
Handling = handling;
28+
}
29+
}
30+
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value,
3838
int index = state.Current.EnumeratorIndex;
3939

4040
JsonConverter<TElement> elementConverter = GetElementConverter(ref state);
41-
if (elementConverter.CanUseDirectReadOrWrite)
41+
if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null)
4242
{
4343
// Fast path that avoids validation and extra indirection.
4444
for (; index < array.Length; index++)

0 commit comments

Comments
 (0)