Skip to content

Commit 59fb3be

Browse files
Ensure that JSON properties can be skipped without buffering their entire contents. (#96856)
1 parent 6508018 commit 59fb3be

18 files changed

Lines changed: 436 additions & 320 deletions

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,6 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
236236
<Compile Include="System\Text\Json\Serialization\IgnoreReferenceHandler.cs" />
237237
<Compile Include="System\Text\Json\Serialization\JsonConverter.cs" />
238238
<Compile Include="System\Text\Json\Serialization\JsonConverter.MetadataHandling.cs" />
239-
<Compile Include="System\Text\Json\Serialization\JsonConverter.ReadAhead.cs" />
240239
<Compile Include="System\Text\Json\Serialization\JsonConverterFactory.cs" />
241240
<Compile Include="System\Text\Json\Serialization\JsonConverterOfT.ReadCore.cs" />
242241
<Compile Include="System\Text\Json\Serialization\JsonConverterOfT.WriteCore.cs" />
@@ -393,10 +392,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
393392
</ItemGroup>
394393

395394
<ItemGroup>
396-
<ProjectReference Include="$(LibrariesProjectRoot)System.Text.RegularExpressions\gen\System.Text.RegularExpressions.Generator.csproj"
397-
ReferenceOutputAssembly="false"
398-
SetTargetFramework="TargetFramework=netstandard2.0"
399-
OutputItemType="Analyzer" />
395+
<ProjectReference Include="$(LibrariesProjectRoot)System.Text.RegularExpressions\gen\System.Text.RegularExpressions.Generator.csproj" ReferenceOutputAssembly="false" SetTargetFramework="TargetFramework=netstandard2.0" OutputItemType="Analyzer" />
400396
</ItemGroup>
401397

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

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,59 @@ public static ReadOnlySpan<byte> GetSpan(this scoped ref Utf8JsonReader reader)
2121
return reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
2222
}
2323

24+
/// <summary>
25+
/// Attempts to perform a Read() operation and optionally checks that the full JSON value has been buffered.
26+
/// The reader will be reset if the operation fails.
27+
/// </summary>
28+
/// <param name="reader">The reader to advance.</param>
29+
/// <param name="requiresReadAhead">If reading a partial payload, read ahead to ensure that the full JSON value has been buffered.</param>
30+
/// <returns>True if the the reader has been buffered with all required data.</returns>
31+
// AggressiveInlining used since this method is on a hot path and short. The AdvanceWithReadAhead method should not be inlined.
32+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
33+
public static bool TryAdvanceWithOptionalReadAhead(this scoped ref Utf8JsonReader reader, bool requiresReadAhead)
34+
{
35+
// No read-ahead necessary if we're at the final block of JSON data.
36+
bool readAhead = requiresReadAhead && !reader.IsFinalBlock;
37+
return readAhead ? TryAdvanceWithReadAhead(ref reader) : reader.Read();
38+
39+
// The read-ahead method is not inlined
40+
static bool TryAdvanceWithReadAhead(scoped ref Utf8JsonReader reader)
41+
{
42+
// When we're reading ahead we always have to save the state
43+
// as we don't know if the next token is a start object or array.
44+
Utf8JsonReader restore = reader;
45+
46+
if (!reader.Read())
47+
{
48+
return false;
49+
}
50+
51+
// Perform the actual read-ahead.
52+
JsonTokenType tokenType = reader.TokenType;
53+
if (tokenType is JsonTokenType.StartObject or JsonTokenType.StartArray)
54+
{
55+
// Attempt to skip to make sure we have all the data we need.
56+
bool complete = reader.TrySkipPartial(targetDepth: reader.CurrentDepth);
57+
58+
// We need to restore the state in all cases as we need to be positioned back before
59+
// the current token to either attempt to skip again or to actually read the value.
60+
reader = restore;
61+
62+
if (!complete)
63+
{
64+
// Couldn't read to the end of the object, exit out to get more data in the buffer.
65+
return false;
66+
}
67+
68+
// Success, requeue the reader to the start token.
69+
reader.ReadWithVerify();
70+
Debug.Assert(tokenType == reader.TokenType);
71+
}
72+
73+
return true;
74+
}
75+
}
76+
2477
#if !NETCOREAPP
2578
/// <summary>
2679
/// Returns <see langword="true"/> if <paramref name="value"/> is a valid Unicode scalar

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

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public readonly int CurrentDepth
106106
get
107107
{
108108
int readerDepth = _bitStack.CurrentDepth;
109-
if (TokenType == JsonTokenType.StartArray || TokenType == JsonTokenType.StartObject)
109+
if (TokenType is JsonTokenType.StartArray or JsonTokenType.StartObject)
110110
{
111111
Debug.Assert(readerDepth >= 1);
112112
readerDepth--;
@@ -115,7 +115,7 @@ public readonly int CurrentDepth
115115
}
116116
}
117117

118-
internal bool IsInArray => !_inObject;
118+
internal readonly bool IsInArray => !_inObject;
119119

