diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index e00a0ecfa63b29..811a4ea8365e6f 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -500,7 +500,13 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata) if (typeMetadata.GenerateSerializationLogic) { - serializeFuncSource = GenerateFastPathFuncForObject(typeCompilableName, serializeMethodName, typeMetadata.CanBeNull, properties); + serializeFuncSource = GenerateFastPathFuncForObject( + typeCompilableName, + serializeMethodName, + typeMetadata.CanBeNull, + typeMetadata.ImplementsIJsonOnSerialized, + typeMetadata.ImplementsIJsonOnSerializing, + properties); serializeFuncNamedArg = $@"serializeFunc: {serializeMethodName}"; } else @@ -635,6 +641,8 @@ private string GenerateFastPathFuncForObject( string typeInfoTypeRef, string serializeMethodName, bool canBeNull, + bool implementsIJsonOnSerialized, + bool implementsIJsonOnSerializing, List? properties) { JsonSourceGenerationOptionsAttribute options = _currentContext.GenerationOptions; @@ -646,6 +654,12 @@ private string GenerateFastPathFuncForObject( StringBuilder sb = new(); // Begin method definition + if (implementsIJsonOnSerializing) + { + sb.Append($@"(({IJsonOnSerializingFullName}){ValueVarName}).OnSerializing();"); + sb.Append($@"{Environment.NewLine} "); + } + sb.Append($@"{WriterVarName}.WriteStartObject();"); if (properties != null) @@ -733,6 +747,12 @@ private string GenerateFastPathFuncForObject( {WriterVarName}.WriteEndObject();"); + if (implementsIJsonOnSerialized) + { + sb.Append($@"{Environment.NewLine} "); + sb.Append($@"(({IJsonOnSerializedFullName}){ValueVarName}).OnSerialized();"); + }; + return GenerateFastPathFuncForType(serializeMethodName, typeInfoTypeRef, sb.ToString(), canBeNull); } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 386a79914cf20e..70c8a412b6e59d 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -494,6 +494,9 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener bool foundDesignTimeCustomConverter = false; string? converterInstatiationLogic = null; + bool implementsIJsonOnSerialized = false; + bool implementsIJsonOnSerializing = false; + IList attributeDataList = CustomAttributeData.GetCustomAttributes(type); foreach (CustomAttributeData attributeData in attributeDataList) { @@ -577,6 +580,11 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener constructionStrategy = ObjectConstructionStrategy.ParameterlessConstructor; } + // GetInterface() is currently not implemented, so we use GetInterfaces(). + IEnumerable interfaces = type.GetInterfaces().Select(interfaceType => interfaceType.FullName); + implementsIJsonOnSerialized = interfaces.FirstOrDefault(interfaceName => interfaceName == IJsonOnSerializedFullName) != null; + implementsIJsonOnSerializing = interfaces.FirstOrDefault(interfaceName => interfaceName == IJsonOnSerializingFullName) != null; + for (Type? currentType = type; currentType != null; currentType = currentType.BaseType) { const BindingFlags bindingFlags = @@ -627,7 +635,9 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener collectionValueTypeMetadata: collectionValueType != null ? GetOrAddTypeGenerationSpec(collectionValueType, generationMode) : null, constructionStrategy, nullableUnderlyingTypeMetadata: nullableUnderlyingType != null ? GetOrAddTypeGenerationSpec(nullableUnderlyingType, generationMode) : null, - converterInstatiationLogic); + converterInstatiationLogic, + implementsIJsonOnSerialized, + implementsIJsonOnSerializing); return typeMetadata; } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs index 0966d4eed8094e..7e8c45ff4299ce 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs @@ -18,6 +18,10 @@ namespace System.Text.Json.SourceGeneration [Generator] public sealed partial class JsonSourceGenerator : ISourceGenerator { + private const string SystemTextJsonSourceGenerationName = "System.Text.Json.SourceGeneration"; + private const string IJsonOnSerializedFullName = "System.Text.Json.Serialization.IJsonOnSerialized"; + private const string IJsonOnSerializingFullName = "System.Text.Json.Serialization.IJsonOnSerializing"; + /// /// Registers a syntax resolver to receive compilation units. /// @@ -51,8 +55,6 @@ public void Execute(GeneratorExecutionContext executionContext) } } - private const string SystemTextJsonSourceGenerationName = "System.Text.Json.SourceGeneration"; - /// /// Helper for unit tests. /// diff --git a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj index 176138ce41f2ac..3e449e71aab1a8 100644 --- a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj +++ b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 false diff --git a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs index 0d457cd01b1969..36a0c58a17a315 100644 --- a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs @@ -33,6 +33,9 @@ internal class TypeGenerationSpec public ClassType ClassType { get; private set; } + public bool ImplementsIJsonOnSerialized { get; private set; } + public bool ImplementsIJsonOnSerializing { get; private set; } + public bool IsValueType { get; private set; } public bool CanBeNull { get; private set; } @@ -67,7 +70,9 @@ public void Initialize( TypeGenerationSpec? collectionValueTypeMetadata, ObjectConstructionStrategy constructionStrategy, TypeGenerationSpec? nullableUnderlyingTypeMetadata, - string? converterInstantiationLogic) + string? converterInstantiationLogic, + bool implementsIJsonOnSerialized, + bool implementsIJsonOnSerializing) { GenerationMode = generationMode; TypeRef = $"global::{typeRef}"; @@ -84,6 +89,8 @@ public void Initialize( ConstructionStrategy = constructionStrategy; NullableUnderlyingTypeMetadata = nullableUnderlyingTypeMetadata; ConverterInstantiationLogic = converterInstantiationLogic; + ImplementsIJsonOnSerialized = implementsIJsonOnSerialized; + ImplementsIJsonOnSerializing = implementsIJsonOnSerializing; } private bool FastPathIsSupported() diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 67a09866d7445e..2d74d3535c3fc6 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -733,6 +733,22 @@ public abstract partial class JsonValue : System.Text.Json.Nodes.JsonNode } namespace System.Text.Json.Serialization { + public partial interface IJsonOnDeserialized + { + void OnDeserialized(); + } + public partial interface IJsonOnDeserializing + { + void OnDeserializing(); + } + public partial interface IJsonOnSerialized + { + void OnSerialized(); + } + public partial interface IJsonOnSerializing + { + void OnSerializing(); + } public abstract partial class JsonAttribute : System.Attribute { protected JsonAttribute() { } diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 190cbd485342a8..9d94ae9bdd4f4e 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -92,6 +92,10 @@ + + + + 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 89b9cdad1eeffa..2f9bc99c6fa418 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 @@ -36,6 +36,11 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, obj = jsonTypeInfo.CreateObject!()!; + if (obj is IJsonOnDeserializing onDeserializing) + { + onDeserializing.OnDeserializing(); + } + // Process all properties. while (true) { @@ -108,6 +113,11 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, obj = jsonTypeInfo.CreateObject!()!; + if (obj is IJsonOnDeserializing onDeserializing) + { + onDeserializing.OnDeserializing(); + } + state.Current.ReturnValue = obj; state.Current.ObjectState = StackFrameObjectState.CreatedObject; } @@ -216,14 +226,21 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, } } + if (obj is IJsonOnDeserialized onDeserialized) + { + onDeserialized.OnDeserialized(); + } + + // Unbox + Debug.Assert(obj != null); + value = (T)obj; + // Check if we are trying to build the sorted cache. if (state.Current.PropertyRefCache != null) { jsonTypeInfo.UpdateSortedPropertyCache(ref state.Current); } - value = (T)obj; - return true; } @@ -235,20 +252,24 @@ internal sealed override bool OnTryWrite( { JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; - // Minimize boxing for structs by only boxing once here - object objectValue = value!; + object obj = value; // box once if (!state.SupportContinuation) { writer.WriteStartObject(); if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve) { - if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref) + if (JsonSerializer.WriteReferenceForObject(this, obj, ref state, writer) == MetadataPropertyName.Ref) { return true; } } + if (obj is IJsonOnSerializing onSerializing) + { + onSerializing.OnSerializing(); + } + List> properties = state.Current.JsonTypeInfo.PropertyCache!.List; for (int i = 0; i < properties.Count; i++) { @@ -259,7 +280,7 @@ internal sealed override bool OnTryWrite( state.Current.DeclaredJsonPropertyInfo = jsonPropertyInfo; state.Current.NumberHandling = jsonPropertyInfo.NumberHandling; - bool success = jsonPropertyInfo.GetMemberAndWriteJson(objectValue, ref state, writer); + bool success = jsonPropertyInfo.GetMemberAndWriteJson(obj, ref state, writer); // Converters only return 'false' when out of data which is not possible in fast path. Debug.Assert(success); @@ -275,14 +296,13 @@ internal sealed override bool OnTryWrite( state.Current.DeclaredJsonPropertyInfo = dataExtensionProperty; state.Current.NumberHandling = dataExtensionProperty.NumberHandling; - bool success = dataExtensionProperty.GetMemberAndWriteJsonExtensionData(objectValue, ref state, writer); + bool success = dataExtensionProperty.GetMemberAndWriteJsonExtensionData(obj, ref state, writer); Debug.Assert(success); state.Current.EndProperty(); } writer.WriteEndObject(); - return true; } else { @@ -291,12 +311,17 @@ internal sealed override bool OnTryWrite( writer.WriteStartObject(); if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve) { - if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref) + if (JsonSerializer.WriteReferenceForObject(this, obj, ref state, writer) == MetadataPropertyName.Ref) { return true; } } + if (obj is IJsonOnSerializing onSerializing) + { + onSerializing.OnSerializing(); + } + state.Current.ProcessedStartToken = true; } @@ -310,7 +335,7 @@ internal sealed override bool OnTryWrite( state.Current.DeclaredJsonPropertyInfo = jsonPropertyInfo; state.Current.NumberHandling = jsonPropertyInfo.NumberHandling; - if (!jsonPropertyInfo.GetMemberAndWriteJson(objectValue!, ref state, writer)) + if (!jsonPropertyInfo.GetMemberAndWriteJson(obj!, ref state, writer)) { Debug.Assert(jsonPropertyInfo.ConverterBase.ConverterStrategy != ConverterStrategy.Value || jsonPropertyInfo.ConverterBase.TypeToConvert == JsonTypeInfo.ObjectType); @@ -342,7 +367,7 @@ internal sealed override bool OnTryWrite( state.Current.DeclaredJsonPropertyInfo = dataExtensionProperty; state.Current.NumberHandling = dataExtensionProperty.NumberHandling; - if (!dataExtensionProperty.GetMemberAndWriteJsonExtensionData(objectValue, ref state, writer)) + if (!dataExtensionProperty.GetMemberAndWriteJsonExtensionData(obj, ref state, writer)) { return false; } @@ -366,9 +391,16 @@ internal sealed override bool OnTryWrite( state.Current.ProcessedEndToken = true; writer.WriteEndObject(); } + } - return true; + if (obj is IJsonOnSerialized onSerialized) + { + onSerialized.OnSerialized(); } + + value = (T)obj; // unbox + + return true; } // AggressiveInlining since this method is only called from two locations and is on a hot path. @@ -437,6 +469,11 @@ internal sealed override void CreateInstanceForReferenceResolver(ref Utf8JsonRea object obj = state.Current.JsonTypeInfo.CreateObject!()!; state.Current.ReturnValue = obj; + + if (obj is IJsonOnDeserializing onDeserializing) + { + onDeserializing.OnDeserializing(); + } } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs index c202157a0d3131..867b74f2996bfc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs @@ -32,7 +32,12 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo ReadConstructorArguments(ref state, ref reader, options); - obj = CreateObject(ref state.Current); + obj = (T)CreateObject(ref state.Current); + + if (obj is IJsonOnDeserializing onDeserializing) + { + onDeserializing.OnDeserializing(); + } if (argumentState.FoundPropertyCount > 0) { @@ -91,7 +96,12 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo return false; } - obj = CreateObject(ref state.Current); + obj = (T)CreateObject(ref state.Current); + + if (obj is IJsonOnDeserializing onDeserializing) + { + onDeserializing.OnDeserializing(); + } if (argumentState.FoundPropertyCount > 0) { @@ -128,6 +138,17 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo } } + if (obj is IJsonOnDeserialized onDeserialized) + { + onDeserialized.OnDeserialized(); + } + + EndRead(ref state); + + // Unbox + Debug.Assert(obj != null); + value = (T)obj; + // Check if we are trying to build the sorted cache. if (state.Current.PropertyRefCache != null) { @@ -140,10 +161,6 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo state.Current.JsonTypeInfo.UpdateSortedParameterCache(ref state.Current); } - EndRead(ref state); - - value = (T)obj; - return true; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/IJsonOnDeserialized.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/IJsonOnDeserialized.cs new file mode 100644 index 00000000000000..441d84861ae42f --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/IJsonOnDeserialized.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization +{ + /// + /// Specifies that the JSON type should have its method called after deserialization occurs. + /// + /// + /// This behavior is only supported on types representing JSON objects. + /// Types that have a custom converter or represent either collections or primitive values do not support this behavior. + /// + public interface IJsonOnDeserialized + { + /// + /// The method that is called after deserialization. + /// + void OnDeserialized(); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/IJsonOnDeserializing.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/IJsonOnDeserializing.cs new file mode 100644 index 00000000000000..80d15d8c37d9fa --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/IJsonOnDeserializing.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization +{ + /// + /// Specifies that the type should have its method called before deserialization occurs. + /// + /// + /// This behavior is only supported on types representing JSON objects. + /// Types that have a custom converter or represent either collections or primitive values do not support this behavior. + /// + public interface IJsonOnDeserializing + { + /// + /// The method that is called before deserialization. + /// + void OnDeserializing(); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/IJsonOnSerialized.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/IJsonOnSerialized.cs new file mode 100644 index 00000000000000..d69eadbef5c79b --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/IJsonOnSerialized.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization +{ + /// + /// Specifies that the type should have its method called after serialization occurs. + /// + /// + /// This behavior is only supported on types representing JSON objects. + /// Types that have a custom converter or represent either collections or primitive values do not support this behavior. + /// + public interface IJsonOnSerialized + { + /// + /// The method that is called after serialization. + /// + void OnSerialized(); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/IJsonOnSerializing.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/IJsonOnSerializing.cs new file mode 100644 index 00000000000000..39508322b2c6de --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/IJsonOnSerializing.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization +{ + /// + /// Specifies that the type should have its method called before serialization occurs. + /// + /// + /// This behavior is only supported on types representing JSON objects. + /// Types that have a custom converter or represent either collections or primitive values do not support this behavior. + /// + public interface IJsonOnSerializing + { + /// + /// The method that is called before serialization. + /// + void OnSerializing(); + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs index e7cde2239aaf82..d07a96cd6f2b27 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs @@ -19,6 +19,7 @@ public interface ITestContext public JsonTypeInfo HighLowTemps { get; } public JsonTypeInfo MyType { get; } public JsonTypeInfo MyType2 { get; } + public JsonTypeInfo MyTypeWithCallbacks { get; } public JsonTypeInfo MyIntermediateType { get; } public JsonTypeInfo HighLowTempsImmutable { get; } public JsonTypeInfo MyNestedClass { get; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs index a5ffbfbd3514fb..3c67d58bfd3277 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs @@ -17,6 +17,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(HighLowTemps))] [JsonSerializable(typeof(MyType))] [JsonSerializable(typeof(MyType2))] + [JsonSerializable(typeof(MyTypeWithCallbacks))] [JsonSerializable(typeof(MyIntermediateType))] [JsonSerializable(typeof(HighLowTempsImmutable))] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass))] @@ -45,6 +46,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.NotNull(MetadataAndSerializationContext.Default.HighLowTemps.Serialize); Assert.NotNull(MetadataAndSerializationContext.Default.MyType.Serialize); Assert.NotNull(MetadataAndSerializationContext.Default.MyType2.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.MyTypeWithCallbacks.Serialize); Assert.NotNull(MetadataAndSerializationContext.Default.MyIntermediateType.Serialize); Assert.NotNull(MetadataAndSerializationContext.Default.HighLowTempsImmutable.Serialize); Assert.NotNull(MetadataAndSerializationContext.Default.MyNestedClass.Serialize); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs index 0bb57ab5721e94..0b253d2ec64f96 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs @@ -16,6 +16,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(HighLowTemps), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(MyTypeWithCallbacks), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Metadata)] @@ -65,6 +66,7 @@ public override void EnsureFastPathGeneratedAsExpected() [JsonSerializable(typeof(HighLowTemps))] [JsonSerializable(typeof(MyType))] [JsonSerializable(typeof(MyType2))] + [JsonSerializable(typeof(MyTypeWithCallbacks))] [JsonSerializable(typeof(MyIntermediateType))] [JsonSerializable(typeof(HighLowTempsImmutable))] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass))] @@ -93,6 +95,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MetadataContext.Default.HighLowTemps.Serialize); Assert.Null(MetadataContext.Default.MyType.Serialize); Assert.Null(MetadataContext.Default.MyType2.Serialize); + Assert.Null(MetadataContext.Default.MyTypeWithCallbacks.Serialize); Assert.Null(MetadataContext.Default.MyIntermediateType.Serialize); Assert.Null(MetadataContext.Default.HighLowTempsImmutable.Serialize); Assert.Null(MetadataContext.Default.MyNestedClass.Serialize); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs index f8317b590ec283..b4f64091ca5785 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs @@ -16,6 +16,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(HighLowTemps), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.Default)] [JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyTypeWithCallbacks), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] @@ -43,6 +44,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.NotNull(MixedModeContext.Default.HighLowTemps.Serialize); Assert.NotNull(MixedModeContext.Default.MyType.Serialize); Assert.NotNull(MixedModeContext.Default.MyType2.Serialize); + Assert.NotNull(MixedModeContext.Default.MyTypeWithCallbacks.Serialize); Assert.NotNull(MixedModeContext.Default.MyIntermediateType.Serialize); Assert.Null(MixedModeContext.Default.HighLowTempsImmutable.Serialize); Assert.NotNull(MixedModeContext.Default.MyNestedClass.Serialize); @@ -165,5 +167,24 @@ public override void SerializeObjectArray_WithCustomOptions() VerifyIndexViewModel(index, JsonSerializer.Deserialize(indexAsJsonElement.GetRawText(), metadataContext.IndexViewModel)); VerifyCampaignSummaryViewModel(campaignSummary, JsonSerializer.Deserialize(campaignSummeryAsJsonElement.GetRawText(), metadataContext.CampaignSummaryViewModel)); } + + [Fact] + public void OnSerializeCallbacks_WithCustomOptions() + { + MyTypeWithCallbacks obj = new(); + Assert.Null(obj.MyProperty); + + ITestContext context = SerializationContextWithCamelCase.Default; + Assert.Same(JsonNamingPolicy.CamelCase, ((JsonSerializerContext)context).Options.PropertyNamingPolicy); + + string json = JsonSerializer.Serialize(obj, context.MyTypeWithCallbacks); + Assert.Equal("{\"myProperty\":\"Before\"}", json); + Assert.Equal("After", obj.MyProperty); + + context = new MetadataContext(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + json = JsonSerializer.Serialize(obj, context.MyTypeWithCallbacks); + Assert.Equal("{\"myProperty\":\"Before\"}", json); + Assert.Equal("After", obj.MyProperty); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs index 02567c4b8ae3b9..e58856ee97ae95 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs @@ -17,6 +17,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(HighLowTemps))] [JsonSerializable(typeof(MyType))] [JsonSerializable(typeof(MyType2))] + [JsonSerializable(typeof(MyTypeWithCallbacks))] [JsonSerializable(typeof(MyIntermediateType))] [JsonSerializable(typeof(HighLowTempsImmutable))] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass))] @@ -38,6 +39,7 @@ internal partial class SerializationContext : JsonSerializerContext, ITestContex [JsonSerializable(typeof(HighLowTemps), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyTypeWithCallbacks), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] @@ -60,6 +62,7 @@ internal partial class SerializationWithPerTypeAttributeContext : JsonSerializer [JsonSerializable(typeof(HighLowTemps), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyTypeWithCallbacks), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] @@ -93,6 +96,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.NotNull(SerializationContext.Default.HighLowTemps.Serialize); Assert.NotNull(SerializationContext.Default.MyType.Serialize); Assert.NotNull(SerializationContext.Default.MyType2.Serialize); + Assert.NotNull(SerializationContext.Default.MyTypeWithCallbacks.Serialize); Assert.NotNull(SerializationContext.Default.MyIntermediateType.Serialize); Assert.NotNull(SerializationContext.Default.HighLowTempsImmutable.Serialize); Assert.NotNull(SerializationContext.Default.MyNestedClass.Serialize); @@ -307,6 +311,17 @@ public override void ParameterizedConstructor() JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.HighLowTempsImmutable), typeof(HighLowTempsImmutable)); } + + [Fact] + public void OnSerializeCallbacks() + { + MyTypeWithCallbacks obj = new(); + Assert.Null(obj.MyProperty); + + string json = JsonSerializer.Serialize(obj, DefaultContext.MyTypeWithCallbacks); + Assert.Equal("{\"MyProperty\":\"Before\"}", json); + Assert.Equal("After", obj.MyProperty); + } } public sealed class SerializationWithPerTypeAttributeContextTests : SerializationContextTests diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs index 0f1f6c68dca884..e43e1bb2b9e138 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Text.Json.Serialization; namespace System.Text.Json.SourceGeneration.Tests.RepeatedTypes { @@ -108,6 +109,14 @@ public class MyIntermediateType public MyType Type = new(); } + public class MyTypeWithCallbacks : IJsonOnSerializing, IJsonOnSerialized + { + public string MyProperty { get; set; } + + public void OnSerializing() => MyProperty = "Before"; + void IJsonOnSerialized.OnSerialized() => MyProperty = "After"; + } + public class JsonMessage { public string Message { get; set; } 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 new file mode 100644 index 00000000000000..32b8374b9741c6 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OnSerializeTests.cs @@ -0,0 +1,428 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static partial class OnSerializeTests + { + private class MyClass : + IJsonOnDeserializing, + IJsonOnDeserialized, + IJsonOnSerializing, + IJsonOnSerialized + { + public int MyInt { get; set; } + + internal int _onSerializingCount; + internal int _onSerializedCount; + internal int _onDeserializingCount; + internal int _onDeserializedCount; + + public void OnSerializing() + { + _onSerializingCount++; + Assert.Equal(1, MyInt); + MyInt++; + } + + public void OnSerialized() + { + Assert.Equal(1, _onSerializingCount); + _onSerializedCount++; + Assert.Equal(2, MyInt); + MyInt++; + } + + public void OnDeserializing() + { + _onDeserializingCount++; + Assert.Equal(0, MyInt); + MyInt = 100; // Gets replaced by the serializer. + } + + public void OnDeserialized() + { + Assert.Equal(1, _onDeserializingCount); + _onDeserializedCount++; + Assert.Equal(1, MyInt); + MyInt++; + } + } + + [Fact] + public static void Test_MyClass() + { + MyClass obj = new(); + obj.MyInt = 1; + + string json = JsonSerializer.Serialize(obj); + Assert.Equal("{\"MyInt\":2}", json); + Assert.Equal(3, obj.MyInt); + Assert.Equal(1, obj._onSerializingCount); + Assert.Equal(1, obj._onSerializedCount); + Assert.Equal(0, obj._onDeserializingCount); + Assert.Equal(0, obj._onDeserializedCount); + + obj = JsonSerializer.Deserialize("{\"MyInt\":1}"); + Assert.Equal(2, obj.MyInt); + Assert.Equal(0, obj._onSerializingCount); + Assert.Equal(0, obj._onSerializedCount); + Assert.Equal(1, obj._onDeserializingCount); + Assert.Equal(1, obj._onDeserializedCount); + } + + private struct MyStruct : + IJsonOnDeserializing, + IJsonOnDeserialized, + IJsonOnSerializing, + IJsonOnSerialized + { + public int MyInt { get; set; } + + internal int _onSerializingCount; + internal int _onSerializedCount; + internal int _onDeserializingCount; + internal int _onDeserializedCount; + + public void OnSerializing() + { + _onSerializingCount++; + Assert.Equal(1, MyInt); + MyInt++; + } + + public void OnSerialized() + { + Assert.Equal(1, _onSerializingCount); + _onSerializedCount++; + MyInt++; // should not affect serialization + } + + public void OnDeserializing() + { + Assert.Equal(0, MyInt); + _onDeserializingCount++; + MyInt = 100; // Gets replaced by the serializer. + } + + public void OnDeserialized() + { + Assert.Equal(1, _onDeserializingCount); + Assert.Equal(1, MyInt); + _onDeserializedCount++; + } + } + + + [Fact] + public static void Test_MyStruct() + { + MyStruct obj = new(); + obj.MyInt = 1; + + string json = JsonSerializer.Serialize(obj); + Assert.Equal("{\"MyInt\":2}", json); + + // Although the OnSerialize* callbacks are invoked, a struct is passed to the serializer byvalue. + Assert.Equal(0, obj._onSerializingCount); + Assert.Equal(0, obj._onSerializedCount); + + Assert.Equal(0, obj._onDeserializingCount); + Assert.Equal(0, obj._onDeserializedCount); + + obj = JsonSerializer.Deserialize("{\"MyInt\":1}"); + Assert.Equal(1, obj.MyInt); + Assert.Equal(0, obj._onSerializingCount); + Assert.Equal(0, obj._onSerializedCount); + Assert.Equal(1, obj._onDeserializingCount); + Assert.Equal(1, obj._onDeserializedCount); + } + + private class MyClassWithSmallConstructor : + IJsonOnDeserializing, + IJsonOnDeserialized, + IJsonOnSerializing, + IJsonOnSerialized + { + public int MyInt { get; set; } + + public MyClassWithSmallConstructor(int myInt) + { + MyInt = myInt; + _constructorCalled = true; + } + + internal bool _constructorCalled; + internal int _onSerializingCount; + internal int _onSerializedCount; + internal int _onDeserializingCount; + internal int _onDeserializedCount; + + public void OnSerializing() + { + _onSerializingCount++; + Assert.Equal(1, MyInt); + MyInt++; + } + + public void OnSerialized() + { + Assert.Equal(1, _onSerializingCount); + _onSerializedCount++; + Assert.Equal(2, MyInt); + MyInt++; + } + + public void OnDeserializing() + { + _onDeserializingCount++; + Assert.Equal(1, MyInt); + MyInt++; // Does not get replaced by the serializer since it was passed into the ctor. + } + + public void OnDeserialized() + { + Assert.Equal(1, _onDeserializingCount); + _onDeserializedCount++; + Assert.Equal(2, MyInt); + MyInt++; + } + } + + [Fact] + public static void Test_MyClassWithSmallConstructor() + { + MyClassWithSmallConstructor obj = new(1); + Assert.Equal(1, obj.MyInt); + + string json = JsonSerializer.Serialize(obj); + Assert.Equal("{\"MyInt\":2}", json); + Assert.Equal(3, obj.MyInt); + Assert.True(obj._constructorCalled); + Assert.Equal(1, obj._onSerializingCount); + Assert.Equal(1, obj._onSerializedCount); + Assert.Equal(0, obj._onDeserializingCount); + Assert.Equal(0, obj._onDeserializedCount); + + obj = JsonSerializer.Deserialize("{\"MyInt\":1}"); + Assert.True(obj._constructorCalled); + Assert.Equal(3, obj.MyInt); + Assert.Equal(0, obj._onSerializingCount); + Assert.Equal(0, obj._onSerializedCount); + Assert.Equal(1, obj._onDeserializingCount); + Assert.Equal(1, obj._onDeserializedCount); + } + + private class MyClassWithLargeConstructor : + IJsonOnDeserializing, + IJsonOnDeserialized, + IJsonOnSerializing, + IJsonOnSerialized + { + public int MyInt1 { get; set; } + public int MyInt2 { get; set; } + public int MyInt3 { get; set; } + public int MyInt4 { get; set; } + public int MyInt5 { get; set; } + + public MyClassWithLargeConstructor(int myInt1, int myInt2, int myInt3, int myInt4, int myInt5) + { + MyInt1 = myInt1; + MyInt2 = myInt2; + MyInt3 = myInt3; + MyInt4 = myInt4; + MyInt5 = myInt5; + _constructorCalled = true; + } + + internal bool _constructorCalled; + internal int _onSerializingCount; + internal int _onSerializedCount; + internal int _onDeserializingCount; + internal int _onDeserializedCount; + + public void OnSerializing() + { + _onSerializingCount++; + Assert.Equal(1, MyInt1); + MyInt1++; + } + + public void OnSerialized() + { + Assert.Equal(1, _onSerializingCount); + _onSerializedCount++; + Assert.Equal(2, MyInt1); + MyInt1++; + } + + public void OnDeserializing() + { + _onDeserializingCount++; + Assert.Equal(1, MyInt1); + MyInt1++; // Does not get replaced by the serializer since it was passed into the ctor. + } + + public void OnDeserialized() + { + Assert.Equal(1, _onDeserializingCount); + _onDeserializedCount++; + Assert.Equal(2, MyInt1); + MyInt1++; + } + } + + [Fact] + public static void Test_MyClassWithLargeConstructor() + { + const string Json = "{\"MyInt1\":1,\"MyInt2\":2,\"MyInt3\":3,\"MyInt4\":4,\"MyInt5\":5}"; + + MyClassWithLargeConstructor obj = new(1, 2, 3, 4, 5); + Assert.Equal(1, obj.MyInt1); + Assert.Equal(2, obj.MyInt2); + Assert.Equal(3, obj.MyInt3); + Assert.Equal(4, obj.MyInt4); + Assert.Equal(5, obj.MyInt5); + + string json = JsonSerializer.Serialize(obj); + Assert.Contains("\"MyInt1\":2", json); + Assert.Equal(3, obj.MyInt1); // Is updated in the callback + Assert.True(obj._constructorCalled); + Assert.Equal(1, obj._onSerializingCount); + Assert.Equal(1, obj._onSerializedCount); + Assert.Equal(0, obj._onDeserializingCount); + Assert.Equal(0, obj._onDeserializedCount); + + obj = JsonSerializer.Deserialize(Json); + Assert.True(obj._constructorCalled); + Assert.Equal(3, obj.MyInt1); + Assert.Equal(0, obj._onSerializingCount); + Assert.Equal(0, obj._onSerializedCount); + Assert.Equal(1, obj._onDeserializingCount); + Assert.Equal(1, obj._onDeserializedCount); + } + + private class MyCyclicClass : + IJsonOnDeserializing, + IJsonOnDeserialized, + IJsonOnSerializing, + IJsonOnSerialized + { + public int MyInt { get; set; } + public MyCyclicClass Cycle { get; set; } + + internal int _onSerializingCount; + internal int _onSerializedCount; + internal int _onDeserializingCount; + internal int _onDeserializedCount; + + public void OnSerializing() + { + _onSerializingCount++; + Assert.Equal(1, MyInt); + MyInt++; + } + + public void OnSerialized() + { + Assert.Equal(1, _onSerializingCount); + _onSerializedCount++; + Assert.Equal(2, MyInt); + MyInt++; + } + + public void OnDeserializing() + { + _onDeserializingCount++; + Assert.Equal(0, MyInt); + MyInt = 100; // Gets replaced by the serializer. + } + + public void OnDeserialized() + { + Assert.Equal(1, _onDeserializingCount); + _onDeserializedCount++; + Assert.Equal(1, MyInt); + MyInt++; + } + } + + [Fact] + public static void Test_MyCyclicClass() + { + const string Json = "{\"$id\":\"1\",\"MyInt\":1,\"Cycle\":{\"$ref\":\"1\"}}"; + + MyCyclicClass obj = new(); + obj.MyInt = 1; + obj.Cycle = obj; + + JsonSerializerOptions options = new(); + options.ReferenceHandler = ReferenceHandler.Preserve; + + string json = JsonSerializer.Serialize(obj, options); + Assert.Contains("\"MyInt\":2", json); + Assert.Equal(1, obj._onSerializingCount); + Assert.Equal(1, obj._onSerializedCount); + Assert.Equal(0, obj._onDeserializingCount); + Assert.Equal(0, obj._onDeserializedCount); + + obj = JsonSerializer.Deserialize(Json, options); + Assert.Equal(2, obj.MyInt); + Assert.Equal(obj, obj.Cycle); + Assert.Equal(0, obj._onSerializingCount); + Assert.Equal(0, obj._onSerializedCount); + Assert.Equal(1, obj._onDeserializingCount); + Assert.Equal(1, obj._onDeserializedCount); + } + + private class MyCollection : List, + IJsonOnDeserializing, + IJsonOnDeserialized, + IJsonOnSerializing, + IJsonOnSerialized + { + public void OnDeserialized() => Assert.True(false, "Not expected"); + public void OnDeserializing() => Assert.True(false, "Not expected"); + public void OnSerialized() => Assert.True(false, "Not expected"); + public void OnSerializing() => Assert.True(false, "Not expected"); + } + + [JsonConverter(converterType: typeof(MyValueConverter))] + private class MyValue : + IJsonOnDeserializing, + IJsonOnDeserialized, + IJsonOnSerializing, + IJsonOnSerialized + { + public void OnDeserialized() => Assert.True(false, "Not expected"); + public void OnDeserializing() => Assert.True(false, "Not expected"); + public void OnSerialized() => Assert.True(false, "Not expected"); + public void OnSerializing() => Assert.True(false, "Not expected"); + } + + private class MyValueConverter : JsonConverter + { + public override MyValue? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new MyValue(); + } + + public override void Write(Utf8JsonWriter writer, MyValue value, JsonSerializerOptions options) + { + writer.WriteStringValue("dummy"); + } + } + + [Fact] + public static void NonPocosIgnored() + { + JsonSerializer.Serialize(new MyCollection()); + JsonSerializer.Deserialize("[]"); + JsonSerializer.Serialize(new MyValue()); + JsonSerializer.Deserialize("[]"); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index d824810851a3f6..3ba9831cb6db03 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);net461 true @@ -140,6 +140,7 @@ +