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

JSON: Add support for Int128, UInt128 and Half #88962

Merged
merged 9 commits into from
Jul 18, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ public KnownTypeSymbols(Compilation compilation)
public INamedTypeSymbol? TimeOnlyType => GetOrResolveType("System.TimeOnly", ref _TimeOnlyType);
private Option<INamedTypeSymbol?> _TimeOnlyType;

public INamedTypeSymbol? Int128Type => GetOrResolveType("System.Int128", ref _Int128Type);
private Option<INamedTypeSymbol?> _Int128Type;

public INamedTypeSymbol? UInt128Type => GetOrResolveType("System.UInt128", ref _UInt128Type);
private Option<INamedTypeSymbol?> _UInt128Type;

public INamedTypeSymbol? HalfType => GetOrResolveType("System.Half", ref _HalfType);
private Option<INamedTypeSymbol?> _HalfType;

public IArrayTypeSymbol? ByteArrayType => _ByteArrayType.HasValue
? _ByteArrayType.Value
: (_ByteArrayType = new(Compilation.CreateArrayTypeSymbol(Compilation.GetSpecialType(SpecialType.System_Byte), rank: 1))).Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,9 @@ private static HashSet<ITypeSymbol> CreateBuiltInSupportTypeSet(KnownTypeSymbols
AddTypeIfNotNull(knownSymbols.DateTimeOffsetType);
AddTypeIfNotNull(knownSymbols.DateOnlyType);
AddTypeIfNotNull(knownSymbols.TimeOnlyType);
AddTypeIfNotNull(knownSymbols.Int128Type);
AddTypeIfNotNull(knownSymbols.UInt128Type);
AddTypeIfNotNull(knownSymbols.HalfType);
AddTypeIfNotNull(knownSymbols.GuidType);
AddTypeIfNotNull(knownSymbols.UriType);
AddTypeIfNotNull(knownSymbols.VersionType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ namespace System.Text.Json.Serialization.Metadata
public static partial class JsonMetadataServices
{
public static System.Text.Json.Serialization.JsonConverter<System.DateOnly> DateOnlyConverter { get { throw null; } }
public static System.Text.Json.Serialization.JsonConverter<System.Half> HalfConverter { get { throw null; } }
public static System.Text.Json.Serialization.JsonConverter<System.TimeOnly> TimeOnlyConverter { get { throw null; } }

#if NET7_0_OR_GREATER
public static System.Text.Json.Serialization.JsonConverter<System.Int128> Int128Converter { get { throw null; } }
[System.CLSCompliantAttribute(false)]
public static System.Text.Json.Serialization.JsonConverter<System.UInt128> UInt128Converter { get { throw null; } }
#endif
}
}
9 changes: 9 additions & 0 deletions src/libraries/System.Text.Json/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -690,4 +690,13 @@
<data name="ObjectCreationHandlingPropertyCannotAllowReferenceHandling" xml:space="preserve">
<value>JsonObjectCreationHandling.Populate is incompatible with reference handling.</value>
</data>
<data name="FormatInt128" xml:space="preserve">
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
<value>Either the JSON value is not in a supported format, or is out of bounds for an Int128.</value>
</data>
<data name="FormatUInt128" xml:space="preserve">
<value>Either the JSON value is not in a supported format, or is out of bounds for an UInt128.</value>
</data>
<data name="FormatHalf" xml:space="preserve">
<value>Either the JSON value is not in a supported format, or is out of bounds for a Half.</value>
</data>
</root>
6 changes: 6 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 @@ -345,11 +345,17 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="$(CoreLibSharedDir)System\Diagnostics\CodeAnalysis\UnconditionalSuppressMessageAttribute.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' and $([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '7.0'))">
<Compile Include="System\Text\Json\Serialization\Converters\Value\Int128Converter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\UInt128Converter.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
<Compile Include="System.Text.Json.Typeforwards.netcoreapp.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializerOptionsUpdateHandler.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\DateOnlyConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\TimeOnlyConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\HalfConverter.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,39 @@ public static bool TryGetEscapedGuid(ReadOnlySpan<byte> source, out Guid value)
return false;
}

