From 856a0a6f5e991fd18e125bcfc88741d919688fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Andr=C3=A9?= <2341261+manandre@users.noreply.github.com> Date: Fri, 12 Jul 2024 21:02:19 +0200 Subject: [PATCH] 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 * Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs Co-authored-by: Eirik Tsarpalis * 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 --- .../src/Resources/Strings.resx | 3 + .../Converters/Collection/ArrayConverter.cs | 1 + ...mmutableDictionaryOfTKeyTValueConverter.cs | 1 + .../ImmutableEnumerableOfTConverter.cs | 1 + .../Collection/JsonCollectionConverter.cs | 26 ++- .../Collection/JsonDictionaryConverter.cs | 25 ++- .../Converters/Collection/MemoryConverter.cs | 1 + .../Collection/ReadOnlyMemoryConverter.cs | 1 + .../Converters/FSharp/FSharpListConverter.cs | 1 + .../Converters/FSharp/FSharpMapConverter.cs | 1 + .../Converters/FSharp/FSharpSetConverter.cs | 1 + .../Object/ObjectDefaultConverter.cs | 4 +- .../Text/Json/Serialization/JsonConverter.cs | 7 + .../Serialization/Metadata/JsonTypeInfo.cs | 18 +- .../Text/Json/ThrowHelper.Serialization.cs | 6 + ...tJsonTypeInfoResolverTests.JsonTypeInfo.cs | 188 +++++++++++++++++- .../Serialization/OnSerializeTests.cs | 123 +++++++++++- 17 files changed, 379 insertions(+), 29 deletions(-) diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index dc9458c6d705b..d7b73df8a9e87 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -674,6 +674,9 @@ Invalid JsonTypeInfo operation for JsonTypeInfoKind '{0}'. + + The type '{0}' does not support setting OnDeserializing callbacks. + The converter for type '{0}' does not support setting 'CreateObject' delegates. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs index 39ec744652117..f50ddf04b40b7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs @@ -24,6 +24,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref R state.Current.ReturnValue = new List(); } + internal sealed override bool IsConvertibleCollection => true; protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) { List list = (List)state.Current.ReturnValue!; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfTKeyTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfTKeyTValueConverter.cs index db6b62a8de82b..0d622ab4142ad 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfTKeyTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfTKeyTValueConverter.cs @@ -25,6 +25,7 @@ protected sealed override void CreateCollection(ref Utf8JsonReader reader, scope state.Current.ReturnValue = new Dictionary(); } + internal sealed override bool IsConvertibleCollection => true; protected sealed override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) { Func>, TDictionary>? creator = diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableEnumerableOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableEnumerableOfTConverter.cs index 541f5cbf57a70..82577539af58d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableEnumerableOfTConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableEnumerableOfTConverter.cs @@ -24,6 +24,7 @@ protected sealed override void CreateCollection(ref Utf8JsonReader reader, scope state.Current.ReturnValue = new List(); } + internal sealed override bool IsConvertibleCollection => true; protected sealed override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) { JsonTypeInfo typeInfo = state.Current.JsonTypeInfo; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonCollectionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonCollectionConverter.cs index 2caa653ae8e52..7c65ff591f2ed 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonCollectionConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonCollectionConverter.cs @@ -41,6 +41,10 @@ protected virtual void CreateCollection(ref Utf8JsonReader reader, scoped ref Re Debug.Assert(state.Current.ReturnValue is TCollection); } + /// + /// When overridden, converts the temporary collection held in state.Current.ReturnValue to the final collection. + /// The property must also be set to . + /// protected virtual void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) { } protected static JsonConverter GetElementConverter(JsonTypeInfo elementTypeInfo) @@ -61,7 +65,8 @@ internal override bool OnTryRead( scoped ref ReadStack state, [MaybeNullWhen(false)] out TCollection value) { - JsonTypeInfo elementTypeInfo = state.Current.JsonTypeInfo.ElementTypeInfo!; + JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; + JsonTypeInfo elementTypeInfo = jsonTypeInfo.ElementTypeInfo!; if (!state.SupportContinuation && !state.Current.CanContainMetadata) { @@ -74,6 +79,8 @@ internal override bool OnTryRead( CreateCollection(ref reader, ref state, options); + jsonTypeInfo.OnDeserializing?.Invoke(state.Current.ReturnValue!); + state.Current.JsonPropertyInfo = elementTypeInfo.PropertyInfoForTypeInfo; JsonConverter elementConverter = GetElementConverter(elementTypeInfo); if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) @@ -112,8 +119,6 @@ internal override bool OnTryRead( else { // Slower path that supports continuation and reading metadata. - JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; - if (state.Current.ObjectState == StackFrameObjectState.None) { if (reader.TokenType == JsonTokenType.StartArray) @@ -183,6 +188,8 @@ internal override bool OnTryRead( state.ReferenceId = null; } + jsonTypeInfo.OnDeserializing?.Invoke(state.Current.ReturnValue!); + state.Current.ObjectState = StackFrameObjectState.CreatedObject; } @@ -274,7 +281,10 @@ internal override bool OnTryRead( } ConvertCollection(ref state, options); - value = (TCollection)state.Current.ReturnValue!; + object returnValue = state.Current.ReturnValue!; + jsonTypeInfo.OnDeserialized?.Invoke(returnValue); + value = (TCollection)returnValue; + return true; } @@ -293,10 +303,14 @@ internal override bool OnTryWrite( } else { + JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; + if (!state.Current.ProcessedStartToken) { state.Current.ProcessedStartToken = true; + jsonTypeInfo.OnSerializing?.Invoke(value); + if (state.CurrentContainsMetadata && CanHaveMetadata) { state.Current.MetadataPropertyName = JsonSerializer.WriteMetadataForCollection(this, ref state, writer); @@ -304,7 +318,7 @@ internal override bool OnTryWrite( // Writing the start of the array must happen after any metadata writer.WriteStartArray(); - state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; + state.Current.JsonPropertyInfo = jsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; } success = OnWriteResume(writer, value, options, ref state); @@ -321,6 +335,8 @@ internal override bool OnTryWrite( writer.WriteEndObject(); } } + + jsonTypeInfo.OnSerialized?.Invoke(value); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs index fad9e0aae5ffa..c1e8f040e3fda 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs @@ -76,8 +76,9 @@ internal sealed override bool OnTryRead( scoped ref ReadStack state, [MaybeNullWhen(false)] out TDictionary value) { - JsonTypeInfo keyTypeInfo = state.Current.JsonTypeInfo.KeyTypeInfo!; - JsonTypeInfo elementTypeInfo = state.Current.JsonTypeInfo.ElementTypeInfo!; + JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; + JsonTypeInfo keyTypeInfo = jsonTypeInfo.KeyTypeInfo!; + JsonTypeInfo elementTypeInfo = jsonTypeInfo.ElementTypeInfo!; if (!state.SupportContinuation && !state.Current.CanContainMetadata) { @@ -90,6 +91,8 @@ internal sealed override bool OnTryRead( CreateCollection(ref reader, ref state); + jsonTypeInfo.OnDeserializing?.Invoke(state.Current.ReturnValue!); + _keyConverter ??= GetConverter(keyTypeInfo); _valueConverter ??= GetConverter(elementTypeInfo); @@ -149,8 +152,6 @@ internal sealed override bool OnTryRead( else { // Slower path that supports continuation and reading metadata. - JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; - if (state.Current.ObjectState == StackFrameObjectState.None) { if (reader.TokenType != JsonTokenType.StartObject) @@ -210,6 +211,8 @@ internal sealed override bool OnTryRead( state.ReferenceId = null; } + jsonTypeInfo.OnDeserializing?.Invoke(state.Current.ReturnValue!); + state.Current.ObjectState = StackFrameObjectState.CreatedObject; } @@ -302,7 +305,10 @@ internal sealed override bool OnTryRead( } ConvertCollection(ref state, options); - value = (TDictionary)state.Current.ReturnValue!; + object result = state.Current.ReturnValue!; + jsonTypeInfo.OnDeserialized?.Invoke(result); + value = (TDictionary)result; + return true; static TKey ReadDictionaryKey(JsonConverter keyConverter, ref Utf8JsonReader reader, scoped ref ReadStack state, JsonSerializerOptions options) @@ -337,9 +343,14 @@ internal sealed override bool OnTryWrite( return true; } + JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; + if (!state.Current.ProcessedStartToken) { state.Current.ProcessedStartToken = true; + + jsonTypeInfo.OnSerializing?.Invoke(dictionary); + writer.WriteStartObject(); if (state.CurrentContainsMetadata && CanHaveMetadata) @@ -347,7 +358,7 @@ internal sealed override bool OnTryWrite( JsonSerializer.WriteMetadataForObject(this, ref state, writer); } - state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; + state.Current.JsonPropertyInfo = jsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; } bool success = OnWriteResume(writer, dictionary, options, ref state); @@ -358,6 +369,8 @@ internal sealed override bool OnTryWrite( state.Current.ProcessedEndToken = true; writer.WriteEndObject(); } + + jsonTypeInfo.OnSerialized?.Invoke(dictionary); } return success; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/MemoryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/MemoryConverter.cs index 670ab037367a8..2be0f0489000f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/MemoryConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/MemoryConverter.cs @@ -37,6 +37,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref R state.Current.ReturnValue = new List(); } + internal sealed override bool IsConvertibleCollection => true; protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) { Memory memory = ((List)state.Current.ReturnValue!).ToArray().AsMemory(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ReadOnlyMemoryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ReadOnlyMemoryConverter.cs index 9b0ea99ed85d3..6c6c079f6b878 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ReadOnlyMemoryConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ReadOnlyMemoryConverter.cs @@ -37,6 +37,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref R state.Current.ReturnValue = new List(); } + internal sealed override bool IsConvertibleCollection => true; protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) { ReadOnlyMemory memory = ((List)state.Current.ReturnValue!).ToArray().AsMemory(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs index 0cd7ccb1a3020..13260472f6151 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs @@ -31,6 +31,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref R state.Current.ReturnValue = new List(); } + internal sealed override bool IsConvertibleCollection => true; protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) { state.Current.ReturnValue = _listConstructor((List)state.Current.ReturnValue!); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs index a6d395e8f345a..78cada33e7ddb 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs @@ -34,6 +34,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref R state.Current.ReturnValue = new List>(); } + internal sealed override bool IsConvertibleCollection => true; protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) { state.Current.ReturnValue = _mapConstructor((List>)state.Current.ReturnValue!); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpSetConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpSetConverter.cs index 167534d8de3f7..bf551ac6a0618 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpSetConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpSetConverter.cs @@ -31,6 +31,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref R state.Current.ReturnValue = new List(); } + internal sealed override bool IsConvertibleCollection => true; protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) { state.Current.ReturnValue = _setConstructor((List)state.Current.ReturnValue!); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs index ee374daf4ec2d..a8927ce5551a6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs @@ -326,6 +326,8 @@ internal sealed override bool OnTryWrite( if (!state.SupportContinuation) { + jsonTypeInfo.OnSerializing?.Invoke(obj); + writer.WriteStartObject(); if (state.CurrentContainsMetadata && CanHaveMetadata) @@ -333,8 +335,6 @@ internal sealed override bool OnTryWrite( JsonSerializer.WriteMetadataForObject(this, ref state, writer); } - jsonTypeInfo.OnSerializing?.Invoke(obj); - foreach (JsonPropertyInfo jsonPropertyInfo in jsonTypeInfo.PropertyCache) { if (jsonPropertyInfo.CanSerialize) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs index 261a6cf433d6b..1a0e007695dcf 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs @@ -179,6 +179,13 @@ internal JsonConverter CreateCastingConverter() /// internal bool IsInternalConverterForNumberType { get; init; } + /// + /// Whether the converter handles collection deserialization by converting from + /// an intermediate buffer such as immutable collections, arrays or memory types. + /// Used in conjunction with . + /// + internal virtual bool IsConvertibleCollection => false; + internal static bool ShouldFlush(ref WriteStack state, Utf8JsonWriter writer) { Debug.Assert(state.FlushThreshold == 0 || (state.PipeWriter is { CanGetUnflushedBytes: true }), diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index b647ea4384b1d..00c4245a4c87b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -123,7 +123,7 @@ public Action? OnSerializing { VerifyMutable(); - if (Kind != JsonTypeInfoKind.Object) + if (Kind is not (JsonTypeInfoKind.Object or JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary)) { ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKind(Kind); } @@ -153,7 +153,7 @@ public Action? OnSerialized { VerifyMutable(); - if (Kind != JsonTypeInfoKind.Object) + if (Kind is not (JsonTypeInfoKind.Object or JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary)) { ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKind(Kind); } @@ -183,11 +183,17 @@ public Action? OnDeserializing { VerifyMutable(); - if (Kind != JsonTypeInfoKind.Object) + if (Kind is not (JsonTypeInfoKind.Object or JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary)) { ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKind(Kind); } + if (Converter.IsConvertibleCollection) + { + // The values for convertible collections aren't available at the start of deserialization. + ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOnDeserializingCallbacksNotSupported(Type); + } + _onDeserializing = value; } } @@ -213,7 +219,7 @@ public Action? OnDeserialized { VerifyMutable(); - if (Kind != JsonTypeInfoKind.Object) + if (Kind is not (JsonTypeInfoKind.Object or JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary)) { ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKind(Kind); } @@ -1256,9 +1262,7 @@ internal void MapInterfaceTypesToCallbacks() { Debug.Assert(!IsReadOnly); - // Callbacks currently only supported in object kinds - // TODO: extend to collections/dictionaries - if (Kind == JsonTypeInfoKind.Object) + if (Kind is JsonTypeInfoKind.Object or JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary) { if (typeof(IJsonOnSerializing).IsAssignableFrom(Type)) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index fa2fcea774d9e..a793d42abdaea 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -414,6 +414,12 @@ public static void ThrowInvalidOperationException_JsonTypeInfoOperationNotPossib throw new InvalidOperationException(SR.Format(SR.InvalidJsonTypeInfoOperationForKind, kind)); } + [DoesNotReturn] + public static void ThrowInvalidOperationException_JsonTypeInfoOnDeserializingCallbacksNotSupported(Type type) + { + throw new InvalidOperationException(SR.Format(SR.OnDeserializingCallbacksNotSupported, type)); + } + [DoesNotReturn] public static void ThrowInvalidOperationException_CreateObjectConverterNotCompatible(Type type) { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs index 55e1ac60fcc3c..fd39a70daf15a 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs @@ -1369,12 +1369,132 @@ public static void ClassWithCallBacks_CanCustomizeCallbacks() Assert.Equal(7, value.IsOnDeserializedInvocations); } + [Fact] + public static void CollectionWithCallBacks_JsonTypeInfoCallbackDelegatesArePopulated() + { + var resolver = new DefaultJsonTypeInfoResolver(); + var jti = resolver.GetTypeInfo(typeof(CollectionWithCallBacks), new()); + + Assert.NotNull(jti.OnSerializing); + Assert.NotNull(jti.OnSerialized); + Assert.NotNull(jti.OnDeserializing); + Assert.NotNull(jti.OnDeserialized); + + var value = new CollectionWithCallBacks(); + jti.OnSerializing(value); + Assert.Equal(1, value.IsOnSerializingInvocations); + + jti.OnSerialized(value); + Assert.Equal(1, value.IsOnSerializedInvocations); + + jti.OnDeserializing(value); + Assert.Equal(1, value.IsOnDeserializingInvocations); + + jti.OnDeserialized(value); + Assert.Equal(1, value.IsOnDeserializedInvocations); + } + + [Fact] + public static void CollectionWithCallBacks_CanCustomizeCallbacks() + { + var options = new JsonSerializerOptions + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static jti => + { + if (jti.Type == typeof(CollectionWithCallBacks)) + { + jti.OnSerializing = null; + jti.OnSerialized = (obj => ((CollectionWithCallBacks)obj).IsOnSerializedInvocations += 10); + + jti.OnDeserializing = null; + jti.OnDeserialized = (obj => ((CollectionWithCallBacks)obj).IsOnDeserializedInvocations += 7); + } + } + } + } + }; + + var value = new CollectionWithCallBacks(); + string json = JsonSerializer.Serialize(value, options); + Assert.Equal("[]", json); + + Assert.Equal(0, value.IsOnSerializingInvocations); + Assert.Equal(10, value.IsOnSerializedInvocations); + + value = JsonSerializer.Deserialize(json, options); + Assert.Equal(0, value.IsOnDeserializingInvocations); + Assert.Equal(7, value.IsOnDeserializedInvocations); + } + + [Fact] + public static void DictionaryWithCallBacks_JsonTypeInfoCallbackDelegatesArePopulated() + { + var resolver = new DefaultJsonTypeInfoResolver(); + var jti = resolver.GetTypeInfo(typeof(DictionaryWithCallBacks), new()); + + Assert.NotNull(jti.OnSerializing); + Assert.NotNull(jti.OnSerialized); + Assert.NotNull(jti.OnDeserializing); + Assert.NotNull(jti.OnDeserialized); + + var value = new DictionaryWithCallBacks(); + jti.OnSerializing(value); + Assert.Equal(1, value.IsOnSerializingInvocations); + + jti.OnSerialized(value); + Assert.Equal(1, value.IsOnSerializedInvocations); + + jti.OnDeserializing(value); + Assert.Equal(1, value.IsOnDeserializingInvocations); + + jti.OnDeserialized(value); + Assert.Equal(1, value.IsOnDeserializedInvocations); + } + + [Fact] + public static void DictionaryWithCallBacks_CanCustomizeCallbacks() + { + var options = new JsonSerializerOptions + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static jti => + { + if (jti.Type == typeof(DictionaryWithCallBacks)) + { + jti.OnSerializing = null; + jti.OnSerialized = (obj => ((DictionaryWithCallBacks)obj).IsOnSerializedInvocations += 10); + + jti.OnDeserializing = null; + jti.OnDeserialized = (obj => ((DictionaryWithCallBacks)obj).IsOnDeserializedInvocations += 7); + } + } + } + } + }; + + var value = new DictionaryWithCallBacks(); + string json = JsonSerializer.Serialize(value, options); + Assert.Equal("{}", json); + + Assert.Equal(0, value.IsOnSerializingInvocations); + Assert.Equal(10, value.IsOnSerializedInvocations); + + value = JsonSerializer.Deserialize(json, options); + Assert.Equal(0, value.IsOnDeserializingInvocations); + Assert.Equal(7, value.IsOnDeserializedInvocations); + } + [Theory] [InlineData(typeof(int))] [InlineData(typeof(string))] [InlineData(typeof(object))] - [InlineData(typeof(List))] - [InlineData(typeof(Dictionary))] public static void SettingCallbacksOnUnsupportedTypes_ThrowsInvalidOperationException(Type type) { var jti = JsonTypeInfo.CreateJsonTypeInfo(type, new()); @@ -1390,6 +1510,32 @@ public static void SettingCallbacksOnUnsupportedTypes_ThrowsInvalidOperationExce Assert.Throws(() => jti.OnDeserialized = (obj => { })); } + [Theory] + [InlineData(typeof(ImmutableArray))] + [InlineData(typeof(ImmutableList))] + [InlineData(typeof(IImmutableList))] + [InlineData(typeof(ImmutableStack))] + [InlineData(typeof(IImmutableStack))] + [InlineData(typeof(ImmutableQueue))] + [InlineData(typeof(IImmutableQueue))] + [InlineData(typeof(ImmutableSortedSet))] + [InlineData(typeof(ImmutableHashSet))] + [InlineData(typeof(IImmutableSet))] + [InlineData(typeof(ImmutableDictionary))] + [InlineData(typeof(ImmutableSortedDictionary))] + [InlineData(typeof(IImmutableDictionary))] + [InlineData(typeof(string[]))] + [InlineData(typeof(Memory))] + [InlineData(typeof(ReadOnlyMemory))] + public static void SettingOnDeserializingCallbackOnImmutableTypes_ThrowsInvalidOperationException(Type type) + { + var jti = JsonTypeInfo.CreateJsonTypeInfo(type, new()); + + Assert.NotEqual(JsonTypeInfoKind.Object, jti.Kind); + Assert.Throws(() => jti.OnDeserializing = null); + Assert.Throws(() => jti.OnDeserializing = (obj => { })); + } + public class ClassWithCallBacks : IJsonOnSerializing, IJsonOnSerialized, IJsonOnDeserializing, IJsonOnDeserialized @@ -1409,6 +1555,44 @@ public class ClassWithCallBacks : public void OnDeserialized() => IsOnDeserializedInvocations++; } + public class CollectionWithCallBacks : List, + IJsonOnSerializing, IJsonOnSerialized, + IJsonOnDeserializing, IJsonOnDeserialized + { + [JsonIgnore] + public int IsOnSerializingInvocations { get; set; } + [JsonIgnore] + public int IsOnSerializedInvocations { get; set; } + [JsonIgnore] + public int IsOnDeserializingInvocations { get; set; } + [JsonIgnore] + public int IsOnDeserializedInvocations { get; set; } + + public void OnSerializing() => IsOnSerializingInvocations++; + public void OnSerialized() => IsOnSerializedInvocations++; + public void OnDeserializing() => IsOnDeserializingInvocations++; + public void OnDeserialized() => IsOnDeserializedInvocations++; + } + + public class DictionaryWithCallBacks : Dictionary, + IJsonOnSerializing, IJsonOnSerialized, + IJsonOnDeserializing, IJsonOnDeserialized + { + [JsonIgnore] + public int IsOnSerializingInvocations { get; set; } + [JsonIgnore] + public int IsOnSerializedInvocations { get; set; } + [JsonIgnore] + public int IsOnDeserializingInvocations { get; set; } + [JsonIgnore] + public int IsOnDeserializedInvocations { get; set; } + + public void OnSerializing() => IsOnSerializingInvocations++; + public void OnSerialized() => IsOnSerializedInvocations++; + public void OnDeserializing() => IsOnDeserializingInvocations++; + public void OnDeserialized() => IsOnDeserializedInvocations++; + } + [Theory] [InlineData(null)] [InlineData(JsonUnmappedMemberHandling.Skip)] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OnSerializeTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OnSerializeTests.cs index 506a8580ca557..dc92dc68fccbc 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OnSerializeTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OnSerializeTests.cs @@ -384,10 +384,122 @@ private class MyCollection : List, IJsonOnSerializing, IJsonOnSerialized { - public void OnDeserialized() => Assert.Fail("Not expected"); - public void OnDeserializing() => Assert.Fail("Not expected"); - public void OnSerialized() => Assert.Fail("Not expected"); - public void OnSerializing() => Assert.Fail("Not expected"); + internal int _onSerializingCount; + internal int _onSerializedCount; + internal int _onDeserializingCount; + internal int _onDeserializedCount; + + public void OnSerializing() + { + _onSerializingCount++; + Assert.Equal(1, this[0]); + this[0]++; + } + + public void OnSerialized() + { + Assert.Equal(1, _onSerializingCount); + _onSerializedCount++; + Assert.Equal(2, this[0]); + this[0]++; + } + + public void OnDeserializing() + { + _onDeserializingCount++; + Assert.Empty(this); + } + + public void OnDeserialized() + { + Assert.Equal(1, _onDeserializingCount); + _onDeserializedCount++; + Assert.Equal(1, this[0]); + this[0]++; + } + } + + [Fact] + public static void Test_MyCollection() + { + MyCollection obj = new() { 1 }; + + string json = JsonSerializer.Serialize(obj); + Assert.Equal("[2]", json); + Assert.Equal(3, obj[0]); + Assert.Equal(1, obj._onSerializingCount); + Assert.Equal(1, obj._onSerializedCount); + Assert.Equal(0, obj._onDeserializingCount); + Assert.Equal(0, obj._onDeserializedCount); + + obj = JsonSerializer.Deserialize("[1]"); + Assert.Equal(2, obj[0]); + Assert.Equal(0, obj._onSerializingCount); + Assert.Equal(0, obj._onSerializedCount); + Assert.Equal(1, obj._onDeserializingCount); + Assert.Equal(1, obj._onDeserializedCount); + } + + private class MyDictionary : Dictionary, + IJsonOnDeserializing, + IJsonOnDeserialized, + IJsonOnSerializing, + IJsonOnSerialized + { + internal int _onSerializingCount; + internal int _onSerializedCount; + internal int _onDeserializingCount; + internal int _onDeserializedCount; + + public void OnSerializing() + { + _onSerializingCount++; + Assert.Equal(1, this["A"]); + this["A"]++; + } + + public void OnSerialized() + { + Assert.Equal(1, _onSerializingCount); + _onSerializedCount++; + Assert.Equal(2, this["A"]); + this["A"]++; + } + + public void OnDeserializing() + { + _onDeserializingCount++; + Assert.Empty(this); + } + + public void OnDeserialized() + { + Assert.Equal(1, _onDeserializingCount); + _onDeserializedCount++; + Assert.Equal(1, this["A"]); + this["A"]++; + } + } + + [Fact] + public static void Test_MyDictionary() + { + MyDictionary obj = new() { ["A"] = 1 }; + + string json = JsonSerializer.Serialize(obj); + Assert.Equal("{\"A\":2}", json); + Assert.Equal(3, obj["A"]); + Assert.Equal(1, obj._onSerializingCount); + Assert.Equal(1, obj._onSerializedCount); + Assert.Equal(0, obj._onDeserializingCount); + Assert.Equal(0, obj._onDeserializedCount); + + obj = JsonSerializer.Deserialize("{\"A\":1}"); + Assert.Equal(2, obj["A"]); + Assert.Equal(0, obj._onSerializingCount); + Assert.Equal(0, obj._onSerializedCount); + Assert.Equal(1, obj._onDeserializingCount); + Assert.Equal(1, obj._onDeserializedCount); } [JsonConverter(converterType: typeof(MyValueConverter))] @@ -419,10 +531,7 @@ public override void Write(Utf8JsonWriter writer, MyValue value, JsonSerializerO [Fact] public static void NonPocosIgnored() { - JsonSerializer.Serialize(new MyCollection()); - JsonSerializer.Deserialize("[]"); JsonSerializer.Serialize(new MyValue()); - JsonSerializer.Deserialize("[]"); } } }