Skip to content

Commit 856a0a6

Browse files
STJ: Support serialization callbacks for collection and dictionary types (#104120)
* STJ: Support serialization callbacks for collection and dictionary types * Fix tests * Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonCollectionConverter.cs Co-authored-by: Eirik Tsarpalis <[email protected]> * Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs Co-authored-by: Eirik Tsarpalis <[email protected]> * Reverse Kind logic to be more future proof * Trigger callbacks before writing any JSON or metadata content * Add JsonTypeInfo tests * Call OnSerializing before any writing operation * Keep result as variable name for partial operations * Prevent setting OnDeserialize callback on immutable types * Avoid using reflection when possible * Set IsImmutableType for all converters overriding ConvertCollection * Rename to IsImmutableCollectionType * Remove extra empty lines * Rename exception message * Rename immutable -> convertible and fix issue around callback use for struct types --------- Co-authored-by: Eirik Tsarpalis <[email protected]>
1 parent 45f3250 commit 856a0a6

17 files changed

+379
-29
lines changed

src/libraries/System.Text.Json/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,9 @@
674674
<data name="InvalidJsonTypeInfoOperationForKind" xml:space="preserve">
675675
<value>Invalid JsonTypeInfo operation for JsonTypeInfoKind '{0}'.</value>
676676
</data>
677+
<data name="OnDeserializingCallbacksNotSupported" xml:space="preserve">
678+
<value>The type '{0}' does not support setting OnDeserializing callbacks.</value>
679+
</data>
677680
<data name="CreateObjectConverterNotCompatible" xml:space="preserve">
678681
<value>The converter for type '{0}' does not support setting 'CreateObject' delegates.</value>
679682
</data>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref R
2424
state.Current.ReturnValue = new List<TElement>();
2525
}
2626

27+
internal sealed override bool IsConvertibleCollection => true;
2728
protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options)
2829
{
2930
List<TElement> list = (List<TElement>)state.Current.ReturnValue!;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ protected sealed override void CreateCollection(ref Utf8JsonReader reader, scope
2525
state.Current.ReturnValue = new Dictionary<TKey, TValue>();
2626
}
2727

28+
internal sealed override bool IsConvertibleCollection => true;
2829
protected sealed override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options)
2930
{
3031
Func<IEnumerable<KeyValuePair<TKey, TValue>>, TDictionary>? creator =

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ protected sealed override void CreateCollection(ref Utf8JsonReader reader, scope
2424
state.Current.ReturnValue = new List<TElement>();
2525
}
2626

27+
internal sealed override bool IsConvertibleCollection => true;
2728
protected sealed override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options)
2829
{
2930
JsonTypeInfo typeInfo = state.Current.JsonTypeInfo;

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ protected virtual void CreateCollection(ref Utf8JsonReader reader, scoped ref Re
4141
Debug.Assert(state.Current.ReturnValue is TCollection);
4242
}
4343

44+
/// <summary>
45+
/// When overridden, converts the temporary collection held in state.Current.ReturnValue to the final collection.
46+
/// The <see cref="JsonConverter.IsConvertibleCollection"/> property must also be set to <see langword="true"/>.
47+
/// </summary>
4448
protected virtual void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) { }
4549