#if NETCOREAPP
public static bool TryGetFloatingPointConstant(ReadOnlySpan<byte> span, out Half value)
{
if (span.Length == 3)
{
if (span.SequenceEqual(JsonConstants.NaNValue))
{
value = Half.NaN;
return true;
}
}
else if (span.Length == 8)
{
if (span.SequenceEqual(JsonConstants.PositiveInfinityValue))
{
value = Half.PositiveInfinity;
return true;
}
}
else if (span.Length == 9)
{
if (span.SequenceEqual(JsonConstants.NegativeInfinityValue))
{
value = Half.NegativeInfinity;
return true;
}
}

value = default;
return false;
}
#endif

public static bool TryGetFloatingPointConstant(ReadOnlySpan<byte> span, out float value)
{
if (span.Length == 3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ namespace System.Text.Json
ThrowHelper.ThrowInvalidOperationException_ExpectedString(_tokenType);
}

return CopyValue(utf8Destination);
}

internal readonly int CopyValue(Span<byte> utf8Destination)
{
Debug.Assert(_tokenType is JsonTokenType.String or JsonTokenType.PropertyName or JsonTokenType.Number);
Debug.Assert(_tokenType != JsonTokenType.Number || !ValueIsEscaped, "Numbers can't contain escape characters.");

int bytesWritten;

if (ValueIsEscaped)
Expand Down Expand Up @@ -129,6 +137,14 @@ namespace System.Text.Json
ThrowHelper.ThrowInvalidOperationException_ExpectedString(_tokenType);
}

return CopyValue(destination);
}

internal readonly int CopyValue(Span<char> destination)
{
Debug.Assert(_tokenType is JsonTokenType.String or JsonTokenType.PropertyName or JsonTokenType.Number);
Debug.Assert(_tokenType != JsonTokenType.Number || !ValueIsEscaped, "Numbers can't contain escape characters.");

scoped ReadOnlySpan<byte> unescapedSource;
byte[]? rentedBuffer = null;
int valueLength;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ internal sealed class CharConverter : JsonPrimitiveConverter<char>

public override char Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType is not (JsonTokenType.String or JsonTokenType.PropertyName))
{
ThrowHelper.ThrowInvalidOperationException_ExpectedString(reader.TokenType);
}

if (!JsonHelpers.IsInRangeInclusive(reader.ValueLength, 1, MaxEscapedCharacterLength))
{
ThrowHelper.ThrowInvalidOperationException_ExpectedChar(reader.TokenType);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Diagnostics;
using System.Globalization;

namespace System.Text.Json.Serialization.Converters
{
internal sealed class HalfConverter : JsonPrimitiveConverter<Half>
{
private const int MaxFormatLength = 20;

public HalfConverter()
{
IsInternalConverterForNumberType = true;
}

public override Half Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.Number)
{
ThrowHelper.ThrowInvalidOperationException_ExpectedNumber(reader.TokenType);
}

return ReadCore(ref reader);
}

public override void Write(Utf8JsonWriter writer, Half value, JsonSerializerOptions options)
{
WriteCore(writer, value);
}

private static Half ReadCore(ref Utf8JsonReader reader)
{
Half result;

byte[]? rentedByteBuffer = null;
int bufferLength = reader.ValueLength;

Span<byte> byteBuffer = bufferLength <= JsonConstants.StackallocByteThreshold
? stackalloc byte[JsonConstants.StackallocByteThreshold]
: (rentedByteBuffer = ArrayPool<byte>.Shared.Rent(bufferLength));

int written = reader.CopyValue(byteBuffer);
byteBuffer = byteBuffer.Slice(0, written);

bool success = TryParse(byteBuffer, out result);
if (rentedByteBuffer != null)
{
ArrayPool<byte>.Shared.Return(rentedByteBuffer);
}

if (!success)
{
ThrowHelper.ThrowFormatException(NumericType.Half);
}

Debug.Assert(!Half.IsNaN(result) && !Half.IsInfinity(result));
return result;
}

private static void WriteCore(Utf8JsonWriter writer, Half value)
{
#if NET8_0_OR_GREATER
Span<byte> buffer = stackalloc byte[MaxFormatLength];
#else
Span<char> buffer = stackalloc char[MaxFormatLength];
#endif
Format(buffer, value, out int written);
writer.WriteRawValue(buffer.Slice(0, written));
}

internal override Half ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
return ReadCore(ref reader);
}

internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, Half value, JsonSerializerOptions options, bool isWritingExtensionDataProperty)
{
#if NET8_0_OR_GREATER
Span<byte> buffer = stackalloc byte[MaxFormatLength];
#else
Span<char> buffer = stackalloc char[MaxFormatLength];
#endif
Format(buffer, value, out int written);
writer.WritePropertyName(buffer.Slice(0, written));
}

internal override Half ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
if ((JsonNumberHandling.AllowReadingFromString & handling) != 0)
{
if (TryGetFloatingPointConstant(ref reader, out Half value))
{
return value;
}

return ReadCore(ref reader);
}
else if ((JsonNumberHandling.AllowNamedFloatingPointLiterals & handling) != 0)
{
if (!TryGetFloatingPointConstant(ref reader, out Half value))
{
ThrowHelper.ThrowFormatException(NumericType.Half);
}

return value;
}
}

return Read(ref reader, Type, options);
}

internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, Half value, JsonNumberHandling handling)
{
if ((JsonNumberHandling.WriteAsString & handling) != 0)
{
#if NET8_0_OR_GREATER
const byte Quote = JsonConstants.Quote;
Span<byte> buffer = stackalloc byte[MaxFormatLength + 2];
#else
const char Quote = (char)JsonConstants.Quote;
Span<char> buffer = stackalloc char[MaxFormatLength + 2];
#endif
buffer[0] = Quote;
Format(buffer.Slice(1), value, out int written);

int length = written + 2;
buffer[length - 1] = Quote;
writer.WriteRawValue(buffer.Slice(0, length));
}
else if ((JsonNumberHandling.AllowNamedFloatingPointLiterals & handling) != 0)
{
WriteFloatingPointConstant(writer, value);
}
else
{
WriteCore(writer, value);
}
}

private static bool TryGetFloatingPointConstant(ref Utf8JsonReader reader, out Half value)
{
Span<byte> buffer = stackalloc byte[MaxFormatLength];
int written = reader.CopyValue(buffer);

return JsonReaderHelper.TryGetFloatingPointConstant(buffer.Slice(0, written), out value);
}

private static void WriteFloatingPointConstant(Utf8JsonWriter writer, Half value)
{
if (Half.IsNaN(value))
{
writer.WriteNumberValueAsStringUnescaped(JsonConstants.NaNValue);
}
else if (Half.IsPositiveInfinity(value))
{
writer.WriteNumberValueAsStringUnescaped(JsonConstants.PositiveInfinityValue);
}
else if (Half.IsNegativeInfinity(value))
{
writer.WriteNumberValueAsStringUnescaped(JsonConstants.NegativeInfinityValue);
}
else
{
WriteCore(writer, value);
}
}

private static bool TryParse(ReadOnlySpan<byte> buffer, out Half result)
{
#if NET8_0_OR_GREATER
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
bool success = Half.TryParse(buffer, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out result);
#else
// Half.TryFormat/TryParse(ROS<byte>) are not available on .NET 7
// we need to use Half.TryFormat/TryParse(ROS<char>) in that case.
char[]? rentedCharBuffer = null;

Span<char> charBuffer = buffer.Length <= JsonConstants.StackallocCharThreshold
? stackalloc char[JsonConstants.StackallocCharThreshold]
: (rentedCharBuffer = ArrayPool<char>.Shared.Rent(buffer.Length));

int written = JsonReaderHelper.TranscodeHelper(buffer, charBuffer);

bool success = Half.TryParse(charBuffer, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out result);

if (rentedCharBuffer != null)
{
ArrayPool<char>.Shared.Return(rentedCharBuffer);
}
#endif

// Half.TryParse is more lax with floating-point literals than other S.T.Json floating-point types
// e.g: it parses "naN" successfully. Only succeed with the exact match.
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
return success &&
(!Half.IsNaN(result) || buffer.SequenceEqual(JsonConstants.NaNValue)) &&
(!Half.IsPositiveInfinity(result) || buffer.SequenceEqual(JsonConstants.PositiveInfinityValue)) &&
(!Half.IsNegativeInfinity(result) || buffer.SequenceEqual(JsonConstants.NegativeInfinityValue));
}

private static void Format(
#if NET8_0_OR_GREATER
Span<byte> destination,
#else
Span<char> destination,
#endif
Half value, out int written)
{
bool formattedSuccessfully = value.TryFormat(destination, out written, provider: CultureInfo.InvariantCulture);
Debug.Assert(formattedSuccessfully);
}
}
}
Loading
Loading