120120
/// <summary>
121121
/// Gets the type of the last processed JSON token in the UTF-8 encoded JSON text.
@@ -322,15 +322,15 @@ private void SkipHelper()
322322
{
323323
Debug.Assert(_isFinalBlock);
324324

325-
if (TokenType == JsonTokenType.PropertyName)
325+
if (TokenType is JsonTokenType.PropertyName)
326326
{
327327
bool result = Read();
328328
// Since _isFinalBlock == true here, and the JSON token is not a primitive value or comment.
329329
// Read() is guaranteed to return true OR throw for invalid/incomplete data.
330330
Debug.Assert(result);
331331
}
332332

333-
if (TokenType == JsonTokenType.StartObject || TokenType == JsonTokenType.StartArray)
333+
if (TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray)
334334
{
335335
int depth = CurrentDepth;
336336
do
@@ -375,41 +375,58 @@ public bool TrySkip()
375375
return true;
376376
}
377377

378-
return TrySkipHelper();
378+
Utf8JsonReader restore = this;
379+
bool success = TrySkipPartial(targetDepth: CurrentDepth);
380+
if (!success)
381+
{
382+
// Roll back the reader if it contains partial data.
383+
this = restore;
384+
}
385+
386+
return success;
379387
}
380388

381-
private bool TrySkipHelper()
389+
/// <summary>
390+
/// Tries to skip the children of the current JSON token, advancing the reader even if there is not enough data.
391+
/// The skip operation can be resumed later, provided that the same <paramref name="targetDepth" /> is passed.
392+
/// </summary>
393+
/// <param name="targetDepth">The target depth we want to eventually skip to.</param>
394+
/// <returns>True if the entire JSON value has been skipped.</returns>
395+
internal bool TrySkipPartial(int targetDepth)
382396
{
383-
Debug.Assert(!_isFinalBlock);
384-
385-
Utf8JsonReader restore = this;
397+
Debug.Assert(0 <= targetDepth && targetDepth <= CurrentDepth);
386398

387-
if (TokenType == JsonTokenType.PropertyName)
399+
if (targetDepth == CurrentDepth)
388400
{
389-
if (!Read())
401+
// This is the first call to TrySkipHelper.
402+
if (TokenType is JsonTokenType.PropertyName)
390403
{
391-
goto Restore;
404+
// Skip any property name tokens preceding the value.
405+
if (!Read())
406+
{
407+
return false;
408+
}
409+
}
410+
411+
if (TokenType is not (JsonTokenType.StartObject or JsonTokenType.StartArray))
412+
{
413+
// The next value is not an object or array, so there is nothing to skip.
414+
return true;
392415
}
393416
}
394417

395-
if (TokenType == JsonTokenType.StartObject || TokenType == JsonTokenType.StartArray)
418+
// Start or resume iterating through the JSON object or array.
419+
do
396420
{
397-
int depth = CurrentDepth;
398-
do
421+
if (!Read())
399422
{
400-
if (!Read())
401-
{
402-
goto Restore;
403-
}
423+
return false;
404424
}
405-
while (depth < CurrentDepth);
406425
}
426+
while (targetDepth < CurrentDepth);
407427

428+
Debug.Assert(targetDepth == CurrentDepth);
408429
return true;
409-
410-
Restore:
411-
this = restore;
412-
return false;
413430
}
414431

