-
Notifications
You must be signed in to change notification settings - Fork 5.2k
New JsonValue derived class for JSON primitives #116798
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
Merged
PranavSenthilnathan
merged 15 commits into
dotnet:main
from
PranavSenthilnathan:jsonvalue-conversion
Jul 22, 2025
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
8aea186
New JsonValue subclass for JSON primitives
PranavSenthilnathan 30ca4ab
Add tests for trailing trivia
PranavSenthilnathan b11c3d0
Revert "Add tests for trailing trivia"
PranavSenthilnathan 7b38057
WIP
PranavSenthilnathan ea84555
Merge branch 'main' of https://github.com/dotnet/runtime into jsonval…
PranavSenthilnathan 6f52640
Remove conversion restrictions from JsonValue
PranavSenthilnathan e59556e
Merge branch 'main' of https://github.com/dotnet/runtime into jsonval…
PranavSenthilnathan d331b24
TryGetValue/GetValue consistency
PranavSenthilnathan 5d49550
Improve coverage of new code
PranavSenthilnathan 99da5c9
Revert to hardcoded conversion targets
PranavSenthilnathan b2b84f7
Store unescaped
PranavSenthilnathan 41bd85c
Store unescaped string
PranavSenthilnathan 75c3a94
fix bound check
PranavSenthilnathan f2fa47e
PR feedback
PranavSenthilnathan 5c91f21
Merge branch 'main' of https://github.com/dotnet/runtime into jsonval…
PranavSenthilnathan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
318 changes: 318 additions & 0 deletions
318
src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,318 @@ | ||
// 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.Buffers.Text; | ||
using System.Diagnostics; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Text.Encodings.Web; | ||
|
||
namespace System.Text.Json.Nodes | ||
{ | ||
internal static class JsonValueOfJsonPrimitive | ||
{ | ||
internal static JsonValue CreatePrimitiveValue(ref Utf8JsonReader reader, JsonNodeOptions options) | ||
{ | ||
switch (reader.TokenType) | ||
{ | ||
case JsonTokenType.False: | ||
case JsonTokenType.True: | ||
return new JsonValueOfJsonBool(reader.GetBoolean(), options); | ||
case JsonTokenType.String: | ||
byte[] buffer = new byte[reader.ValueLength]; | ||
ReadOnlyMemory<byte> utf8String = buffer.AsMemory(0, reader.CopyString(buffer)); | ||
return new JsonValueOfJsonString(utf8String, options); | ||
case JsonTokenType.Number: | ||
byte[] numberValue = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray(); | ||
return new JsonValueOfJsonNumber(numberValue, options); | ||
default: | ||
Debug.Fail("Only primitives allowed."); | ||
ThrowHelper.ThrowJsonException(); | ||
return null!; // Unreachable, but required for compilation. | ||
} | ||
} | ||
|
||
private sealed class JsonValueOfJsonString : JsonValue | ||
{ | ||
private readonly ReadOnlyMemory<byte> _value; | ||
|
||
internal JsonValueOfJsonString(ReadOnlyMemory<byte> utf8String, JsonNodeOptions? options) | ||
: base(options) | ||
{ | ||
_value = utf8String; | ||
} | ||
|
||
internal override JsonNode DeepCloneCore() => new JsonValueOfJsonString(_value, Options); | ||
private protected override JsonValueKind GetValueKindCore() => JsonValueKind.String; | ||
|
||
public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null) | ||
{ | ||
ArgumentNullException.ThrowIfNull(writer); | ||
|
||
writer.WriteStringValue(_value.Span); | ||
} | ||
|
||
public override T GetValue<T>() | ||
{ | ||
if (!TryGetValue(out T? value)) | ||
{ | ||
ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvertElement(JsonValueKind.String, typeof(T)); | ||
} | ||
|
||
return value; | ||
} | ||
|
||
public override bool TryGetValue<T>([NotNullWhen(true)] out T? value) | ||
where T : default | ||
{ | ||
if (typeof(T) == typeof(JsonElement)) | ||
{ | ||
value = (T)(object)JsonWriterHelper.WriteString(_value.Span, static serialized => JsonElement.Parse(serialized)); | ||
return true; | ||
} | ||
|
||
if (typeof(T) == typeof(string)) | ||
{ | ||
string? result = JsonReaderHelper.TranscodeHelper(_value.Span); | ||
|
||
Debug.Assert(result != null); | ||
value = (T)(object)result; | ||
return true; | ||
} | ||
|
||
bool success; | ||
|
||
if (typeof(T) == typeof(DateTime) || typeof(T) == typeof(DateTime?)) | ||
{ | ||
success = JsonReaderHelper.TryGetValue(_value.Span, isEscaped: false, out DateTime result); | ||
value = (T)(object)result; | ||
return success; | ||
} | ||
|
||
if (typeof(T) == typeof(DateTimeOffset) || typeof(T) == typeof(DateTimeOffset?)) | ||
{ | ||
success = JsonReaderHelper.TryGetValue(_value.Span, isEscaped: false, out DateTimeOffset result); | ||
value = (T)(object)result; | ||
return success; | ||
} | ||
|
||
if (typeof(T) == typeof(Guid) || typeof(T) == typeof(Guid?)) | ||
{ | ||
success = JsonReaderHelper.TryGetValue(_value.Span, isEscaped: false, out Guid result); | ||
value = (T)(object)result; | ||
return success; | ||
} | ||
|
||
if (typeof(T) == typeof(char) || typeof(T) == typeof(char?)) | ||
{ | ||
string? result = JsonReaderHelper.TranscodeHelper(_value.Span); | ||
|
||
Debug.Assert(result != null); | ||
if (result.Length == 1) | ||
{ | ||
value = (T)(object)result[0]; | ||
return true; | ||
} | ||
} | ||
|
||
value = default!; | ||
return false; | ||
} | ||
} | ||
|
||
private sealed class JsonValueOfJsonBool : JsonValue | ||
{ | ||
private readonly bool _value; | ||
|
||
private JsonValueKind ValueKind => _value ? JsonValueKind.True : JsonValueKind.False; | ||
|
||
internal JsonValueOfJsonBool(bool value, JsonNodeOptions? options) | ||
: base(options) | ||
{ | ||
_value = value; | ||
} | ||
|
||
public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null) => writer.WriteBooleanValue(_value); | ||
internal override JsonNode DeepCloneCore() => new JsonValueOfJsonBool(_value, Options); | ||
private protected override JsonValueKind GetValueKindCore() => ValueKind; | ||
|
||
public override T GetValue<T>() | ||
{ | ||
if (!TryGetValue(out T? value)) | ||
{ | ||
ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvertElement(_value ? JsonValueKind.True : JsonValueKind.False, typeof(T)); | ||
} | ||
|
||
return value; | ||
} | ||
|
||
public override bool TryGetValue<T>([NotNullWhen(true)] out T? value) | ||
where T : default | ||
{ | ||
if (typeof(T) == typeof(JsonElement)) | ||
{ | ||
value = (T)(object)JsonElement.Parse(_value ? JsonConstants.TrueValue : JsonConstants.FalseValue); | ||
return true; | ||
} | ||
|
||
if (typeof(T) == typeof(bool) || typeof(T) == typeof(bool?)) | ||
{ | ||
value = (T)(object)_value; | ||
return true; | ||
} | ||
|
||
value = default!; | ||
return false; | ||
} | ||
} | ||
|
||
private sealed class JsonValueOfJsonNumber : JsonValue | ||
{ | ||
// This can be optimized to store the decimal point position and the exponent so that | ||
// conversion to different numeric types can be done without parsing the string again. | ||
// Utf8Parser uses an internal ref struct, Number.NumberBuffer, which is really the | ||
// same functionality that we would want here. | ||
private readonly byte[] _value; | ||
|
||
internal JsonValueOfJsonNumber(byte[] number, JsonNodeOptions? options) | ||
: base(options) | ||
{ | ||
_value = number; | ||
} | ||
|
||
internal override JsonNode DeepCloneCore() => new JsonValueOfJsonNumber(_value, Options); | ||
private protected override JsonValueKind GetValueKindCore() => JsonValueKind.Number; | ||
|
||
public override T GetValue<T>() | ||
{ | ||
if (!TryGetValue(out T? value)) | ||
{ | ||
ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvertElement(JsonValueKind.Number, typeof(T)); | ||
} | ||
|
||
return value; | ||
} | ||
|
||
public override bool TryGetValue<T>([NotNullWhen(true)] out T? value) | ||
where T : default | ||
{ | ||
if (typeof(T) == typeof(JsonElement)) | ||
{ | ||
value = (T)(object)JsonElement.Parse(_value); | ||
return true; | ||
} | ||
|
||
bool success; | ||
|
||
if (typeof(T) == typeof(int) || typeof(T) == typeof(int?)) | ||
{ | ||
success = Utf8Parser.TryParse(_value, out int result, out int consumed) && | ||
consumed == _value.Length; | ||
|
||
value = (T)(object)result; | ||
return success; | ||
} | ||
|
||
if (typeof(T) == typeof(long) || typeof(T) == typeof(long?)) | ||
{ | ||
success = Utf8Parser.TryParse(_value, out long result, out int consumed) && | ||
consumed == _value.Length; | ||
|
||
value = (T)(object)result; | ||
return success; | ||
} | ||
|
||
if (typeof(T) == typeof(double) || typeof(T) == typeof(double?)) | ||
{ | ||
success = Utf8Parser.TryParse(_value, out double result, out int consumed) && | ||
consumed == _value.Length; | ||
|
||
value = (T)(object)result; | ||
return success; | ||
} | ||
|
||
if (typeof(T) == typeof(short) || typeof(T) == typeof(short?)) | ||
{ | ||
success = Utf8Parser.TryParse(_value, out short result, out int consumed) && | ||
consumed == _value.Length; | ||
|
||
value = (T)(object)result; | ||
return success; | ||
} | ||
|
||
if (typeof(T) == typeof(decimal) || typeof(T) == typeof(decimal?)) | ||
{ | ||
success = Utf8Parser.TryParse(_value, out decimal result, out int consumed) && | ||
consumed == _value.Length; | ||
|
||
value = (T)(object)result; | ||
return success; | ||
} | ||
|
||
if (typeof(T) == typeof(byte) || typeof(T) == typeof(byte?)) | ||
{ | ||
success = Utf8Parser.TryParse(_value, out byte result, out int consumed) && | ||
consumed == _value.Length; | ||
|
||
value = (T)(object)result; | ||
return success; | ||
} | ||
|
||
if (typeof(T) == typeof(float) || typeof(T) == typeof(float?)) | ||
{ | ||
success = Utf8Parser.TryParse(_value, out float result, out int consumed) && | ||
consumed == _value.Length; | ||
|
||
value = (T)(object)result; | ||
return success; | ||
} | ||
|
||
if (typeof(T) == typeof(uint) || typeof(T) == typeof(uint?)) | ||
{ | ||
success = Utf8Parser.TryParse(_value, out uint result, out int consumed) && | ||
consumed == _value.Length; | ||
|
||
value = (T)(object)result; | ||
return success; | ||
} | ||
|
||
if (typeof(T) == typeof(ushort) || typeof(T) == typeof(ushort?)) | ||
{ | ||
success = Utf8Parser.TryParse(_value, out ushort result, out int consumed) && | ||
consumed == _value.Length; | ||
|
||
value = (T)(object)result; | ||
return success; | ||
} | ||
|
||
if (typeof(T) == typeof(ulong) || typeof(T) == typeof(ulong?)) | ||
{ | ||
success = Utf8Parser.TryParse(_value, out ulong result, out int consumed) && | ||
consumed == _value.Length; | ||
|
||
value = (T)(object)result; | ||
return success; | ||
} | ||
|
||
if (typeof(T) == typeof(sbyte) || typeof(T) == typeof(sbyte?)) | ||
{ | ||
success = Utf8Parser.TryParse(_value, out sbyte result, out int consumed) && | ||
consumed == _value.Length; | ||
|
||
value = (T)(object)result; | ||
return success; | ||
} | ||
|
||
value = default!; | ||
return false; | ||
} | ||
|
||
public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null) | ||
{ | ||
ArgumentNullException.ThrowIfNull(writer); | ||
|
||
writer.WriteNumberValue(_value); | ||
} | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.