Skip to content

Commit fb69200

Browse files
Support resumable serialization in NullableConverter<T> (#65524)
* Support resumable serialization in NullableConverter<T> * use null instead of default
1 parent cc649ac commit fb69200

File tree

13 files changed

+97
-43
lines changed

13 files changed

+97
-43
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ protected static JsonConverter<TElement> GetElementConverter(JsonTypeInfo elemen
5757

5858
protected static JsonConverter<TElement> GetElementConverter(ref WriteStack state)
5959
{
60-
JsonConverter<TElement> converter = (JsonConverter<TElement>)state.Current.DeclaredJsonPropertyInfo!.ConverterBase;
60+
JsonConverter<TElement> converter = (JsonConverter<TElement>)state.Current.JsonPropertyInfo!.ConverterBase;
6161
Debug.Assert(converter != null); // It should not be possible to have a null converter at this point.
6262

6363
return converter;
@@ -282,7 +282,7 @@ internal override bool OnTryWrite(
282282
writer.WriteStartArray();
283283
}
284284

285-
state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
285+
state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
286286
}
287287

288288
success = OnWriteResume(writer, value, options, ref state);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ internal sealed override bool OnTryWrite(
317317
}
318318
}
319319

320-
state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
320+
state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
321321
}
322322

323323
bool success = OnWriteResume(writer, dictionary, options, ref state);

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, TOption value, JsonSeri
6666
}
6767

6868
TElement element = _optionValueGetter(value);
69-
state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
69+
state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
7070
return _elementConverter.TryWrite(writer, element, options, ref state);
7171
}
7272

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, TValueOption value, Jso
6767

6868
TElement element = _optionValueGetter(ref value);
6969

