Skip to content

Commit e2c04e0

Browse files
authored
JSON: Add support for Int128, UInt128 and Half (#88962)
* JSON: Add support for Int128, UInt128 and Half and add Number support for Utf8JsonReader.CopyString(...) * Remove parsing limits on Read and move Number support of CopyString to an internal helper * Fix AllowNamedFloatingPointLiterals on Write for Half * Specify InvariantCulture on TryParse and TryFormat Fix handling of floating-point literals on HalfConverter Remove CopyString tests related to Number support * Add test for invalid number input format * Fix net6.0 build error about missing Half.TryParse overload * Move rentedCharBuffer logic to TryParse helper * Address feedback * Disable test for OSX
1 parent 2f843a8 commit e2c04e0

20 files changed

+1003
-4
lines changed

src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,15 @@ public KnownTypeSymbols(Compilation compilation)
135135
public INamedTypeSymbol? TimeOnlyType => GetOrResolveType("System.TimeOnly", ref _TimeOnlyType);
136136
private Option<INamedTypeSymbol?> _TimeOnlyType;
137137

138+
public INamedTypeSymbol? Int128Type => GetOrResolveType("System.Int128", ref _Int128Type);
139+
private Option<INamedTypeSymbol?> _Int128Type;
140+
141+
public INamedTypeSymbol? UInt128Type => GetOrResolveType("System.UInt128", ref _UInt128Type);
142+
private Option<INamedTypeSymbol?> _UInt128Type;
143+
144+
public INamedTypeSymbol? HalfType => GetOrResolveType("System.Half", ref _HalfType);
145+
private Option<INamedTypeSymbol?> _HalfType;
146+
138147
public IArrayTypeSymbol? ByteArrayType => _ByteArrayType.HasValue
139148
? _ByteArrayType.Value
140149
: (_ByteArrayType = new(Compilation.CreateArrayTypeSymbol(Compilation.GetSpecialType(SpecialType.System_Byte), rank: 1))).Value;

src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1699,6 +1699,9 @@ private static HashSet<ITypeSymbol> CreateBuiltInSupportTypeSet(KnownTypeSymbols
16991699
AddTypeIfNotNull(knownSymbols.DateTimeOffsetType);
17001700
AddTypeIfNotNull(knownSymbols.DateOnlyType);
17011701
AddTypeIfNotNull(knownSymbols.TimeOnlyType);
1702+
AddTypeIfNotNull(knownSymbols.Int128Type);
1703+
AddTypeIfNotNull(knownSymbols.UInt128Type);
1704+
AddTypeIfNotNull(knownSymbols.HalfType);
17021705
AddTypeIfNotNull(knownSymbols.GuidType);
17031706
AddTypeIfNotNull(knownSymbols.UriType);
17041707
AddTypeIfNotNull(knownSymbols.VersionType);

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ namespace System.Text.Json.Serialization.Metadata
99
public static partial class JsonMetadataServices
1010
{
1111
public static System.Text.Json.Serialization.JsonConverter<System.DateOnly> DateOnlyConverter { get { throw null; } }
12+
public static System.Text.Json.Serialization.JsonConverter<System.Half> HalfConverter { get { throw null; } }
1213
public static System.Text.Json.Serialization.JsonConverter<System.TimeOnly> TimeOnlyConverter { get { throw null; } }
14+
15+
#if NET7_0_OR_GREATER
16+
public static System.Text.Json.Serialization.JsonConverter<System.Int128> Int128Converter { get { throw null; } }
17+
[System.CLSCompliantAttribute(false)]
18+
public static System.Text.Json.Serialization.JsonConverter<System.UInt128> UInt128Converter { get { throw null; } }
19+
#endif
1320
}
1421
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,4 +690,13 @@
690690
<data name="ObjectCreationHandlingPropertyCannotAllowReferenceHandling" xml:space="preserve">
691691
<value>JsonObjectCreationHandling.Populate is incompatible with reference handling.</value>
692692
</data>
693+
<data name="FormatInt128" xml:space="preserve">
694+
<value>Either the JSON value is not in a supported format, or is out of bounds for an Int128.</value>
695+
</data>
696+
<data name="FormatUInt128" xml:space="preserve">
697+
<value>Either the JSON value is not in a supported format, or is out of bounds for an UInt128.</value>
698+
</data>
699+
<data name="FormatHalf" xml:space="preserve">
700+
<value>Either the JSON value is not in a supported format, or is out of bounds for a Half.</value>
701+
</data>
693702
</root>

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,11 +346,17 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
346346
<Compile Include="$(CoreLibSharedDir)System\Diagnostics\CodeAnalysis\UnconditionalSuppressMessageAttribute.cs" />
347347
</ItemGroup>
348348

349+
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' and $([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '7.0'))">
350+
<Compile Include="System\Text\Json\Serialization\Converters\Value\Int128Converter.cs" />
351+
<Compile Include="System\Text\Json\Serialization\Converters\Value\UInt128Converter.cs" />
352+
</ItemGroup>
353+
349354
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
350355
<Compile Include="System.Text.Json.Typeforwards.netcoreapp.cs" />
351356
<Compile Include="System\Text\Json\Serialization\JsonSerializerOptionsUpdateHandler.cs" />
352357
<Compile Include="System\Text\Json\Serialization\Converters\Value\DateOnlyConverter.cs" />
353358
<Compile Include="System\Text\Json\Serialization\Converters\Value\TimeOnlyConverter.cs" />
359+
<Compile Include="System\Text\Json\Serialization\Converters\Value\HalfConverter.cs" />
354360
</ItemGroup>
355361

356362
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,39 @@ public static bool TryGetEscapedGuid(ReadOnlySpan<byte> source, out Guid value)
145145
return false;
146146
}
147147

148+
#if NETCOREAPP
149+
public static bool TryGetFloatingPointConstant(ReadOnlySpan<byte> span, out Half value)
150+
{
151+
if (span.Length == 3)
152+
{
153+
if (span.SequenceEqual(JsonConstants.NaNValue))
154+
{
155+
value = Half.NaN;
156+
return true;
157+
}
158+
}
159+
else if (span.Length == 8)
160+
{
161+
if (span.SequenceEqual(JsonConstants.PositiveInfinityValue))
162+
{
163+
value = Half.PositiveInfinity;
164+
return true;
165+
}
166+
}
167+
else if (span.Length == 9)
168+
{
169+
if (span.SequenceEqual(JsonConstants.NegativeInfinityValue))
170+
{
171+
value = Half.NegativeInfinity;
172+
return true;
173+
}
174+
}
175+
176+
value = default;
177+
return false;
178+
}
179+
#endif
180+
148181
public static bool TryGetFloatingPointConstant(ReadOnlySpan<byte> span, out float value)
149182
{
150183
if (span.Length == 3)

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ public readonly int CopyString(Span<byte> utf8Destination)
7373
ThrowHelper.ThrowInvalidOperationException_ExpectedString(_tokenType);
7474
}
7575

76+
return CopyValue(utf8Destination);
77+
}
78+
79+
internal readonly int CopyValue(Span<byte> utf8Destination)
80+
{
81+
Debug.Assert(_tokenType is JsonTokenType.String or JsonTokenType.PropertyName or JsonTokenType.Number);
82+
Debug.Assert(_tokenType != JsonTokenType.Number || !ValueIsEscaped, "Numbers can't contain escape characters.");
83+
7684
int bytesWritten;
7785

7886
if (ValueIsEscaped)
@@ -129,6 +137,14 @@ public readonly int CopyString(Span<char> destination)
129137
ThrowHelper.ThrowInvalidOperationException_ExpectedString(_tokenType);
130138
}
131139