4650
protected static JsonConverter<TElement> GetElementConverter(JsonTypeInfo elementTypeInfo)
@@ -61,7 +65,8 @@ internal override bool OnTryRead(
6165
scoped ref ReadStack state,
6266
[MaybeNullWhen(false)] out TCollection value)
6367
{
64-
JsonTypeInfo elementTypeInfo = state.Current.JsonTypeInfo.ElementTypeInfo!;
68+
JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo;
69+
JsonTypeInfo elementTypeInfo = jsonTypeInfo.ElementTypeInfo!;
6570

6671
if (!state.SupportContinuation && !state.Current.CanContainMetadata)
6772
{
@@ -74,6 +79,8 @@ internal override bool OnTryRead(
7479

7580
CreateCollection(ref reader, ref state, options);
7681

82+
jsonTypeInfo.OnDeserializing?.Invoke(state.Current.ReturnValue!);
83+
7784
state.Current.JsonPropertyInfo = elementTypeInfo.PropertyInfoForTypeInfo;
7885
JsonConverter<TElement> elementConverter = GetElementConverter(elementTypeInfo);
7986
if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null)
@@ -112,8 +119,6 @@ internal override bool OnTryRead(
112119
else
113120
{
114121
// Slower path that supports continuation and reading metadata.
115-
JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo;
116-
117122
if (state.Current.ObjectState == StackFrameObjectState.None)
118123
{
119124
if (reader.TokenType == JsonTokenType.StartArray)
@@ -183,6 +188,8 @@ internal override bool OnTryRead(
183188
state.ReferenceId = null;
184189
}
185190

191+
jsonTypeInfo.OnDeserializing?.Invoke(state.Current.ReturnValue!);
192+
186193
state.Current.ObjectState = StackFrameObjectState.CreatedObject;
187194
}
188195

@@ -274,7 +281,10 @@ internal override bool OnTryRead(
274281
}
275282

276283
ConvertCollection(ref state, options);
277-
value = (TCollection)state.Current.ReturnValue!;
284+
object returnValue = state.Current.ReturnValue!;
285+
jsonTypeInfo.OnDeserialized?.Invoke(returnValue);
286+
value = (TCollection)returnValue;
287+
278288
return true;
279289
}
280290

@@ -293,18 +303,22 @@ internal override bool OnTryWrite(
293303
}
294304
else
295305
{
306+
JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo;
307+
296308
if (!state.Current.ProcessedStartToken)
297309
{
298310
state.Current.ProcessedStartToken = true;
299311

312+
jsonTypeInfo.OnSerializing?.Invoke(value);
313+
300314
if (state.CurrentContainsMetadata && CanHaveMetadata)
301315
{
302316
state.Current.MetadataPropertyName = JsonSerializer.WriteMetadataForCollection(this, ref state, writer);
303317
}
304318

305319
// Writing the start of the array must happen after any metadata
306320
writer.WriteStartArray();
307-
state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
321+
state.Current.JsonPropertyInfo = jsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
308322
}
309323

310324
success = OnWriteResume(writer, value, options, ref state);
@@ -321,6 +335,8 @@ internal override bool OnTryWrite(
321335
writer.WriteEndObject();
322336
}
323337
}
338+
339+
jsonTypeInfo.OnSerialized?.Invoke(value);
324340
}
325341
}
326342

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

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ internal sealed override bool OnTryRead(
7676
scoped ref ReadStack state,
7777
[MaybeNullWhen(false)] out TDictionary value)
7878
{
79-
JsonTypeInfo keyTypeInfo = state.Current.JsonTypeInfo.KeyTypeInfo!;
80-
JsonTypeInfo elementTypeInfo = state.Current.JsonTypeInfo.ElementTypeInfo!;
79+
JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo;
80+
JsonTypeInfo keyTypeInfo = jsonTypeInfo.KeyTypeInfo!;
81+
JsonTypeInfo elementTypeInfo = jsonTypeInfo.ElementTypeInfo!;
8182

8283
if (!state.SupportContinuation && !state.Current.CanContainMetadata)
8384
{
@@ -90,6 +91,8 @@ internal sealed override bool OnTryRead(
9091

9192
CreateCollection(ref reader, ref state);
9293

94+
jsonTypeInfo.OnDeserializing?.Invoke(state.Current.ReturnValue!);
95+
9396
_keyConverter ??= GetConverter<TKey>(keyTypeInfo);
9497
_valueConverter ??= GetConverter<TValue>(elementTypeInfo);
9598

@@ -149,8 +152,6 @@ internal sealed override bool OnTryRead(
149152
else
150153
{
151154
// Slower path that supports continuation and reading metadata.
152-
JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo;
153-
154155
if (state.Current.ObjectState == StackFrameObjectState.None)
155156
{
156157
if (reader.TokenType != JsonTokenType.StartObject)
@@ -210,6 +211,8 @@ internal sealed override bool OnTryRead(
210211
state.ReferenceId = null;
211212
}
212213

214+
jsonTypeInfo.OnDeserializing?.Invoke(state.Current.ReturnValue!);
215+
213216
state.Current.ObjectState = StackFrameObjectState.CreatedObject;
214217
}
215218

@@ -302,7 +305,10 @@ internal sealed override bool OnTryRead(
302305
}
303306

304307
ConvertCollection(ref state, options);
305-
value = (TDictionary)state.Current.ReturnValue!;
308+
object result = state.Current.ReturnValue!;
309+
jsonTypeInfo.OnDeserialized?.Invoke(result);
310+
value = (TDictionary)result;
311+
306312
return true;
307313

308314
static TKey ReadDictionaryKey(JsonConverter<TKey> keyConverter, ref Utf8JsonReader reader, scoped ref ReadStack state, JsonSerializerOptions options)
@@ -337,17 +343,22 @@ internal sealed override bool OnTryWrite(
337343
return true;
338344
}
339345

346+
JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo;
347+
340348
if (!state.Current.ProcessedStartToken)
341349
{
342350
state.Current.ProcessedStartToken = true;
351+
352+
jsonTypeInfo.OnSerializing?.Invoke(dictionary);
353+
343354
writer.WriteStartObject();
344355

345356
if (state.CurrentContainsMetadata && CanHaveMetadata)
346357
{
347358
JsonSerializer.WriteMetadataForObject(this, ref state, writer);
348359
}
349360

350-
state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
361+
state.Current.JsonPropertyInfo = jsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
351362
}
352363

353364
bool success = OnWriteResume(writer, dictionary, options, ref state);
@@ -358,6 +369,8 @@ internal sealed override bool OnTryWrite(
358369
state.Current.ProcessedEndToken = true;
359370
writer.WriteEndObject();
360371
}
372+
373+
jsonTypeInfo.OnSerialized?.Invoke(dictionary);
361374
}
362375

363376
return success;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref R
3737
state.Current.ReturnValue = new List<T>();
3838
}
3939

40+
internal sealed override bool IsConvertibleCollection => true;
4041
protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options)
4142
{
4243
Memory<T> memory = ((List<T>)state.Current.ReturnValue!).ToArray().AsMemory();

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref R
3737
state.Current.ReturnValue = new List<T>();
3838
}
3939

40+
internal sealed override bool IsConvertibleCollection => true;
4041
protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options)
4142
{
4243
ReadOnlyMemory<T> memory = ((List<T>)state.Current.ReturnValue!).ToArray().AsMemory();

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref R
3131
state.Current.ReturnValue = new List<TElement>();
3232
}
3333

34+
internal sealed override bool IsConvertibleCollection => true;
3435
protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options)
3536
{
3637
state.Current.ReturnValue = _listConstructor((List<TElement>)state.Current.ReturnValue!);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref R
3434
state.Current.ReturnValue = new List<Tuple<TKey, TValue>>();
3535
}
3636

37+
internal sealed override bool IsConvertibleCollection => true;
3738
protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options)
3839
{
3940
state.Current.ReturnValue = _mapConstructor((List<Tuple<TKey, TValue>>)state.Current.ReturnValue!);

0 commit comments

Comments
 (0)