70-
state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
70+
state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
7171
return _elementConverter.TryWrite(writer, element, options, ref state);
7272
}
7373

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ internal sealed override bool OnTryWrite(
279279
if (jsonPropertyInfo.ShouldSerialize)
280280
{
281281
// Remember the current property for JsonPath support if an exception is thrown.
282-
state.Current.DeclaredJsonPropertyInfo = jsonPropertyInfo;
282+
state.Current.JsonPropertyInfo = jsonPropertyInfo;
283283
state.Current.NumberHandling = jsonPropertyInfo.NumberHandling;
284284

285285
bool success = jsonPropertyInfo.GetMemberAndWriteJson(obj, ref state, writer);
@@ -295,7 +295,7 @@ internal sealed override bool OnTryWrite(
295295
if (dataExtensionProperty?.ShouldSerialize == true)
296296
{
297297
// Remember the current property for JsonPath support if an exception is thrown.
298-
state.Current.DeclaredJsonPropertyInfo = dataExtensionProperty;
298+
state.Current.JsonPropertyInfo = dataExtensionProperty;
299299
state.Current.NumberHandling = dataExtensionProperty.NumberHandling;
300300

301301
bool success = dataExtensionProperty.GetMemberAndWriteJsonExtensionData(obj, ref state, writer);
@@ -334,7 +334,7 @@ internal sealed override bool OnTryWrite(
334334
Debug.Assert(jsonPropertyInfo != null);
335335
if (jsonPropertyInfo.ShouldSerialize)
336336
{
337-
state.Current.DeclaredJsonPropertyInfo = jsonPropertyInfo;
337+
state.Current.JsonPropertyInfo = jsonPropertyInfo;
338338
state.Current.NumberHandling = jsonPropertyInfo.NumberHandling;
339339

340340
if (!jsonPropertyInfo.GetMemberAndWriteJson(obj!, ref state, writer))
@@ -366,7 +366,7 @@ internal sealed override bool OnTryWrite(
366366
if (dataExtensionProperty?.ShouldSerialize == true)
367367
{
368368
// Remember the current property for JsonPath support if an exception is thrown.
369-
state.Current.DeclaredJsonPropertyInfo = dataExtensionProperty;
369+
state.Current.JsonPropertyInfo = dataExtensionProperty;
370370
state.Current.NumberHandling = dataExtensionProperty.NumberHandling;
371371

372372
if (!dataExtensionProperty.GetMemberAndWriteJsonExtensionData(obj, ref state, writer))

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

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,102 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Diagnostics;
5-
64
namespace System.Text.Json.Serialization.Converters
75
{
86
internal sealed class NullableConverter<T> : JsonConverter<T?> where T : struct
97
{
8+
internal override ConverterStrategy ConverterStrategy { get; }
9+
internal override Type? ElementType => typeof(T);
10+
public override bool HandleNull => true;
11+
1012
// It is possible to cache the underlying converter since this is an internal converter and
1113
// an instance is created only once for each JsonSerializerOptions instance.
12-
private readonly JsonConverter<T> _converter;
14+
private readonly JsonConverter<T> _elementConverter;
15+
16+
public NullableConverter(JsonConverter<T> elementConverter)
17+
{
18+
_elementConverter = elementConverter;
19+
ConverterStrategy = elementConverter.ConverterStrategy;
20+
IsInternalConverterForNumberType = elementConverter.IsInternalConverterForNumberType;
21+
// temporary workaround for JsonConverter base constructor needing to access
22+
// ConverterStrategy when calculating `CanUseDirectReadOrWrite`.
23+
CanUseDirectReadOrWrite = elementConverter.ConverterStrategy == ConverterStrategy.Value;
24+
}
25+
26+
internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T? value)
27+
{
28+
if (!state.IsContinuation && reader.TokenType == JsonTokenType.Null)
29+
{
30+
value = null;
31+
return true;
32+
}
1333

14-
public NullableConverter(JsonConverter<T> converter)
34+
state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
35+
if (_elementConverter.TryRead(ref reader, typeof(T), options, ref state, out T element))
36+
{
37+
value = element;
38+
return true;
39+
}
40+
41+
value = null;
42+
return false;
43+
}
44+
45+
internal override bool OnTryWrite(Utf8JsonWriter writer, T? value, JsonSerializerOptions options, ref WriteStack state)
1546
{
16-
_converter = converter;
17-
IsInternalConverterForNumberType = converter.IsInternalConverterForNumberType;
47+
if (value is null)
48+
{
49+
writer.WriteNullValue();
50+
return true;
51+
}
52+
53+
state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
54+
return _elementConverter.TryWrite(writer, value.Value, options, ref state);
1855
}
1956

2057
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
2158
{
22-
// We do not check _converter.HandleNull, as the underlying struct cannot be null.
23-
// A custom converter for some type T? can handle null.
2459
if (reader.TokenType == JsonTokenType.Null)
2560
{
2661
return null;
2762
}
2863

29-
T value = _converter.Read(ref reader, typeof(T), options);
64+
T value = _elementConverter.Read(ref reader, typeof(T), options);
3065
return value;
3166
}
3267

3368
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
3469
{
35-
if (!value.HasValue)
70+
if (value is null)
3671
{
37-
// We do not check _converter.HandleNull, as the underlying struct cannot be null.
38-
// A custom converter for some type T? can handle null.
3972
writer.WriteNullValue();
4073
}
4174
else
4275
{
43-
_converter.Write(writer, value.Value, options);
76+
_elementConverter.Write(writer, value.Value, options);
4477
}
4578
}
4679

4780
internal override T? ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling numberHandling, JsonSerializerOptions options)
4881
{
49-
// We do not check _converter.HandleNull, as the underlying struct cannot be null.
50-
// A custom converter for some type T? can handle null.
5182
if (reader.TokenType == JsonTokenType.Null)
5283
{
5384
return null;
5485
}
5586

56-
T value = _converter.ReadNumberWithCustomHandling(ref reader, numberHandling, options);
87+
T value = _elementConverter.ReadNumberWithCustomHandling(ref reader, numberHandling, options);
5788
return value;
5889
}
5990

6091
internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, T? value, JsonNumberHandling handling)
6192
{
62-
if (!value.HasValue)
93+
if (value is null)
6394
{
64-
// We do not check _converter.HandleNull, as the underlying struct cannot be null.
65-
// A custom converter for some type T? can handle null.
6695
writer.WriteNullValue();
6796
}
6897
else
6998
{
70-
_converter.WriteNumberWithCustomHandling(writer, value.Value, handling);
99+
_elementConverter.WriteNumberWithCustomHandling(writer, value.Value, handling);
71100
}
72101
}
73102
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali
227227
JsonTypeInfo originalJsonTypeInfo = state.Current.JsonTypeInfo;
228228
#endif
229229
state.Push();
230-
Debug.Assert(TypeToConvert.IsAssignableFrom(state.Current.JsonTypeInfo.Type));
230+
Debug.Assert(TypeToConvert == state.Current.JsonTypeInfo.Type);
231231

232232
#if !DEBUG
233233
// For performance, only perform validation on internal converters on debug builds.
@@ -462,7 +462,7 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions
462462
JsonTypeInfo originalJsonTypeInfo = state.Current.JsonTypeInfo;
463463
#endif
464464
state.Push();
465-
Debug.Assert(TypeToConvert.IsAssignableFrom(state.Current.JsonTypeInfo.Type));
465+
Debug.Assert(TypeToConvert == state.Current.JsonTypeInfo.Type);
466466

467467
if (!isContinuation)
468468
{
@@ -528,7 +528,7 @@ internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, Json
528528

529529
// Extension data properties change how dictionary key naming policies are applied.
530530
state.Current.IsWritingExtensionDataProperty = true;
531-
state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
531+
state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
532532

533533
success = dictionaryConverter.OnWriteResume(writer, value, options, ref state);
534534
if (success)

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public static JsonTypeInfo<T> CreateObjectInfo<T>(JsonSerializerOptions options!
8080
/// <remarks>This API is for use by the output of the System.Text.Json source generator and should not be called directly.</remarks>
8181
public static JsonTypeInfo<T> CreateValueInfo<T>(JsonSerializerOptions options, JsonConverter converter)
8282
{
83-
JsonTypeInfo<T> info = new JsonTypeInfoInternal<T>(options);
83+
JsonTypeInfo<T> info = new JsonTypeInfoInternal<T>(converter, options);
8484
info.PropertyInfoForTypeInfo = CreateJsonPropertyInfoForClassInfo(typeof(T), info, converter, options);
8585
converter.ConfigureJsonTypeInfo(info, options);
8686
return info;

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoInternalOfT.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ internal sealed class JsonTypeInfoInternal<T> : JsonTypeInfo<T>
1414
/// <summary>
1515
/// Creates serialization metadata for a type using a simple converter.
1616
/// </summary>
17-
public JsonTypeInfoInternal(JsonSerializerOptions options)
17+
public JsonTypeInfoInternal(JsonConverter converter, JsonSerializerOptions options)
1818
: base(typeof(T), options)
1919
{
20+
ElementType = converter.ElementType;
2021
}
2122

2223
/// <summary>
@@ -45,6 +46,7 @@ public JsonTypeInfoInternal(JsonSerializerOptions options, JsonObjectInfoValues<
4546
#pragma warning restore CS8714
4647

4748
PropInitFunc = objectInfo.PropertyMetadataInitializer;
49+
ElementType = converter.ElementType;
4850
SerializeHandler = objectInfo.SerializeHandler;
4951
PropertyInfoForTypeInfo = JsonMetadataServices.CreateJsonPropertyInfoForClassInfo(typeof(T), this, converter, Options);
5052
NumberHandling = objectInfo.NumberHandling;

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ public JsonConverter Initialize(Type type, JsonSerializerOptions options, bool s
106106
internal JsonConverter Initialize(JsonTypeInfo jsonTypeInfo, bool supportContinuation)
107107
{
108108
Current.JsonTypeInfo = jsonTypeInfo;
109-
Current.DeclaredJsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo;
110-
Current.NumberHandling = Current.DeclaredJsonPropertyInfo.NumberHandling;
109+
Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo;
110+
Current.NumberHandling = Current.JsonPropertyInfo.NumberHandling;
111111

112112
JsonSerializerOptions options = jsonTypeInfo.Options;
113113
if (options.ReferenceHandlingStrategy != ReferenceHandlingStrategy.None)
@@ -141,9 +141,9 @@ public void Push()
141141
_count++;
142142

143143
Current.JsonTypeInfo = jsonTypeInfo;
144-
Current.DeclaredJsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo;
144+
Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo;
145145
// Allow number handling on property to win over handling on type.
146-
Current.NumberHandling = numberHandling ?? Current.DeclaredJsonPropertyInfo.NumberHandling;
146+
Current.NumberHandling = numberHandling ?? Current.JsonPropertyInfo.NumberHandling;
147147
}
148148
}
149149
else
@@ -347,7 +347,7 @@ public string PropertyPath()
347347
static void AppendStackFrame(StringBuilder sb, ref WriteStackFrame frame)
348348
{
349349
// Append the property name.
350-
string? propertyName = frame.DeclaredJsonPropertyInfo?.ClrName;
350+
string? propertyName = frame.JsonPropertyInfo?.ClrName;
351351
if (propertyName == null)
352352
{
353353
// Attempt to get the JSON property name from the property name specified in re-entry.

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ internal struct WriteStackFrame
3434
/// For objects, it is either the actual (real) JsonPropertyInfo or the <see cref="JsonTypeInfo.PropertyInfoForTypeInfo"/> for the class.
3535
/// For collections, it is the <see cref="JsonTypeInfo.PropertyInfoForTypeInfo"/> for the class and current element.
3636
/// </remarks>
37-
public JsonPropertyInfo? DeclaredJsonPropertyInfo;
37+
public JsonPropertyInfo? JsonPropertyInfo;
3838

3939
/// <summary>
4040
/// Used when processing extension data dictionaries.
@@ -90,7 +90,7 @@ public void EndDictionaryElement()
9090

9191
public void EndProperty()
9292
{
93-
DeclaredJsonPropertyInfo = null!;
93+
JsonPropertyInfo = null!;
9494
JsonPropertyNameAsString = null;
9595
PolymorphicJsonPropertyInfo = null;
9696
PropertyState = StackFramePropertyState.None;
@@ -102,7 +102,7 @@ public void EndProperty()
102102
/// </summary>
103103
public JsonPropertyInfo GetPolymorphicJsonPropertyInfo()
104104
{
105-
return PolymorphicJsonPropertyInfo ?? DeclaredJsonPropertyInfo!;
105+
return PolymorphicJsonPropertyInfo ?? JsonPropertyInfo!;
106106
}
107107

108108
/// <summary>

src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ public static void ThrowNotSupportedException(ref WriteStack state, NotSupported
408408
Debug.Assert(!message.Contains(" Path: "));
409409

410410
// Obtain the type to show in the message.
411-
Type? propertyType = state.Current.DeclaredJsonPropertyInfo?.PropertyType;
411+
Type? propertyType = state.Current.JsonPropertyInfo?.PropertyType;
412412
if (propertyType == null)
413413
{
414414
propertyType = state.Current.JsonTypeInfo.Type;

src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.AsyncEnumerable.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,29 @@ public async Task WriteNestedAsyncEnumerable_DTO<TElement>(IEnumerable<TElement>
7474
Assert.Equal(1, asyncEnumerable.TotalDisposedEnumerators);
7575
}
7676

77+
[Theory]
78+
[MemberData(nameof(GetAsyncEnumerableSources))]
79+
public async Task WriteNestedAsyncEnumerable_Nullable<TElement>(IEnumerable<TElement> source, int delayInterval, int bufferSize)
80+
{
81+
// Primarily tests the ability of NullableConverter to flow async serialization state
82+
83+
JsonSerializerOptions options = new JsonSerializerOptions
84+
{
85+
DefaultBufferSize = bufferSize,
86+
IncludeFields = true,
87+
};
88+
89+
string expectedJson = await JsonSerializerWrapperForString.SerializeWrapper<(IEnumerable<TElement>, bool)?>((source, false), options);
90+
91+
using var stream = new Utf8MemoryStream();
92+
var asyncEnumerable = new MockedAsyncEnumerable<TElement>(source, delayInterval);
93+
await JsonSerializerWrapperForStream.SerializeWrapper<(IAsyncEnumerable<TElement>, bool)?>(stream, (asyncEnumerable, false), options);
94+
95+
JsonTestHelper.AssertJsonEqual(expectedJson, stream.ToString());
96+
Assert.Equal(1, asyncEnumerable.TotalCreatedEnumerators);
97+
Assert.Equal(1, asyncEnumerable.TotalDisposedEnumerators);
98+
}
99+
77100
[Theory, OuterLoop]
78101
[InlineData(5000, 1000, true)]
79102
[InlineData(5000, 1000, false)]

0 commit comments

Comments
 (0)