415432
/// <summary>

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,13 @@ internal override bool OnTryRead(
206206
{
207207
if (state.Current.PropertyState < StackFramePropertyState.ReadValue)
208208
{
209-
state.Current.PropertyState = StackFramePropertyState.ReadValue;
210-
211-
if (!SingleValueReadWithReadAhead(elementConverter.RequiresReadAhead, ref reader, ref state))
209+
if (!reader.TryAdvanceWithOptionalReadAhead(elementConverter.RequiresReadAhead))
212210
{
213211
value = default;
214212
return false;
215213
}
214+
215+
state.Current.PropertyState = StackFramePropertyState.ReadValue;
216216
}
217217

218218
if (state.Current.PropertyState < StackFramePropertyState.ReadValueIsEnd)
@@ -246,8 +246,6 @@ internal override bool OnTryRead(
246246

247247
if (state.Current.ObjectState < StackFrameObjectState.EndToken)
248248
{
249-
state.Current.ObjectState = StackFrameObjectState.EndToken;
250-
251249
// Array payload is nested inside a $values metadata property.
252250
if ((state.Current.MetadataPropertyNames & MetadataPropertyName.Values) != 0)
253251
{
@@ -257,6 +255,8 @@ internal override bool OnTryRead(
257255
return false;
258256
}
259257
}
258+
259+
state.Current.ObjectState = StackFrameObjectState.EndToken;
260260
}
261261

262262
if (state.Current.ObjectState < StackFrameObjectState.EndTokenValidation)

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,14 +230,14 @@ internal sealed override bool OnTryRead(
230230
{
231231
if (state.Current.PropertyState == StackFramePropertyState.None)
232232
{
233-
state.Current.PropertyState = StackFramePropertyState.ReadName;
234-
235233
// Read the key name.
236234
if (!reader.Read())
237235
{
238236
value = default;
239237
return false;
240238
}
239+
240+
state.Current.PropertyState = StackFramePropertyState.ReadName;
241241
}
242242

243243
// Determine the property.
@@ -274,14 +274,14 @@ internal sealed override bool OnTryRead(
274274

275275
if (state.Current.PropertyState < StackFramePropertyState.ReadValue)
276276
{
277-
state.Current.PropertyState = StackFramePropertyState.ReadValue;
278-
279-
if (!SingleValueReadWithReadAhead(_valueConverter.RequiresReadAhead, ref reader, ref state))
277+
if (!reader.TryAdvanceWithOptionalReadAhead(_valueConverter.RequiresReadAhead))
280278
{
281279
state.Current.DictionaryKey = key;
282280
value = default;
283281
return false;
284282
}
283+
284+
state.Current.PropertyState = StackFramePropertyState.ReadValue;
285285
}
286286

287287
if (state.Current.PropertyState < StackFramePropertyState.TryRead)

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -148,23 +148,20 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
148148
// Determine the property.
149149
if (state.Current.PropertyState == StackFramePropertyState.None)
150150
{
151-
state.Current.PropertyState = StackFramePropertyState.ReadName;
152-
153151
if (!reader.Read())
154152
{
155-
// The read-ahead functionality will do the Read().
156153
state.Current.ReturnValue = obj;
157154
value = default;
158155
return false;
159156
}
157+
158+
state.Current.PropertyState = StackFramePropertyState.ReadName;
160159
}
161160

162161
JsonPropertyInfo jsonPropertyInfo;
163162

164163
if (state.Current.PropertyState < StackFramePropertyState.Name)
165164
{
166-
state.Current.PropertyState = StackFramePropertyState.Name;
167-
168165
JsonTokenType tokenType = reader.TokenType;
169166
if (tokenType == JsonTokenType.EndObject)
170167
{
@@ -183,6 +180,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
183180
out bool useExtensionProperty);
184181

185182
state.Current.UseExtensionProperty = useExtensionProperty;
183+
state.Current.PropertyState = StackFramePropertyState.Name;
186184
}
187185
else
188186
{
@@ -194,7 +192,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
194192
{
195193
if (!jsonPropertyInfo.CanDeserializeOrPopulate)
196194
{
197-
if (!reader.TrySkip())
195+
if (!reader.TrySkipPartial(targetDepth: state.Current.OriginalDepth + 1))
198196
{
199197
state.Current.ReturnValue = obj;
200198
value = default;
@@ -211,6 +209,8 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
211209
value = default;
212210
return false;
213211
}
212+
213+
state.Current.PropertyState = StackFramePropertyState.ReadValue;
214214
}
215215

216216
if (state.Current.PropertyState < StackFramePropertyState.TryRead)
@@ -258,7 +258,9 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
258258
}
259259

260260
// This method is using aggressive inlining to avoid extra stack frame for deep object graphs.
261+
#if !DEBUG
261262
[MethodImpl(MethodImplOptions.AggressiveInlining)]
263+
#endif
262264
internal static void PopulatePropertiesFastPath(object obj, JsonTypeInfo jsonTypeInfo, JsonSerializerOptions options, ref Utf8JsonReader reader, scoped ref ReadStack state)
263265
{
264266
jsonTypeInfo.OnDeserializing?.Invoke(obj);
@@ -485,26 +487,9 @@ protected static void ReadPropertyValue(
485487

486488
protected static bool ReadAheadPropertyValue(scoped ref ReadStack state, ref Utf8JsonReader reader, JsonPropertyInfo jsonPropertyInfo)
487489
{
488-
// Returning false below will cause the read-ahead functionality to finish the read.
489-
state.Current.PropertyState = StackFramePropertyState.ReadValue;
490-
491-
if (!state.Current.UseExtensionProperty)
492-
{
493-
if (!SingleValueReadWithReadAhead(jsonPropertyInfo.EffectiveConverter.RequiresReadAhead, ref reader, ref state))
494-
{
495-
return false;
496-
}
497-
}
498-
else
499-
{
500-
// The actual converter is JsonElement, so force a read-ahead.
501-
if (!SingleValueReadWithReadAhead(requiresReadAhead: true, ref reader, ref state))
502-
{
503-
return false;
504-
}
505-
}
506-
507-
return true;
490+
// Extension properties can use the JsonElement converter and thus require read-ahead.
491+
bool requiresReadAhead = jsonPropertyInfo.EffectiveConverter.RequiresReadAhead || state.Current.UseExtensionProperty;
492+
return reader.TryAdvanceWithOptionalReadAhead(requiresReadAhead);
508493
}
509494
}
510495
}

0 commit comments

Comments
 (0)