140+
return CopyValue(destination);
141+
}
142+
143+
internal readonly int CopyValue(Span<char> destination)
144+
{
145+
Debug.Assert(_tokenType is JsonTokenType.String or JsonTokenType.PropertyName or JsonTokenType.Number);
146+
Debug.Assert(_tokenType != JsonTokenType.Number || !ValueIsEscaped, "Numbers can't contain escape characters.");
147+
132148
scoped ReadOnlySpan<byte> unescapedSource;
133149
byte[]? rentedBuffer = null;
134150
int valueLength;

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/CharConverter.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ internal sealed class CharConverter : JsonPrimitiveConverter<char>
1212

1313
public override char Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
1414
{
15+
if (reader.TokenType is not (JsonTokenType.String or JsonTokenType.PropertyName))
16+
{
17+
ThrowHelper.ThrowInvalidOperationException_ExpectedString(reader.TokenType);
18+
}
19+
1520
if (!JsonHelpers.IsInRangeInclusive(reader.ValueLength, 1, MaxEscapedCharacterLength))
1621
{
1722
ThrowHelper.ThrowInvalidOperationException_ExpectedChar(reader.TokenType);
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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+
using System.Buffers;
5+
using System.Diagnostics;
6+
using System.Globalization;
7+
8+
namespace System.Text.Json.Serialization.Converters
9+
{
10+
internal sealed class HalfConverter : JsonPrimitiveConverter<Half>
11+
{
12+
private const int MaxFormatLength = 20;
13+
14+
public HalfConverter()
15+
{
16+
IsInternalConverterForNumberType = true;
17+
}
18+
19+
public override Half Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
20+
{
21+
if (reader.TokenType != JsonTokenType.Number)
22+
{
23+
ThrowHelper.ThrowInvalidOperationException_ExpectedNumber(reader.TokenType);
24+
}
25+
26+
return ReadCore(ref reader);
27+
}
28+
29+
public override void Write(Utf8JsonWriter writer, Half value, JsonSerializerOptions options)
30+
{
31+
WriteCore(writer, value);
32+
}
33+
34+
private static Half ReadCore(ref Utf8JsonReader reader)
35+
{
36+
Half result;
37+
38+
byte[]? rentedByteBuffer = null;
39+
int bufferLength = reader.ValueLength;
40+
41+
Span<byte> byteBuffer = bufferLength <= JsonConstants.StackallocByteThreshold
42+
? stackalloc byte[JsonConstants.StackallocByteThreshold]
43+
: (rentedByteBuffer = ArrayPool<byte>.Shared.Rent(bufferLength));
44+
45+
int written = reader.CopyValue(byteBuffer);
46+
byteBuffer = byteBuffer.Slice(0, written);
47+
48+
bool success = TryParse(byteBuffer, out result);
49+
if (rentedByteBuffer != null)
50+
{
51+
ArrayPool<byte>.Shared.Return(rentedByteBuffer);
52+
}
53+
54+
if (!success)
55+
{
56+
ThrowHelper.ThrowFormatException(NumericType.Half);
57+
}
58+
59+
Debug.Assert(!Half.IsNaN(result) && !Half.IsInfinity(result));
60+
return result;
61+
}
62+
63+
private static void WriteCore(Utf8JsonWriter writer, Half value)
64+
{
65+
#if NET8_0_OR_GREATER
66+
Span<byte> buffer = stackalloc byte[MaxFormatLength];
67+
#else
68+
Span<char> buffer = stackalloc char[MaxFormatLength];
69+
#endif
70+
Format(buffer, value, out int written);
71+
writer.WriteRawValue(buffer.Slice(0, written));
72+
}
73+
74+
internal override Half ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
75+
{
76+
Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
77+
return ReadCore(ref reader);
78+
}
79+
80+
internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, Half value, JsonSerializerOptions options, bool isWritingExtensionDataProperty)
81+
{
82+
#if NET8_0_OR_GREATER
83+
Span<byte> buffer = stackalloc byte[MaxFormatLength];
84+
#else
85+
Span<char> buffer = stackalloc char[MaxFormatLength];
86+
#endif
87+
Format(buffer, value, out int written);
88+
writer.WritePropertyName(buffer.Slice(0, written));
89+
}
90+
91+
internal override Half ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
92+
{
93+
if (reader.TokenType == JsonTokenType.String)
94+
{
95+
if ((JsonNumberHandling.AllowReadingFromString & handling) != 0)
96+
{
97+
if (TryGetFloatingPointConstant(ref reader, out Half value))
98+
{
99+
return value;
100+
}
101+
102+
return ReadCore(ref reader);
103+
}
104+
else if ((JsonNumberHandling.AllowNamedFloatingPointLiterals & handling) != 0)
105+
{
106+
if (!TryGetFloatingPointConstant(ref reader, out Half value))
107+
{
108+
ThrowHelper.ThrowFormatException(NumericType.Half);
109+
}
110+
111+
return value;
112+
}
113+
}
114+
115+
return Read(ref reader, Type, options);
116+
}
117+
118+
internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, Half value, JsonNumberHandling handling)
119+
{
120+
if ((JsonNumberHandling.WriteAsString & handling) != 0)
121+
{
122+
#if NET8_0_OR_GREATER
123+
const byte Quote = JsonConstants.Quote;
124+
Span<byte> buffer = stackalloc byte[MaxFormatLength + 2];
125+
#else
126+
const char Quote = (char)JsonConstants.Quote;
127+
Span<char> buffer = stackalloc char[MaxFormatLength + 2];
128+
#endif
129+
buffer[0] = Quote;
130+
Format(buffer.Slice(1), value, out int written);
131+
132+
int length = written + 2;
133+
buffer[length - 1] = Quote;
134+
writer.WriteRawValue(buffer.Slice(0, length));
135+
}
136+
else if ((JsonNumberHandling.AllowNamedFloatingPointLiterals & handling) != 0)
137+
{
138+
WriteFloatingPointConstant(writer, value);
139+
}
140+
else
141+
{
142+
WriteCore(writer, value);
143+
}
144+
}
145+
146+
private static bool TryGetFloatingPointConstant(ref Utf8JsonReader reader, out Half value)
147+
{
148+
Span<byte> buffer = stackalloc byte[MaxFormatLength];
149+
int written = reader.CopyValue(buffer);
150+
151+
return JsonReaderHelper.TryGetFloatingPointConstant(buffer.Slice(0, written), out value);
152+
}
153+
154+
private static void WriteFloatingPointConstant(Utf8JsonWriter writer, Half value)
155+
{
156+
if (Half.IsNaN(value))
157+
{
158+
writer.WriteNumberValueAsStringUnescaped(JsonConstants.NaNValue);
159+
}
160+
else if (Half.IsPositiveInfinity(value))
161+
{
162+
writer.WriteNumberValueAsStringUnescaped(JsonConstants.PositiveInfinityValue);
163+
}
164+
else if (Half.IsNegativeInfinity(value))
165+
{
166+
writer.WriteNumberValueAsStringUnescaped(JsonConstants.NegativeInfinityValue);
167+
}
168+
else
169+
{
170+
WriteCore(writer, value);
171+
}
172+
}
173+
174+
private static bool TryParse(ReadOnlySpan<byte> buffer, out Half result)
175+
{
176+
#if NET8_0_OR_GREATER
177+
bool success = Half.TryParse(buffer, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out result);
178+
#else
179+
// Half.TryFormat/TryParse(ROS<byte>) are not available on .NET 7
180+
// we need to use Half.TryFormat/TryParse(ROS<char>) in that case.
181+
char[]? rentedCharBuffer = null;
182+
183+
Span<char> charBuffer = buffer.Length <= JsonConstants.StackallocCharThreshold
184+
? stackalloc char[JsonConstants.StackallocCharThreshold]
185+
: (rentedCharBuffer = ArrayPool<char>.Shared.Rent(buffer.Length));
186+
187+
int written = JsonReaderHelper.TranscodeHelper(buffer, charBuffer);
188+
189+
bool success = Half.TryParse(charBuffer, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out result);
190+
191+
if (rentedCharBuffer != null)
192+
{
193+
ArrayPool<char>.Shared.Return(rentedCharBuffer);
194+
}
195+
#endif
196+
197+
// Half.TryParse is more lax with floating-point literals than other S.T.Json floating-point types
198+
// e.g: it parses "naN" successfully. Only succeed with the exact match.
199+
return success &&
200+
(!Half.IsNaN(result) || buffer.SequenceEqual(JsonConstants.NaNValue)) &&
201+
(!Half.IsPositiveInfinity(result) || buffer.SequenceEqual(JsonConstants.PositiveInfinityValue)) &&
202+
(!Half.IsNegativeInfinity(result) || buffer.SequenceEqual(JsonConstants.NegativeInfinityValue));
203+
}
204+
205+
private static void Format(
206+
#if NET8_0_OR_GREATER
207+
Span<byte> destination,
208+
#else
209+
Span<char> destination,
210+
#endif
211+
Half value, out int written)
212+
{
213+
bool formattedSuccessfully = value.TryFormat(destination, out written, provider: CultureInfo.InvariantCulture);
214+
Debug.Assert(formattedSuccessfully);
215+
}
216+
}
217+
}

0 commit comments

Comments
 (0)