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

Add Utf8JsonReader.AllowMultipleValues and related APIs. #104328

Merged
5 changes: 5 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 @@ -187,6 +187,7 @@ public partial struct JsonReaderOptions
{
private int _dummyPrimitive;
public bool AllowTrailingCommas { readonly get { throw null; } set { } }
public bool AllowMultipleValues { readonly get { throw null; } set { } }
public System.Text.Json.JsonCommentHandling CommentHandling { readonly get { throw null; } set { } }
public int MaxDepth { readonly get { throw null; } set { } }
}
Expand Down Expand Up @@ -247,7 +248,11 @@ public static partial class JsonSerializer
public static System.Threading.Tasks.ValueTask<object?> DeserializeAsync(System.IO.Stream utf8Json, System.Type returnType, System.Text.Json.Serialization.JsonSerializerContext context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
[System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")]
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")]
public static System.Collections.Generic.IAsyncEnumerable<TValue?> DeserializeAsyncEnumerable<TValue>(System.IO.Stream utf8Json, bool topLevelValues, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
[System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")]
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")]
public static System.Collections.Generic.IAsyncEnumerable<TValue?> DeserializeAsyncEnumerable<TValue>(System.IO.Stream utf8Json, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Collections.Generic.IAsyncEnumerable<TValue?> DeserializeAsyncEnumerable<TValue>(System.IO.Stream utf8Json, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TValue> jsonTypeInfo, bool topLevelValues, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Collections.Generic.IAsyncEnumerable<TValue?> DeserializeAsyncEnumerable<TValue>(System.IO.Stream utf8Json, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TValue> jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
[System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")]
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")]
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="System\Text\Json\Serialization\Converters\Collection\ImmutableEnumerableOfTConverterWithReflection.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Collection\ReadOnlyMemoryConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Collection\MemoryConverterFactory.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Collection\RootLevelListConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Collection\StackOrQueueConverterWithReflection.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\JsonMetadataServicesConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectWithParameterizedConstructorConverter.Large.Reflection.cs" />
Expand Down
87 changes: 58 additions & 29 deletions src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,43 +37,72 @@ public static bool TryAdvanceWithOptionalReadAhead(this scoped ref Utf8JsonReade
// No read-ahead necessary if we're at the final block of JSON data.
bool readAhead = requiresReadAhead && !reader.IsFinalBlock;
return readAhead ? TryAdvanceWithReadAhead(ref reader) : reader.Read();
}

// The read-ahead method is not inlined
static bool TryAdvanceWithReadAhead(scoped ref Utf8JsonReader reader)
/// <summary>
/// Attempts to read ahead to the next root-level JSON value, if it exists.
/// </summary>
public static bool TryAdvanceToNextRootLevelValueWithOptionalReadAhead(this scoped ref Utf8JsonReader reader, bool requiresReadAhead, out bool isAtEndOfStream)
{
Debug.Assert(reader.AllowMultipleValues, "only supported by readers that support multiple values.");
Debug.Assert(reader.CurrentDepth == 0, "should only invoked for top-level values.");

Utf8JsonReader checkpoint = reader;
if (!reader.Read())
{
// When we're reading ahead we always have to save the state
// as we don't know if the next token is a start object or array.
Utf8JsonReader restore = reader;
// If the reader didn't return any tokens and it's the final block,
// then there are no other JSON values to be read.
isAtEndOfStream = reader.IsFinalBlock;
reader = checkpoint;
return false;
}

if (!reader.Read())
{
return false;
}
// We found another JSON value, read ahead accordingly.
isAtEndOfStream = false;
if (requiresReadAhead && !reader.IsFinalBlock)
{
// Perform full read-ahead to ensure the full JSON value has been buffered.
reader = checkpoint;
return TryAdvanceWithReadAhead(ref reader);
}

// Perform the actual read-ahead.
JsonTokenType tokenType = reader.TokenType;
if (tokenType is JsonTokenType.StartObject or JsonTokenType.StartArray)
return true;
}

private static bool TryAdvanceWithReadAhead(scoped ref Utf8JsonReader reader)
{
// When we're reading ahead we always have to save the state
// as we don't know if the next token is a start object or array.
Utf8JsonReader restore = reader;

if (!reader.Read())
{
return false;
}

// Perform the actual read-ahead.
JsonTokenType tokenType = reader.TokenType;
if (tokenType is JsonTokenType.StartObject or JsonTokenType.StartArray)
{
// Attempt to skip to make sure we have all the data we need.
bool complete = reader.TrySkipPartial();

// We need to restore the state in all cases as we need to be positioned back before
// the current token to either attempt to skip again or to actually read the value.
reader = restore;

if (!complete)
{
// Attempt to skip to make sure we have all the data we need.
bool complete = reader.TrySkipPartial();

// We need to restore the state in all cases as we need to be positioned back before
// the current token to either attempt to skip again or to actually read the value.
reader = restore;

if (!complete)
{
// Couldn't read to the end of the object, exit out to get more data in the buffer.
return false;
}

// Success, requeue the reader to the start token.
reader.ReadWithVerify();
Debug.Assert(tokenType == reader.TokenType);
// Couldn't read to the end of the object, exit out to get more data in the buffer.
return false;
}

return true;
// Success, requeue the reader to the start token.
reader.ReadWithVerify();
Debug.Assert(tokenType == reader.TokenType);
}

return true;
}

#if !NET
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,14 @@ public int MaxDepth
/// By default, it's set to false, and <exception cref="JsonException"/> is thrown if a trailing comma is encountered.
/// </remarks>
public bool AllowTrailingCommas { get; set; }

/// <summary>
/// Defines whether the <see cref="Utf8JsonReader"/> should tolerate
/// zero or more top-level JSON values that are whitespace separated.
/// </summary>
/// <remarks>
/// By default, it's set to false, and <exception cref="JsonException"/> is thrown if trailing content is encountered.
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
/// </remarks>
public bool AllowMultipleValues { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -363,17 +363,13 @@ private bool ReadFirstTokenMultiSegment(byte first)
}
_tokenType = JsonTokenType.Number;
_consumed += numberOfBytes;
return true;
}
else if (!ConsumeValueMultiSegment(first))
{
return false;
}

if (_tokenType == JsonTokenType.StartObject || _tokenType == JsonTokenType.StartArray)
{
_isNotPrimitive = true;
}
_isNotPrimitive = _tokenType is JsonTokenType.StartObject or JsonTokenType.StartArray;
// Intentionally fall out of the if-block to return true
}
return true;
Expand Down Expand Up @@ -1580,6 +1576,11 @@ private ConsumeTokenResult ConsumeNextTokenMultiSegment(byte marker)

if (_bitStack.CurrentDepth == 0)
{
if (_readerOptions.AllowMultipleValues)
{
return ReadFirstTokenMultiSegment(marker) ? ConsumeTokenResult.Success : ConsumeTokenResult.NotEnoughDataRollBackState;
}

ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.ExpectedEndAfterSingleJson, marker);
}

Expand Down Expand Up @@ -1711,6 +1712,11 @@ private ConsumeTokenResult ConsumeNextTokenFromLastNonCommentTokenMultiSegment()

if (_bitStack.CurrentDepth == 0 && _tokenType != JsonTokenType.None)
{
if (_readerOptions.AllowMultipleValues)
{
return ReadFirstTokenMultiSegment(first) ? ConsumeTokenResult.Success : ConsumeTokenResult.NotEnoughDataRollBackState;
}

ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.ExpectedEndAfterSingleJson, first);
}

Expand Down Expand Up @@ -2064,6 +2070,11 @@ private ConsumeTokenResult ConsumeNextTokenUntilAfterAllCommentsAreSkippedMultiS
}
else if (_bitStack.CurrentDepth == 0)
{
if (_readerOptions.AllowMultipleValues)
{
return ReadFirstTokenMultiSegment(marker) ? ConsumeTokenResult.Success : ConsumeTokenResult.NotEnoughDataRollBackState;
}

ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.ExpectedEndAfterSingleJson, marker);
}
else if (marker == JsonConstants.ListSeparator)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace System.Text.Json
{
Expand Down Expand Up @@ -55,6 +54,8 @@ public ref partial struct Utf8JsonReader

internal readonly int ValueLength => HasValueSequence ? checked((int)ValueSequence.Length) : ValueSpan.Length;

internal readonly bool AllowMultipleValues => _readerOptions.AllowMultipleValues;

/// <summary>
/// Gets the value of the last processed token as a ReadOnlySpan&lt;byte&gt; slice
/// of the input payload. If the JSON is provided within a ReadOnlySequence&lt;byte&gt;
Expand Down Expand Up @@ -280,7 +281,7 @@ public bool Read()

if (!retVal)
{
if (_isFinalBlock && TokenType == JsonTokenType.None)
if (_isFinalBlock && TokenType is JsonTokenType.None && !_readerOptions.AllowMultipleValues)
{
ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.ExpectedJsonTokens);
}
Expand Down Expand Up @@ -929,7 +930,7 @@ private bool HasMoreData()
return false;
}

if (_tokenType != JsonTokenType.EndArray && _tokenType != JsonTokenType.EndObject)
if (_tokenType is not JsonTokenType.EndArray and not JsonTokenType.EndObject)
{
ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.InvalidEndOfJsonNonPrimitive);
}
Expand Down Expand Up @@ -991,17 +992,13 @@ private bool ReadFirstToken(byte first)
_tokenType = JsonTokenType.Number;
_consumed += numberOfBytes;
_bytePositionInLine += numberOfBytes;
return true;
}
else if (!ConsumeValue(first))
{
return false;
}

if (_tokenType == JsonTokenType.StartObject || _tokenType == JsonTokenType.StartArray)
{
_isNotPrimitive = true;
}
_isNotPrimitive = _tokenType is JsonTokenType.StartObject or JsonTokenType.StartArray;
// Intentionally fall out of the if-block to return true
}
return true;
Expand All @@ -1016,10 +1013,10 @@ private void SkipWhiteSpace()
byte val = localBuffer[_consumed];

// JSON RFC 8259 section 2 says only these 4 characters count, not all of the Unicode definitions of whitespace.
if (val != JsonConstants.Space &&
val != JsonConstants.CarriageReturn &&
val != JsonConstants.LineFeed &&
val != JsonConstants.Tab)
if (val is not JsonConstants.Space and
not JsonConstants.CarriageReturn and
not JsonConstants.LineFeed and
not JsonConstants.Tab)
{
break;
}
Expand Down Expand Up @@ -1747,6 +1744,11 @@ private ConsumeTokenResult ConsumeNextToken(byte marker)

if (_bitStack.CurrentDepth == 0)
{
if (_readerOptions.AllowMultipleValues)
{
return ReadFirstToken(marker) ? ConsumeTokenResult.Success : ConsumeTokenResult.NotEnoughDataRollBackState;
}

ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.ExpectedEndAfterSingleJson, marker);
}

Expand Down Expand Up @@ -1869,6 +1871,11 @@ private ConsumeTokenResult ConsumeNextTokenFromLastNonCommentToken()

if (_bitStack.CurrentDepth == 0 && _tokenType != JsonTokenType.None)
{
if (_readerOptions.AllowMultipleValues)
{
return ReadFirstToken(first) ? ConsumeTokenResult.Success : ConsumeTokenResult.NotEnoughDataRollBackState;
}

ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.ExpectedEndAfterSingleJson, first);
}

Expand Down Expand Up @@ -2033,7 +2040,7 @@ private ConsumeTokenResult ConsumeNextTokenFromLastNonCommentToken()
}
else
{
Debug.Assert(_tokenType == JsonTokenType.EndArray || _tokenType == JsonTokenType.EndObject);
Debug.Assert(_tokenType is JsonTokenType.EndArray or JsonTokenType.EndObject);
if (_inObject)
{
Debug.Assert(first != JsonConstants.CloseBrace);
Expand Down Expand Up @@ -2207,6 +2214,11 @@ private ConsumeTokenResult ConsumeNextTokenUntilAfterAllCommentsAreSkipped(byte
}
else if (_bitStack.CurrentDepth == 0)
{
if (_readerOptions.AllowMultipleValues)
{
return ReadFirstToken(marker) ? ConsumeTokenResult.Success : ConsumeTokenResult.NotEnoughDataRollBackState;
}

ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.ExpectedEndAfterSingleJson, marker);
}
else if (marker == JsonConstants.ListSeparator)
Expand Down
Loading
Loading