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 ed8880c117feae..96829aebc3267f 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -267,10 +267,12 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { } public bool IncludeFields { get { throw null; } set { } } public int MaxDepth { get { throw null; } set { } } public System.Text.Json.Serialization.JsonNumberHandling NumberHandling { get { throw null; } set { } } + public System.Func SupportedPolymorphicTypes { get { throw null; } set { } } public bool PropertyNameCaseInsensitive { get { throw null; } set { } } public System.Text.Json.JsonNamingPolicy? PropertyNamingPolicy { get { throw null; } set { } } public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } } public System.Text.Json.Serialization.ReferenceHandler? ReferenceHandler { get { throw null; } set { } } + public System.Collections.Generic.IList TypeDiscriminatorConfigurations { get { throw null; } } public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } } public bool WriteIndented { get { throw null; } set { } } public void AddContext() where TContext : System.Text.Json.Serialization.JsonSerializerContext, new() { } @@ -796,6 +798,11 @@ public sealed partial class JsonNumberHandlingAttribute : System.Text.Json.Seria public JsonNumberHandlingAttribute(System.Text.Json.Serialization.JsonNumberHandling handling) { } public System.Text.Json.Serialization.JsonNumberHandling Handling { get { throw null; } } } + [System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] + public sealed partial class JsonPolymorphicTypeAttribute : System.Text.Json.Serialization.JsonAttribute + { + public JsonPolymorphicTypeAttribute() { } + } [System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple=false)] public sealed partial class JsonPropertyNameAttribute : System.Text.Json.Serialization.JsonAttribute { @@ -846,6 +853,27 @@ public enum JsonUnknownTypeHandling JsonElement = 0, JsonNode = 1, } + [System.AttributeUsageAttribute(System.AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] + public partial class JsonKnownTypeAttribute : System.Text.Json.Serialization.JsonAttribute + { + public JsonKnownTypeAttribute(System.Type subtype, string identifier) { } + public string Identifier { get { throw null; } } + public System.Type Subtype { get { throw null; } } + } + public partial class TypeDiscriminatorConfiguration : System.Collections.Generic.IEnumerable>, System.Collections.Generic.IReadOnlyCollection>, System.Collections.IEnumerable + { + public TypeDiscriminatorConfiguration(System.Type baseType) { } + public System.Type BaseType { get { throw null; } } + int System.Collections.Generic.IReadOnlyCollection>.Count { get { throw null; } } + System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public System.Text.Json.Serialization.TypeDiscriminatorConfiguration WithKnownType(System.Type derivedType, string identifier) { throw null; } + } + public partial class TypeDiscriminatorConfiguration : System.Text.Json.Serialization.TypeDiscriminatorConfiguration where TBaseType : class + { + public TypeDiscriminatorConfiguration() : base(default(System.Type)) { } + public System.Text.Json.Serialization.TypeDiscriminatorConfiguration WithKnownType(string identifier) where TDerivedType : TBaseType { throw null; } + } public abstract partial class ReferenceHandler { protected ReferenceHandler() { } 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 e3ba73d6b6c23b..ab7e83b378f0ec 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -88,19 +88,24 @@ + + + + + - + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonKnownTypeAttribute.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonKnownTypeAttribute.cs new file mode 100644 index 00000000000000..5d3d4236ff9308 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonKnownTypeAttribute.cs @@ -0,0 +1,34 @@ +// 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 +{ + /// + /// When placed on a type, indicates that the specified subtype should + /// be serialized polymorphically using type discriminator identifiers. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] + public class JsonKnownTypeAttribute : JsonAttribute + { + /// + /// Initializes a new attribute with specified parameters. + /// + /// The known subtype that should be serialized polymorphically. + /// The string identifier to be used for the serialization of the subtype. + public JsonKnownTypeAttribute(Type subtype, string identifier) + { + Subtype = subtype; + Identifier = identifier; + } + + /// + /// The known subtype that should be serialized polymorphically. + /// + public Type Subtype { get; } + + /// + /// The string identifier to be used for the serialization of the subtype. + /// + public string Identifier { get; } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonPolymorphicTypeAttribute.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonPolymorphicTypeAttribute.cs new file mode 100644 index 00000000000000..ecc1886aebf211 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonPolymorphicTypeAttribute.cs @@ -0,0 +1,14 @@ +// 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 +{ + /// + /// When placed on a type, indicates that values should + /// be serialized using the schema of their runtime types. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] + public sealed class JsonPolymorphicTypeAttribute : JsonAttribute + { + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConverterList.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs similarity index 67% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConverterList.cs rename to src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs index 58102d827e544f..be9ccfb1edd82e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConverterList.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs @@ -7,25 +7,28 @@ namespace System.Text.Json.Serialization { /// - /// A list of JsonConverters that respects the options class being immuttable once (de)serialization occurs. + /// A list of configuration items that respects the options class being immutable once (de)serialization occurs. /// - internal sealed class ConverterList : IList + internal sealed class ConfigurationList : IList { - private readonly List _list = new List(); + private readonly List _list; private readonly JsonSerializerOptions _options; - public ConverterList(JsonSerializerOptions options) + public Action? OnElementAdded { get; set; } + + public ConfigurationList(JsonSerializerOptions options) { _options = options; + _list = new(); } - public ConverterList(JsonSerializerOptions options, ConverterList source) + public ConfigurationList(JsonSerializerOptions options, IList source) { _options = options; - _list = new List(source._list); + _list = new List(source is ConfigurationList cl ? cl._list : source); } - public JsonConverter this[int index] + public TItem this[int index] { get { @@ -47,7 +50,7 @@ public JsonConverter this[int index] public bool IsReadOnly => false; - public void Add(JsonConverter item) + public void Add(TItem item) { if (item == null) { @@ -56,6 +59,7 @@ public void Add(JsonConverter item) _options.VerifyMutable(); _list.Add(item); + OnElementAdded?.Invoke(item); } public void Clear() @@ -64,27 +68,27 @@ public void Clear() _list.Clear(); } - public bool Contains(JsonConverter item) + public bool Contains(TItem item) { return _list.Contains(item); } - public void CopyTo(JsonConverter[] array, int arrayIndex) + public void CopyTo(TItem[] array, int arrayIndex) { _list.CopyTo(array, arrayIndex); } - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { return _list.GetEnumerator(); } - public int IndexOf(JsonConverter item) + public int IndexOf(TItem item) { return _list.IndexOf(item); } - public void Insert(int index, JsonConverter item) + public void Insert(int index, TItem item) { if (item == null) { @@ -93,9 +97,10 @@ public void Insert(int index, JsonConverter item) _options.VerifyMutable(); _list.Insert(index, item); + OnElementAdded?.Invoke(item); } - public bool Remove(JsonConverter item) + public bool Remove(TItem item) { _options.VerifyMutable(); return _list.Remove(item); 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 9147243ef95be1..2e7b5b901c7cc5 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 @@ -38,7 +38,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, int index = state.Current.EnumeratorIndex; JsonConverter elementConverter = GetElementConverter(ref state); - if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) + if (GetElementTypeInfo(ref state).CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { // Fast path that avoids validation and extra indirection. for (; index < array.Length; index++) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs index 1c5147ee9294c1..92ded054009299 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs @@ -55,7 +55,7 @@ internal sealed override bool OnTryRead( { JsonTypeInfo elementTypeInfo = state.Current.JsonTypeInfo.ElementTypeInfo!; - if (state.UseFastPath) + if (state.UseFastPath && !state.CanContainPolymorphismMetadata) { // Fast path that avoids maintaining state variables and dealing with preserved references. @@ -67,7 +67,7 @@ internal sealed override bool OnTryRead( CreateCollection(ref reader, ref state); _valueConverter ??= GetConverter(elementTypeInfo); - if (_valueConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) + if (elementTypeInfo.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { // Process all elements. while (true) @@ -132,8 +132,11 @@ internal sealed override bool OnTryRead( } // Handle the metadata properties. - bool preserveReferences = options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve; - if (preserveReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue) + bool canContainMetadata = + options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve || + state.CanContainPolymorphismMetadata; + + if (canContainMetadata && state.Current.ObjectState < StackFrameObjectState.PropertyValue) { if (JsonSerializer.ResolveMetadataForJsonObject(ref reader, ref state, options)) { @@ -188,7 +191,7 @@ internal sealed override bool OnTryRead( state.Current.PropertyState = StackFramePropertyState.Name; - if (preserveReferences) + if (canContainMetadata) { ReadOnlySpan propertyName = reader.GetSpan(); if (propertyName.Length > 0 && propertyName[0] == '$') @@ -278,9 +281,9 @@ internal sealed override bool OnTryWrite( { state.Current.ProcessedStartToken = true; writer.WriteStartObject(); - if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve) + if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve || state.PolymorphicTypeDiscriminator is not null) { - if (JsonSerializer.WriteReferenceForObject(this, dictionary, ref state, writer) == MetadataPropertyName.Ref) + if (JsonSerializer.WriteMetadataForObject(this, dictionary, ref state, writer, options.ReferenceHandlingStrategy) == MetadataPropertyName.Ref) { return true; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs index 2214e0ee16a241..87de947ade66dc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs @@ -55,7 +55,7 @@ protected internal override bool OnWriteResume( _keyConverter ??= GetConverter(typeInfo.KeyTypeInfo!); _valueConverter ??= GetConverter(typeInfo.ElementTypeInfo!); - if (!state.SupportContinuation && _valueConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) + if (!state.SupportContinuation && typeInfo.ElementTypeInfo!.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { // Fast path that avoids validation and extra indirection. do diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ICollectionOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ICollectionOfTConverter.cs index bc1e2fcbcbf5fd..ac062e19a8751d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ICollectionOfTConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ICollectionOfTConverter.cs @@ -29,7 +29,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStac if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) { - if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + if (!TypeToConvert.IsAssignableFrom(typeof(List))) { ThrowHelper.ThrowNotSupportedException_CannotPopulateCollection(TypeToConvert, ref reader, ref state); } @@ -96,17 +96,7 @@ protected override bool OnWriteResume( return true; } - internal override Type RuntimeType - { - get - { - if (TypeToConvert.IsAbstract || TypeToConvert.IsInterface) - { - return typeof(List); - } - - return TypeToConvert; - } - } + internal override JsonTypeInfo.ConstructorDelegate? ConstructorDelegate => + MemberAccessor.CreateConstructor>(TypeToConvert); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs index 710c5b1c923cda..11b8d99444762b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs @@ -31,7 +31,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStac if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) { - if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + if (!TypeToConvert.IsAssignableFrom(typeof(Dictionary))) { ThrowHelper.ThrowNotSupportedException_CannotPopulateCollection(TypeToConvert, ref reader, ref state); } @@ -115,17 +115,7 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio return true; } - internal override Type RuntimeType - { - get - { - if (TypeToConvert.IsAbstract || TypeToConvert.IsInterface) - { - return typeof(Dictionary); - } - - return TypeToConvert; - } - } + internal override JsonTypeInfo.ConstructorDelegate? ConstructorDelegate => + MemberAccessor.CreateConstructor>(TypeToConvert); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfTKeyTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfTKeyTValueConverter.cs index 50e63352ad9c1e..a114769da7bee4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfTKeyTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfTKeyTValueConverter.cs @@ -31,7 +31,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStac if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) { - if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + if (!TypeToConvert.IsAssignableFrom(typeof(Dictionary))) { ThrowHelper.ThrowNotSupportedException_CannotPopulateCollection(TypeToConvert, ref reader, ref state); } @@ -110,17 +110,7 @@ protected internal override bool OnWriteResume( return true; } - internal override Type RuntimeType - { - get - { - if (TypeToConvert.IsAbstract || TypeToConvert.IsInterface) - { - return typeof(Dictionary); - } - - return TypeToConvert; - } - } + internal override JsonTypeInfo.ConstructorDelegate? ConstructorDelegate => + MemberAccessor.CreateConstructor>(TypeToConvert); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverter.cs index ea7756ac8ee31d..35567a90f12116 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverter.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; +using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization.Converters { @@ -21,7 +22,7 @@ protected override void Add(in object? value, ref ReadStack state) protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options) { - if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + if (!TypeToConvert.IsAssignableFrom(typeof(List))) { ThrowHelper.ThrowNotSupportedException_CannotPopulateCollection(TypeToConvert, ref reader, ref state); } @@ -72,6 +73,7 @@ protected override bool OnWriteResume( return true; } - internal override Type RuntimeType => typeof(List); + internal override JsonTypeInfo.ConstructorDelegate? ConstructorDelegate => + MemberAccessor.CreateConstructor>(TypeToConvert); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs index b1a020260ee393..d6cc5ffb2d90e7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs @@ -33,6 +33,14 @@ protected static JsonConverter GetElementConverter(ref WriteStack stat return converter; } + protected static JsonTypeInfo GetElementTypeInfo(ref WriteStack state) + { + JsonTypeInfo typeInfo = state.Current.DeclaredJsonPropertyInfo!.RuntimeTypeInfo; + Debug.Assert(typeInfo != null); // It should not be possible to have a null JsonTypeInfo + + return typeInfo; + } + internal override bool OnTryRead( ref Utf8JsonReader reader, Type typeToConvert, @@ -42,7 +50,7 @@ internal override bool OnTryRead( { JsonTypeInfo elementTypeInfo = state.Current.JsonTypeInfo.ElementTypeInfo!; - if (state.UseFastPath) + if (state.UseFastPath && !state.CanContainPolymorphismMetadata) { // Fast path that avoids maintaining state variables and dealing with preserved references. @@ -54,7 +62,7 @@ internal override bool OnTryRead( CreateCollection(ref reader, ref state, options); JsonConverter elementConverter = GetElementConverter(elementTypeInfo); - if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) + if (elementTypeInfo.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { // Fast path that avoids validation and extra indirection. while (true) @@ -89,16 +97,18 @@ internal override bool OnTryRead( } else { - // Slower path that supports continuation and preserved references. + // Slower path that supports continuation and metadata reads + bool canContainMetadata = + options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve || + state.CanContainPolymorphismMetadata; - bool preserveReferences = options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve; if (state.Current.ObjectState == StackFrameObjectState.None) { if (reader.TokenType == JsonTokenType.StartArray) { state.Current.ObjectState = StackFrameObjectState.PropertyValue; } - else if (preserveReferences) + else if (canContainMetadata) { if (reader.TokenType != JsonTokenType.StartObject) { @@ -114,7 +124,7 @@ internal override bool OnTryRead( } // Handle the metadata properties. - if (preserveReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue) + if (canContainMetadata && state.Current.ObjectState < StackFrameObjectState.PropertyValue) { if (JsonSerializer.ResolveMetadataForJsonArray(ref reader, ref state, options)) { @@ -237,9 +247,9 @@ internal override bool OnTryWrite( if (!state.Current.ProcessedStartToken) { state.Current.ProcessedStartToken = true; - if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve) + if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve || state.PolymorphicTypeDiscriminator is not null) { - MetadataPropertyName metadata = JsonSerializer.WriteReferenceForCollection(this, value, ref state, writer); + MetadataPropertyName metadata = JsonSerializer.WriteMetadataForCollection(this, value, ref state, writer, options.ReferenceHandlingStrategy); if (metadata == MetadataPropertyName.Ref) { return true; @@ -263,7 +273,7 @@ internal override bool OnTryWrite( state.Current.ProcessedEndToken = true; writer.WriteEndArray(); - if (state.Current.MetadataPropertyName == MetadataPropertyName.Id) + if ((state.Current.MetadataPropertyName & (MetadataPropertyName.Id | MetadataPropertyName.Type)) != 0) { // Write the EndObject for $values. writer.WriteEndObject(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableOfTConverter.cs index 25eb1c9e0dd022..d5d2776d86d381 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableOfTConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableOfTConverter.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization.Converters { @@ -20,7 +21,7 @@ protected override void Add(in TElement value, ref ReadStack state) protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options) { - if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + if (!TypeToConvert.IsAssignableFrom(typeof(List))) { ThrowHelper.ThrowNotSupportedException_CannotPopulateCollection(TypeToConvert, ref reader, ref state); } @@ -67,6 +68,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, return true; } - internal override Type RuntimeType => typeof(List); + internal override JsonTypeInfo.ConstructorDelegate? ConstructorDelegate => + MemberAccessor.CreateConstructor>(TypeToConvert); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListConverter.cs index 28434508518e53..67dea8db01b343 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListConverter.cs @@ -28,7 +28,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStac if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) { - if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + if (!TypeToConvert.IsAssignableFrom(typeof(List))) { ThrowHelper.ThrowNotSupportedException_CannotPopulateCollection(TypeToConvert, ref reader, ref state); } @@ -61,7 +61,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, int index = state.Current.EnumeratorIndex; JsonConverter elementConverter = GetElementConverter(ref state); - if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) + if (GetElementTypeInfo(ref state).CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { // Fast path that avoids validation and extra indirection. for (; index < list.Count; index++) @@ -91,17 +91,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, return true; } - internal override Type RuntimeType - { - get - { - if (TypeToConvert.IsAbstract || TypeToConvert.IsInterface) - { - return typeof(List); - } - - return TypeToConvert; - } - } + internal override JsonTypeInfo.ConstructorDelegate? ConstructorDelegate => + MemberAccessor.CreateConstructor>(TypeToConvert); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListOfTConverter.cs index 2850c83401de3b..9ebdbe3c6946b7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListOfTConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListOfTConverter.cs @@ -29,7 +29,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStac if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) { - if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + if (!TypeToConvert.IsAssignableFrom(typeof(List))) { ThrowHelper.ThrowNotSupportedException_CannotPopulateCollection(TypeToConvert, ref reader, ref state); } @@ -92,17 +92,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, return true; } - internal override Type RuntimeType - { - get - { - if (TypeToConvert.IsAbstract || TypeToConvert.IsInterface) - { - return typeof(List); - } - - return TypeToConvert; - } - } + internal override JsonTypeInfo.ConstructorDelegate? ConstructorDelegate => + MemberAccessor.CreateConstructor>(TypeToConvert); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfTKeyTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfTKeyTValueConverter.cs index 8d161e3d4bdcd0..f8d3bb124ff231 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfTKeyTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfTKeyTValueConverter.cs @@ -18,7 +18,7 @@ protected override void Add(TKey key, in TValue value, JsonSerializerOptions opt protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state) { - if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + if (!TypeToConvert.IsAssignableFrom(typeof(Dictionary))) { ThrowHelper.ThrowNotSupportedException_CannotPopulateCollection(TypeToConvert, ref reader, ref state); } @@ -77,6 +77,7 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio return true; } - internal override Type RuntimeType => typeof(Dictionary); + internal override JsonTypeInfo.ConstructorDelegate? ConstructorDelegate => + MemberAccessor.CreateConstructor>(TypeToConvert); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ISetOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ISetOfTConverter.cs index b1486aee36034c..d63793d64513c8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ISetOfTConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ISetOfTConverter.cs @@ -26,7 +26,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStac if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) { - if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + if (!TypeToConvert.IsAssignableFrom(typeof(HashSet))) { ThrowHelper.ThrowNotSupportedException_CannotPopulateCollection(TypeToConvert, ref reader, ref state); } @@ -89,17 +89,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, return true; } - internal override Type RuntimeType - { - get - { - if (TypeToConvert.IsAbstract || TypeToConvert.IsInterface) - { - return typeof(HashSet); - } - - return TypeToConvert; - } - } + internal override JsonTypeInfo.ConstructorDelegate? ConstructorDelegate => + MemberAccessor.CreateConstructor>(TypeToConvert); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ListOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ListOfTConverter.cs index 835b827fbc5e5e..8f89d6d6a7f2dd 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ListOfTConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ListOfTConverter.cs @@ -33,7 +33,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, int index = state.Current.EnumeratorIndex; JsonConverter elementConverter = GetElementConverter(ref state); - if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) + if (GetElementTypeInfo(ref state).CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { // Fast path that avoids validation and extra indirection. for (; index < list.Count; index++) 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 4a2be44c8629c7..e6140b3bad36a3 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 @@ -241,9 +241,10 @@ internal sealed override bool OnTryWrite( if (!state.SupportContinuation) { writer.WriteStartObject(); - if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve) + + if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve || state.PolymorphicTypeDiscriminator is not null) { - if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref) + if (JsonSerializer.WriteMetadataForObject(this, objectValue, ref state, writer, options.ReferenceHandlingStrategy) == MetadataPropertyName.Ref) { return true; } @@ -289,9 +290,9 @@ internal sealed override bool OnTryWrite( if (!state.Current.ProcessedStartToken) { writer.WriteStartObject(); - if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve) + if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve || state.PolymorphicTypeDiscriminator is not null) { - if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref) + if (JsonSerializer.WriteMetadataForObject(this, objectValue, ref state, writer, options.ReferenceHandlingStrategy) == MetadataPropertyName.Ref) { return true; } @@ -312,7 +313,10 @@ internal sealed override bool OnTryWrite( if (!jsonPropertyInfo.GetMemberAndWriteJson(objectValue!, ref state, writer)) { - Debug.Assert(jsonPropertyInfo.ConverterBase.ConverterStrategy != ConverterStrategy.Value); + Debug.Assert( + jsonPropertyInfo.ConverterBase.ConverterStrategy != ConverterStrategy.Value || + jsonPropertyInfo.ConverterBase.TypeToConvert == JsonTypeInfo.ObjectType); + return false; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs index 227a8c32a9f42f..ba69c3efbf9ffd 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs @@ -70,7 +70,5 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, T? v _converter.WriteNumberWithCustomHandling(writer, value.Value, handling); } } - - internal override bool IsNull(in T? value) => !value.HasValue; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.ReadAhead.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.ReadAhead.cs index fa75dfe851fbcf..161a8024650b50 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.ReadAhead.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.ReadAhead.cs @@ -22,7 +22,8 @@ internal static bool SingleValueReadWithReadAhead(ConverterStrategy converterStr bool readAhead = state.ReadAhead && converterStrategy == ConverterStrategy.Value; if (!readAhead) { - return reader.Read(); + // Do not advance the reader if converter was suspended due to a read-ahead operation + return state.NoReaderAdvanceOnContinuation || reader.Read(); } return DoSingleValueReadWithReadAhead(ref reader, ref state); @@ -35,7 +36,8 @@ internal static bool DoSingleValueReadWithReadAhead(ref Utf8JsonReader reader, r JsonReaderState initialReaderState = reader.CurrentState; long initialReaderBytesConsumed = reader.BytesConsumed; - if (!reader.Read()) + // Do not advance the reader if converter was suspended due to a read-ahead operation + if (!state.NoReaderAdvanceOnContinuation && !reader.Read()) { return false; } @@ -64,7 +66,12 @@ internal static bool DoSingleValueReadWithReadAhead(ref Utf8JsonReader reader, r } // Success, requeue the reader to the start token. - reader.ReadWithVerify(); + // Do not advance the reader if converter was suspended due to a read-ahead operation + if (!state.NoReaderAdvanceOnContinuation) + { + reader.ReadWithVerify(); + } + Debug.Assert(tokenType == reader.TokenType); } 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 f324f672c915f7..5b76a33d7b1d78 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 @@ -22,18 +22,11 @@ internal JsonConverter() { } internal abstract ConverterStrategy ConverterStrategy { get; } - /// - /// Can direct Read or Write methods be called (for performance). - /// - internal bool CanUseDirectReadOrWrite { get; set; } - /// /// Can the converter have $id metadata. /// internal virtual bool CanHaveIdMetadata => true; - internal bool CanBePolymorphic { get; set; } - /// /// Used to support JsonObject as an extension property in a loosely-typed, trimmable manner. /// @@ -83,8 +76,8 @@ internal virtual void ReadElementAndSetProperty( /// internal abstract object? ReadCoreAsObject(ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state); - // For polymorphic cases, the concrete type to create. - internal virtual Type RuntimeType => TypeToConvert; + /// Overriden by converters of abstract types. + internal virtual JsonTypeInfo.ConstructorDelegate? ConstructorDelegate => null; internal bool ShouldFlush(Utf8JsonWriter writer, ref WriteStack state) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.WriteCore.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.WriteCore.cs index 1d60dbf4e38c31..3d2170cad65a17 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.WriteCore.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.WriteCore.cs @@ -11,7 +11,13 @@ internal sealed override bool WriteCoreAsObject( JsonSerializerOptions options, ref WriteStack state) { - if (IsValueType) + if ( +#if NET5_0_OR_GREATER + typeof(T).IsValueType // treated as a constant by recent versions of the JIT. +#else + IsValueType +#endif + ) { // Value types can never have a null except for Nullable. if (value == null && Nullable.GetUnderlyingType(TypeToConvert) == null) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs index 6ea73dd49c9ec5..d2db67d9b8b532 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization @@ -18,11 +19,7 @@ public abstract partial class JsonConverter : JsonConverter /// protected internal JsonConverter() { - // Today only typeof(object) can have polymorphic writes. - // In the future, this will be check for !IsSealed (and excluding value types). - CanBePolymorphic = TypeToConvert == JsonTypeInfo.ObjectType; IsValueType = TypeToConvert.IsValueType; - CanBeNull = !IsValueType || TypeToConvert.IsNullableOfT(); IsInternalConverter = GetType().Assembly == typeof(JsonConverter).Assembly; if (HandleNull) @@ -30,14 +27,6 @@ protected internal JsonConverter() HandleNullOnRead = true; HandleNullOnWrite = true; } - - // For the HandleNull == false case, either: - // 1) The default values are assigned in this type's virtual HandleNull property - // or - // 2) A converter overroad HandleNull and returned false so HandleNullOnRead and HandleNullOnWrite - // will be their default values of false. - - CanUseDirectReadOrWrite = !CanBePolymorphic && IsInternalConverter && ConverterStrategy == ConverterStrategy.Value; } /// @@ -86,7 +75,7 @@ public virtual bool HandleNull // If the type doesn't support null, allow the converter a chance to modify. // These semantics are backwards compatible with 3.0. - HandleNullOnRead = !CanBeNull; + HandleNullOnRead = default(T) is not null; // The framework handles null automatically on writes. HandleNullOnWrite = false; @@ -105,11 +94,6 @@ public virtual bool HandleNull /// internal bool HandleNullOnWrite { get; private set; } - /// - /// Can be assigned to ? - /// - internal bool CanBeNull { get; } - // This non-generic API is sealed as it just forwards to the generic version. internal sealed override bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state) { @@ -145,6 +129,84 @@ internal virtual bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, J internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T? value) { + // Handle polymorphic deserialization + if ( +#if NET5_0_OR_GREATER + !typeof(T).IsValueType && // treated as a constant by recent versions of the JIT. +#else + !IsValueType && +#endif + ConverterStrategy != ConverterStrategy.Value) + { + JsonTypeInfo jsonTypeInfo = state.PeekNextJsonTypeInfo(); + Debug.Assert(jsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase.TypeToConvert == TypeToConvert); + + if (jsonTypeInfo.TypeDiscriminatorResolver is not null) + { + // The current converter supports polymorphic deserialization + JsonConverter? polymorphicConverter = null; + + switch (state.Current.PolymorphicSerializationState) + { + case PolymorphicSerializationState.PolymorphicReEntryStarted: + // Current frame is already dispatched to a polymorphic converter, abort. + break; + + case PolymorphicSerializationState.PolymorphicReEntrySuspended: + // Resuming a polymorphic converter. + Debug.Assert(state.IsContinuation); + polymorphicConverter = state.Current.ResumePolymorphicReEntry(); + Debug.Assert(typeToConvert.IsAssignableFrom(polymorphicConverter.TypeToConvert)); + break; + + default: + Debug.Assert(state.Current.PolymorphicSerializationState == PolymorphicSerializationState.None); + + // Need to read ahead for the type discriminator before dispatching to the relevant polymorphic converter + // Use a copy of the reader to avoid advancing the buffer. + Utf8JsonReader readerCopy = reader; + if (!JsonSerializer.TryReadTypeDiscriminator(ref readerCopy, out string? typeId)) + { + // Insufficient data in the buffer to read the type discriminator. + // Signal to the state that only the read-ahead operation requires more data + // and that the Utf8JsonReader state should not be advanced. + state.NoReaderAdvanceOnContinuation = true; + value = default; + return false; + } + + if (state.NoReaderAdvanceOnContinuation) + { + // the converter was suspended while attempting to read ahead the type discrimator. + // Unset the continuation the flag since for all intents and purposes this is the first run of the converter. + state.NoReaderAdvanceOnContinuation = false; + } + + if (typeId is not null && + jsonTypeInfo.TypeDiscriminatorResolver.TryResolveTypeByTypeId(typeId, out Type? type) && + type != typeToConvert) + { + polymorphicConverter = state.Current.InitializePolymorphicReEntry(type, options); + Debug.Assert(polymorphicConverter.TypeToConvert == type); + } + + break; + } + + if (polymorphicConverter is not null) + { + bool success2 = polymorphicConverter.TryReadAsObject(ref reader, options, ref state, out object? objectValue); + value = (T?)objectValue; + + state.Current.PolymorphicSerializationState = success2 + ? PolymorphicSerializationState.None + : PolymorphicSerializationState.PolymorphicReEntrySuspended; + + return success2; + } + } + } + if (ConverterStrategy == ConverterStrategy.Value) { // A value converter should never be within a continuation. @@ -153,13 +215,15 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali // For perf and converter simplicity, handle null here instead of forwarding to the converter. if (reader.TokenType == JsonTokenType.Null && !HandleNullOnRead) { - if (!CanBeNull) + if (default(T) is not null) { ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); } - - value = default; - return true; + else + { + value = default; + return true; + } } #if !DEBUG @@ -200,12 +264,10 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali } if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve && - CanBePolymorphic && value is JsonElement element) + TypeToConvert == JsonTypeInfo.ObjectType && value is JsonElement element) { // Edge case where we want to lookup for a reference when parsing into typeof(object) // instead of return `value` as a JsonElement. - Debug.Assert(TypeToConvert == typeof(object)); - if (JsonSerializer.TryGetReferenceFromJsonElement(ref state, element, out object? referenceValue)) { value = (T?)referenceValue; @@ -215,20 +277,24 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali return true; } - bool success; - // Remember if we were a continuation here since Push() may affect IsContinuation. bool wasContinuation = state.IsContinuation; +#if DEBUG + // DEBUG: ensure push/pop operations preserve stack integrity + JsonTypeInfo originalJsonTypeInfo = state.Current.JsonTypeInfo; +#endif state.Push(); + Debug.Assert(state.Current.JsonTypeInfo.Type == TypeToConvert); + bool success; #if !DEBUG // For performance, only perform validation on internal converters on debug builds. if (IsInternalConverter) { if (reader.TokenType == JsonTokenType.Null && !HandleNullOnRead && !wasContinuation) { - if (!CanBeNull) + if (default(T) is not null) { ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); } @@ -250,14 +316,16 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali // For perf and converter simplicity, handle null here instead of forwarding to the converter. if (reader.TokenType == JsonTokenType.Null && !HandleNullOnRead) { - if (!CanBeNull) + if (default(T) is not null) { ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); } - - value = default; - state.Pop(true); - return true; + else + { + value = default; + state.Pop(true); + return true; + } } Debug.Assert(state.Current.OriginalTokenType == JsonTokenType.None); @@ -288,6 +356,9 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali } state.Pop(success); +#if DEBUG + Debug.Assert(ReferenceEquals(originalJsonTypeInfo, state.Current.JsonTypeInfo)); +#endif return success; } @@ -298,7 +369,7 @@ internal override sealed bool TryReadAsObject(ref Utf8JsonReader reader, JsonSer return success; } - internal virtual bool IsNull(in T value) => value == null; + internal bool IsNull(T value) => value is null; internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions options, ref WriteStack state) { @@ -307,7 +378,7 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions ThrowHelper.ThrowJsonException_SerializerCycleDetected(options.EffectiveMaxDepth); } - if (CanBeNull && !HandleNullOnWrite && IsNull(value)) + if (default(T) is null && IsNull(value) && !HandleNullOnWrite) { // We do not pass null values to converters unless HandleNullOnWrite is true. Null values for properties were // already handled in GetMemberAndWriteJson() so we don't need to check for IgnoreNullValues here. @@ -315,83 +386,136 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions return true; } - bool ignoreCyclesPopReference = false; - - if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.IgnoreCycles && - // .NET types that are serialized as JSON primitive values don't need to be tracked for cycle detection e.g: string. - ConverterStrategy != ConverterStrategy.Value && - !IsValueType && !IsNull(value)) - { - // Custom (user) converters shall not track references - // it is responsibility of the user to break cycles in case there's any - // if we compare against Preserve, objects don't get preserved when a custom converter exists - // given that the custom converter executes prior to the preserve logic. - Debug.Assert(IsInternalConverter); - Debug.Assert(value != null); - - ReferenceResolver resolver = state.ReferenceResolver; - - // Write null to break reference cycles. - if (resolver.ContainsReferenceForCycleDetection(value)) - { - writer.WriteNullValue(); - return true; - } - - // For boxed reference types: do not push when boxed in order to avoid false positives - // when we run the ContainsReferenceForCycleDetection check for the converter of the unboxed value. - Debug.Assert(!CanBePolymorphic); - resolver.PushReferenceForCycleDetection(value); - ignoreCyclesPopReference = true; - } + bool isCycleDetectionReferencePushed = false; - if (CanBePolymorphic) + // Check if converter is eligible for cycle detection or polymorphism. + // We do not support custom converters, with the exception of System.Object converters. + // TODO: make ObjectConverter use ConverterStrategy.Object. + if ( +#if NET5_0_OR_GREATER + !typeof(T).IsValueType && // treated as a constant by recent versions of the JIT. +#else + !IsValueType && +#endif + value is not null && + (ConverterStrategy != ConverterStrategy.Value || TypeToConvert == JsonTypeInfo.ObjectType)) { - if (value == null) + // Only enter reference handling section value is first picked up by a converter: + // do not run if continuation or if dispatched to a polymorphic converter. + if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.IgnoreCycles && + !state.IsContinuation && state.Current.PolymorphicSerializationState == PolymorphicSerializationState.None) { - Debug.Assert(ConverterStrategy == ConverterStrategy.Value); - Debug.Assert(!state.IsContinuation); - Debug.Assert(HandleNullOnWrite); + ReferenceResolver resolver = state.ReferenceResolver; - int originalPropertyDepth = writer.CurrentDepth; - Write(writer, value, options); - VerifyWrite(originalPropertyDepth, writer); + // Write null to break reference cycles. + if (resolver.ContainsReferenceForCycleDetection(value)) + { + writer.WriteNullValue(); + return true; + } - return true; + resolver.PushReferenceForCycleDetection(value); + isCycleDetectionReferencePushed = true; } - Type type = value.GetType(); - if (type == JsonTypeInfo.ObjectType) - { - writer.WriteStartObject(); - writer.WriteEndObject(); - return true; - } + JsonTypeInfo jsonTypeInfo = state.PeekNextJsonTypeInfo(); + Debug.Assert(jsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase.TypeToConvert == TypeToConvert || !IsInternalConverter); - if (type != TypeToConvert && IsInternalConverter) + // TODO: refactor into a standalone TryGetPolymorphicConverter method + if (jsonTypeInfo.CanBePolymorphic) { - // For internal converter only: Handle polymorphic case and get the new converter. - // Custom converter, even though polymorphic converter, get called for reading AND writing. - JsonConverter jsonConverter = state.Current.InitializeReEntry(type, options); - Debug.Assert(jsonConverter != this); + JsonConverter? polymorphicConverter = null; - if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.IgnoreCycles && - jsonConverter.IsValueType) + switch (state.Current.PolymorphicSerializationState) { - // For boxed value types: push the value before it gets unboxed on TryWriteAsObject. - state.ReferenceResolver.PushReferenceForCycleDetection(value); - ignoreCyclesPopReference = true; + case PolymorphicSerializationState.PolymorphicReEntryStarted: + // Current frame is already dispatched to a polymorphic converter, abort. + break; + + case PolymorphicSerializationState.PolymorphicReEntrySuspended: + // Resuming a polymorphic converter. + Debug.Assert(state.IsContinuation); + polymorphicConverter = state.Current.ResumePolymorphicReEntry(); + Debug.Assert(value is not null && polymorphicConverter.TypeToConvert.IsAssignableFrom(value.GetType())); + break; + + default: + Debug.Assert(state.Current.PolymorphicSerializationState == PolymorphicSerializationState.None); + + Type type = value.GetType(); + + if (jsonTypeInfo.TypeDiscriminatorResolver is not null) + { + // Prepare serialization for type discriminator polymorphism: + // if the resolver yields a valid typeId dispatch to the converter for the resolved type, + // otherwise revert back to using the current converter type and do not serialize polymorphically. + Debug.Assert(state.PolymorphicTypeDiscriminator is null); + + if (jsonTypeInfo.TypeDiscriminatorResolver.TryResolvePolymorphicSubtype(type, out Type? resolvedType, out string? typeId)) + { + Debug.Assert(resolvedType.IsAssignableFrom(type)); + + type = resolvedType; + state.PolymorphicTypeDiscriminator = typeId; + } + else + { + type = TypeToConvert; + } + } + + // Special handling for System.Object instance + if (type == JsonTypeInfo.ObjectType) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + + if (isCycleDetectionReferencePushed) + { + state.ReferenceResolver.PopReferenceForCycleDetection(); + } + + return true; + } + + if (type != TypeToConvert && IsInternalConverter) // TODO: IsInternalConverter should be moved to outer check + // Currently used here so that user converters include handling for System.Object instances + { + // For internal converter only: Handle polymorphic case and get the new converter. + // Custom converter, even though polymorphic converter, get called for reading AND writing. + polymorphicConverter = state.Current.InitializePolymorphicReEntry(type, options); + Debug.Assert(polymorphicConverter.TypeToConvert == type || !polymorphicConverter.IsInternalConverter); + + // Store the cycle detection in case a continuation is required. + state.Current.IsCycleDetectionReferencePushed = isCycleDetectionReferencePushed; + } + + break; } - // We found a different converter; forward to that. - bool success2 = jsonConverter.TryWriteAsObject(writer, value, options, ref state); - - if (ignoreCyclesPopReference) + if (polymorphicConverter is not null) { - state.ReferenceResolver.PopReferenceForCycleDetection(); - } + // We found a different converter; forward to that. + bool success2 = polymorphicConverter.TryWriteAsObject(writer, value, options, ref state); - return success2; + if (success2) + { + state.Current.PolymorphicSerializationState = PolymorphicSerializationState.None; + state.PolymorphicTypeDiscriminator = null; + + if (state.Current.IsCycleDetectionReferencePushed) + { + state.ReferenceResolver.PopReferenceForCycleDetection(); + state.Current.IsCycleDetectionReferencePushed = false; + } + } + else + { + state.Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntrySuspended; + } + + return success2; + } } } @@ -416,28 +540,39 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions bool isContinuation = state.IsContinuation; +#if DEBUG + // DEBUG: ensure push/pop operations preserve stack integrity + JsonTypeInfo originalJsonTypeInfo = state.Current.JsonTypeInfo; +#endif state.Push(); + Debug.Assert(state.Current.JsonTypeInfo.Type == TypeToConvert); if (!isContinuation) { Debug.Assert(state.Current.OriginalDepth == 0); state.Current.OriginalDepth = writer.CurrentDepth; + Debug.Assert(!state.Current.IsCycleDetectionReferencePushed); + state.Current.IsCycleDetectionReferencePushed = isCycleDetectionReferencePushed; } bool success = OnTryWrite(writer, value, options, ref state); if (success) { VerifyWrite(state.Current.OriginalDepth, writer); - // No need to clear state.Current.OriginalDepth since a stack pop will occur. - } - state.Pop(success); + if (state.Current.IsCycleDetectionReferencePushed) + { + state.ReferenceResolver.PopReferenceForCycleDetection(); + state.Current.IsCycleDetectionReferencePushed = false; + } - if (ignoreCyclesPopReference) - { - state.ReferenceResolver.PopReferenceForCycleDetection(); + // No need to clear state.Current.OriginalDepth since a stack pop will occur. } + state.Pop(success); +#if DEBUG + Debug.Assert(ReferenceEquals(originalJsonTypeInfo, state.Current.JsonTypeInfo)); +#endif return success; } @@ -476,6 +611,7 @@ internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, Json // Ignore the naming policy for extension data. state.Current.IgnoreDictionaryKeyPolicy = true; + state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; success = dictionaryConverter.OnWriteResume(writer, value, options, ref state); if (success) @@ -488,7 +624,7 @@ internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, Json return success; } - internal sealed override Type TypeToConvert => typeof(T); + internal sealed override Type TypeToConvert { get; } = typeof(T); internal void VerifyRead(JsonTokenType tokenType, int depth, long bytesConsumed, bool isValueConverter, ref Utf8JsonReader reader) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs index b7a5dffe77e7c5..3060b9c8133723 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs @@ -18,6 +18,9 @@ internal static readonly byte[] s_refPropertyName internal static readonly byte[] s_valuesPropertyName = new byte[] { (byte)'$', (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', (byte)'s' }; + internal static readonly byte[] s_TypeIdPropertyName + = new byte[] { (byte)'$', (byte)'t', (byte)'y', (byte)'p', (byte)'e' }; + /// /// Returns true if successful, false is the reader ran out of buffer. /// Sets state.Current.ReturnValue to the reference target for $ref cases; @@ -78,6 +81,11 @@ internal static bool ResolveMetadataForJsonObject( { ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(propertyName, ref state, reader); } + else if (metadata == MetadataPropertyName.Type) + { + state.Current.JsonPropertyName = s_TypeIdPropertyName; + state.Current.ObjectState = StackFrameObjectState.ReadAheadTypeValue; + } else { Debug.Assert(metadata == MetadataPropertyName.NoMetadata); @@ -103,6 +111,13 @@ internal static bool ResolveMetadataForJsonObject( return false; } } + else if (state.Current.ObjectState == StackFrameObjectState.ReadAheadTypeValue) + { + if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadTypeValue)) + { + return false; + } + } if (state.Current.ObjectState == StackFrameObjectState.ReadRefValue) { @@ -219,6 +234,11 @@ internal static bool ResolveMetadataForJsonArray( { ThrowHelper.ThrowJsonException_MetadataMissingIdBeforeValues(ref state, propertyName); } + else if (metadata == MetadataPropertyName.Type) + { + state.Current.JsonPropertyName = s_TypeIdPropertyName; + state.Current.ObjectState = StackFrameObjectState.ReadAheadTypeValue; + } else { Debug.Assert(metadata == MetadataPropertyName.NoMetadata); @@ -242,6 +262,13 @@ internal static bool ResolveMetadataForJsonArray( return false; } } + else if (state.Current.ObjectState == StackFrameObjectState.ReadAheadTypeValue) + { + if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadTypeValue)) + { + return false; + } + } if (state.Current.ObjectState == StackFrameObjectState.ReadRefValue) { @@ -272,6 +299,13 @@ internal static bool ResolveMetadataForJsonArray( // Need to Read $values property name. state.Current.ObjectState = StackFrameObjectState.ReadAheadValuesName; } + else if (state.Current.ObjectState == StackFrameObjectState.ReadTypeValue) + { + converter.CreateInstanceForReferenceResolver(ref reader, ref state, options); + + // Need to Read $values property name. + state.Current.ObjectState = StackFrameObjectState.ReadAheadValuesName; + } // Clear the metadata property name that was set in case of failure on ResolverReference/AddReference. state.Current.JsonPropertyName = null; @@ -346,6 +380,66 @@ internal static bool ResolveMetadataForJsonArray( return true; } + /// + /// Attempts to read the type discriminator property from a JSON object. + /// The boolean return value indicates whether the JSON object is fully populated in the buffer, + /// and success of the read operation is reflected on the nullity of the typeId out parameter. + /// + internal static bool TryReadTypeDiscriminator(ref Utf8JsonReader reader, out string? typeId) + { + bool isReadSuccessful = true; + typeId = null; + + if (reader.TokenType == JsonTokenType.StartObject) + { + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + goto Return; + } + + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + MetadataPropertyName propertyName = GetMetadataPropertyName(reader.GetSpan()); + if (propertyName == MetadataPropertyName.Type) + { + // Found the $type property + + if (reader.Read()) + { + if (reader.TokenType == JsonTokenType.String) + { + typeId = reader.GetString(); + } + } + else + { + isReadSuccessful = false; + } + + goto Return; + } + else if (propertyName == MetadataPropertyName.NoMetadata) + { + // Stop looking once we encounter non-metadata properties + goto Return; + } + + // Skip the non-$type metadata value + if (!reader.TrySkip()) + { + isReadSuccessful = false; + goto Return; + } + } + + isReadSuccessful = false; + } + + Return: + return isReadSuccessful; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryReadAheadMetadataAndSetState(ref Utf8JsonReader reader, ref ReadStack state, StackFrameObjectState nextState) { @@ -378,6 +472,16 @@ internal static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan } break; + case 5: + if (propertyName[1] == 't' && + propertyName[2] == 'y' && + propertyName[3] == 'p' && + propertyName[4] == 'e') + { + return MetadataPropertyName.Type; + } + break; + case 7: if (propertyName[1] == 'v' && propertyName[2] == 'a' && diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs index 014ea974ed247f..676fc06be41fbd 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs @@ -83,7 +83,9 @@ internal static ReadOnlySpan GetPropertyName( if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve) { - if (propertyName.Length > 0 && propertyName[0] == '$') + if (propertyName.Length > 0 && propertyName[0] == '$' && + // TODO replace with something more tenable + !propertyName.SequenceEqual(s_TypeIdPropertyName)) { ThrowHelper.ThrowUnexpectedMetadataException(propertyName, ref reader, ref state); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs index 45c1462ea68f3a..f2886779bd2711 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs @@ -12,77 +12,128 @@ public static partial class JsonSerializer internal static readonly JsonEncodedText s_metadataId = JsonEncodedText.Encode("$id", encoder: null); internal static readonly JsonEncodedText s_metadataRef = JsonEncodedText.Encode("$ref", encoder: null); internal static readonly JsonEncodedText s_metadataValues = JsonEncodedText.Encode("$values", encoder: null); + internal static readonly JsonEncodedText s_metadataType = JsonEncodedText.Encode("$type", encoder: null); - internal static MetadataPropertyName WriteReferenceForObject( + internal static MetadataPropertyName WriteMetadataForObject( JsonConverter jsonConverter, object currentValue, ref WriteStack state, - Utf8JsonWriter writer) + Utf8JsonWriter writer, + ReferenceHandlingStrategy referenceHandlingStrategy) { + Debug.Assert(referenceHandlingStrategy == ReferenceHandlingStrategy.Preserve || state.PolymorphicTypeDiscriminator is not null); MetadataPropertyName writtenMetadataName; - // If the jsonConverter supports immutable dictionaries or value types, don't write any metadata - if (!jsonConverter.CanHaveIdMetadata || jsonConverter.IsValueType) + if (referenceHandlingStrategy == ReferenceHandlingStrategy.Preserve) { - writtenMetadataName = MetadataPropertyName.NoMetadata; - } - else - { - string referenceId = state.ReferenceResolver.GetReference(currentValue, out bool alreadyExists); - Debug.Assert(referenceId != null); - - if (alreadyExists) + // If the jsonConverter supports immutable dictionaries or value types, don't write any metadata + if (!jsonConverter.CanHaveIdMetadata || jsonConverter.IsValueType) { - writer.WriteString(s_metadataRef, referenceId); - writer.WriteEndObject(); - writtenMetadataName = MetadataPropertyName.Ref; + writtenMetadataName = MetadataPropertyName.NoMetadata; } else { - writer.WriteString(s_metadataId, referenceId); - writtenMetadataName = MetadataPropertyName.Id; + string referenceId = state.ReferenceResolver.GetReference(currentValue, out bool alreadyExists); + Debug.Assert(referenceId != null); + + if (alreadyExists) + { + writer.WriteString(s_metadataRef, referenceId); + writer.WriteEndObject(); + writtenMetadataName = MetadataPropertyName.Ref; + } + else + { + writtenMetadataName = MetadataPropertyName.Id; + + // write the typeId ahead of the referenceId + if (state.PolymorphicTypeDiscriminator is string typeId) + { + writer.WriteString(s_TypeIdPropertyName, typeId); + writtenMetadataName |= MetadataPropertyName.Type; + state.PolymorphicTypeDiscriminator = null; + } + + writer.WriteString(s_metadataId, referenceId); + } } } + else + { + Debug.Assert(state.PolymorphicTypeDiscriminator is not null); + writer.WriteString(s_TypeIdPropertyName, state.PolymorphicTypeDiscriminator); + writtenMetadataName = MetadataPropertyName.Type; + state.PolymorphicTypeDiscriminator = null; + } return writtenMetadataName; } - internal static MetadataPropertyName WriteReferenceForCollection( + internal static MetadataPropertyName WriteMetadataForCollection( JsonConverter jsonConverter, object currentValue, ref WriteStack state, - Utf8JsonWriter writer) + Utf8JsonWriter writer, + ReferenceHandlingStrategy referenceHandlingStrategy) { + Debug.Assert(referenceHandlingStrategy == ReferenceHandlingStrategy.Preserve || state.PolymorphicTypeDiscriminator is not null); MetadataPropertyName writtenMetadataName; - // If the jsonConverter supports immutable enumerables or value type collections, don't write any metadata - if (!jsonConverter.CanHaveIdMetadata || jsonConverter.IsValueType) - { - writer.WriteStartArray(); - writtenMetadataName = MetadataPropertyName.NoMetadata; - } - else + if (referenceHandlingStrategy == ReferenceHandlingStrategy.Preserve) { - string referenceId = state.ReferenceResolver.GetReference(currentValue, out bool alreadyExists); - Debug.Assert(referenceId != null); - - if (alreadyExists) + // If the jsonConverter supports immutable enumerables or value type collections, don't write any metadata + if (!jsonConverter.CanHaveIdMetadata || jsonConverter.IsValueType) { - writer.WriteStartObject(); - writer.WriteString(s_metadataRef, referenceId); - writer.WriteEndObject(); - writtenMetadataName = MetadataPropertyName.Ref; + writer.WriteStartArray(); + writtenMetadataName = MetadataPropertyName.NoMetadata; } else { - writer.WriteStartObject(); - writer.WriteString(s_metadataId, referenceId); - writer.WriteStartArray(s_metadataValues); - writtenMetadataName = MetadataPropertyName.Id; + string referenceId = state.ReferenceResolver.GetReference(currentValue, out bool alreadyExists); + Debug.Assert(referenceId != null); + + if (alreadyExists) + { + writer.WriteStartObject(); + writer.WriteString(s_metadataRef, referenceId); + writer.WriteEndObject(); + writtenMetadataName = MetadataPropertyName.Ref; + } + else + { + writtenMetadataName = MetadataPropertyName.Id; + writer.WriteStartObject(); + + // write the typeId ahead of the referenceId + if (state.PolymorphicTypeDiscriminator is string typeId) + { + writer.WriteString(s_TypeIdPropertyName, typeId); + writtenMetadataName |= MetadataPropertyName.Type; + state.PolymorphicTypeDiscriminator = null; + } + + writer.WriteString(s_metadataId, referenceId); + writer.WriteStartArray(s_metadataValues); + } } } + else + { + Debug.Assert(state.PolymorphicTypeDiscriminator is not null); + + writer.WriteStartObject(); + writer.WriteString(s_TypeIdPropertyName, state.PolymorphicTypeDiscriminator); + writer.WriteStartArray(s_metadataValues); + state.PolymorphicTypeDiscriminator = null; + writtenMetadataName = MetadataPropertyName.Type; + } return writtenMetadataName; } + + internal static void WriteTypeMetata(Utf8JsonWriter writer, string typeId) + { + writer.WriteString(s_metadataType, typeId); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs index 71c30c512756c6..db52ffc079ffe3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs @@ -125,9 +125,6 @@ private static string WriteUsingMetadata(in TValue value, JsonTypeInfo? throw new ArgumentNullException(nameof(jsonTypeInfo)); } - WriteStack state = default; - state.Initialize(jsonTypeInfo, supportContinuation: false); - JsonSerializerOptions options = jsonTypeInfo.Options; using (var output = new PooledByteBufferWriter(options.DefaultBufferSize)) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs index 95de3062e667f8..7b0eff45812665 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs @@ -94,6 +94,14 @@ void Add(JsonConverter converter) => /// public IList Converters { get; } + /// + /// The list of custom type discriminator polymorphic type declarations. + /// + /// + /// Once serialization or deserialization occurs, the list cannot be modified. + /// + public IList TypeDiscriminatorConfigurations { get; } + internal JsonConverter DetermineConverter(Type? parentClassType, Type runtimePropertyType, MemberInfo? memberInfo) { JsonConverter converter = null!; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index b37f11ea941cf3..366aa57b93cb61 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -39,6 +39,7 @@ public sealed partial class JsonSerializerOptions private JsonNamingPolicy? _jsonPropertyNamingPolicy; private JsonCommentHandling _readCommentHandling; private ReferenceHandler? _referenceHandler; + private Func? _supportedPolymorphicTypes; private JavaScriptEncoder? _encoder; private JsonIgnoreCondition _defaultIgnoreCondition; private JsonNumberHandling _numberHandling; @@ -60,7 +61,13 @@ public sealed partial class JsonSerializerOptions /// public JsonSerializerOptions() { - Converters = new ConverterList(this); + Converters = new ConfigurationList(this); + + TypeDiscriminatorConfigurations = new ConfigurationList(this) + { + OnElementAdded = static config => { config.IsAssignedToOptionsInstance = true; } + }; + TrackOptionsInstance(this); } @@ -83,6 +90,7 @@ public JsonSerializerOptions(JsonSerializerOptions options) _jsonPropertyNamingPolicy = options._jsonPropertyNamingPolicy; _readCommentHandling = options._readCommentHandling; _referenceHandler = options._referenceHandler; + _supportedPolymorphicTypes = options._supportedPolymorphicTypes; _encoder = options._encoder; _defaultIgnoreCondition = options._defaultIgnoreCondition; _numberHandling = options._numberHandling; @@ -98,7 +106,8 @@ public JsonSerializerOptions(JsonSerializerOptions options) _propertyNameCaseInsensitive = options._propertyNameCaseInsensitive; _writeIndented = options._writeIndented; - Converters = new ConverterList(this, (ConverterList)options.Converters); + Converters = new ConfigurationList(this, options.Converters); + TypeDiscriminatorConfigurations = new ConfigurationList(this, options.TypeDiscriminatorConfigurations); EffectiveMaxDepth = options.EffectiveMaxDepth; ReferenceHandlingStrategy = options.ReferenceHandlingStrategy; @@ -536,6 +545,20 @@ public bool WriteIndented } } + /// + /// Type predicate configuring what types should be serialized polymorphically. + /// Note that this abstraction only governs serialization and offses no support for deserialization. + /// + public Func SupportedPolymorphicTypes + { + get => _supportedPolymorphicTypes ??= static type => type == JsonTypeInfo.ObjectType; + set + { + VerifyMutable(); + _supportedPolymorphicTypes = value; + } + } + /// /// Configures how object references are handled when reading and writing JSON. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs index 911ee9f3ede5ff..605d3a4186242c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs @@ -234,12 +234,15 @@ internal override bool GetMemberAndWriteJson(object obj, ref WriteStack state, U { T value = Get!(obj); - if (Options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.IgnoreCycles && - value != null && - // .NET types that are serialized as JSON primitive values don't need to be tracked for cycle detection e.g: string. - // However JsonConverter that uses ConverterStrategy == Value is an exception. - (Converter.CanBePolymorphic || (!Converter.IsValueType && ConverterStrategy != ConverterStrategy.Value)) && - state.ReferenceResolver.ContainsReferenceForCycleDetection(value)) + if ( +#if NET5_0_OR_GREATER + !typeof(T).IsValueType && // treated as a constant by recent versions of the JIT. +#endif + Options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.IgnoreCycles && + !state.IsContinuation && // Do not check for cycles in continuation frames: + // will result in false positives since the value is already present in the reference resolver stack; + // actual cycle occurrences never reappear as continuations since they are serialized as null. + value is not null && state.ReferenceResolver.ContainsReferenceForCycleDetection(value)) { // If a reference cycle is detected, treat value as null. value = default!; @@ -357,7 +360,7 @@ internal override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref success = true; } - else if (Converter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) + else if (state.Current.JsonTypeInfo.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { // CanUseDirectReadOrWrite == false when using streams Debug.Assert(!state.IsContinuation); @@ -422,7 +425,7 @@ internal override bool ReadJsonAsObject(ref ReadStack state, ref Utf8JsonReader else { // Optimize for internal converters by avoiding the extra call to TryRead. - if (Converter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) + if (state.Current.JsonTypeInfo.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { // CanUseDirectReadOrWrite == false when using streams Debug.Assert(!state.IsContinuation); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs index 7b3369a90ee67d..275b6d2a0a25e3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs @@ -65,12 +65,11 @@ internal static JsonPropertyInfo AddProperty( memberType, parentClassType, memberInfo, - out Type runtimeType, options); return CreateProperty( declaredPropertyType: memberType, - runtimePropertyType: runtimeType, + runtimePropertyType: memberType, memberInfo, parentClassType, converter, 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 53ed06d9c36645..3db60a7f4e7c30 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 @@ -32,6 +32,11 @@ public partial class JsonTypeInfo internal JsonPropertyInfo? DataExtensionProperty { get; private set; } + internal bool CanBePolymorphic => CanBeWritePolymorphic || TypeDiscriminatorResolver is not null; + internal TypeDiscriminatorResolver? TypeDiscriminatorResolver { get; private set; } + internal bool CanBeWritePolymorphic { get; private set; } + internal bool CanUseDirectReadOrWrite { get; private set; } + // If enumerable or dictionary, the JsonTypeInfo for the element type. private JsonTypeInfo? _elementTypeInfo; @@ -153,6 +158,8 @@ internal JsonTypeInfo(Type type, JsonSerializerOptions options, ConverterStrateg Options = options ?? throw new ArgumentNullException(nameof(options)); // Setting this option is deferred to the initialization methods of the various metadada info types. PropertyInfoForTypeInfo = null!; + + InitializePolymorphismConfiguration(converterStrategy, isInternalConverter: true); } [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] @@ -163,9 +170,8 @@ internal JsonTypeInfo(Type type, JsonSerializerOptions options) : type, parentClassType: null, // A TypeInfo never has a "parent" class. memberInfo: null, // A TypeInfo never has a "parent" property. - out Type runtimeType, options), - runtimeType, + runtimeType: type, options) { } @@ -176,7 +182,9 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json Type = type; Options = options; - JsonNumberHandling? typeNumberHandling = GetNumberHandlingForType(Type); + JsonNumberHandling? typeNumberHandling = GetNumberHandlingForType(type); + + InitializePolymorphismConfiguration(converter.ConverterStrategy, converter.IsInternalConverter); PropertyInfoForTypeInfo = CreatePropertyInfoForTypeInfo(Type, runtimeType, converter, typeNumberHandling, Options); @@ -286,14 +294,14 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json case ConverterStrategy.Enumerable: { ElementType = converter.ElementType; - CreateObject = Options.MemberAccessorStrategy.CreateConstructor(runtimeType); + CreateObject = converter.ConstructorDelegate ?? Options.MemberAccessorStrategy.CreateConstructor(runtimeType); } break; case ConverterStrategy.Dictionary: { KeyType = converter.KeyType; ElementType = converter.ElementType; - CreateObject = Options.MemberAccessorStrategy.CreateConstructor(runtimeType); + CreateObject = converter.ConstructorDelegate ?? Options.MemberAccessorStrategy.CreateConstructor(runtimeType); } break; case ConverterStrategy.Value: @@ -525,6 +533,40 @@ private void ValidateAndAssignDataExtensionProperty(JsonPropertyInfo jsonPropert DataExtensionProperty = jsonPropertyInfo; } + private void InitializePolymorphismConfiguration(ConverterStrategy converterStrategy, bool isInternalConverter) + { + Debug.Assert(Type != null); + + // Resolve any tagged polymorphism configuration: Options config takes precedence over attribute config + foreach (TypeDiscriminatorConfiguration config in Options?.TypeDiscriminatorConfigurations ?? Array.Empty()) + { + if (config.BaseType == Type) + { + TypeDiscriminatorResolver = new TypeDiscriminatorResolver(config); + break; + } + } + + TypeDiscriminatorResolver ??= TypeDiscriminatorResolver.CreateFromAttributes(Type); + + // Resolve polymorphic serialization configuration + CanBeWritePolymorphic = + Type == JsonTypeInfo.ObjectType || + !Type.IsValueType && !Type.IsSealed && + (Options?.SupportedPolymorphicTypes(Type) == true || + Type.GetCustomAttribute(inherit:false) is not null); + + // For the HandleNull == false case, either: + // 1) The default values are assigned in this type's virtual HandleNull property + // or + // 2) A converter overroad HandleNull and returned false so HandleNullOnRead and HandleNullOnWrite + // will be their default values of false. + CanUseDirectReadOrWrite = + !CanBePolymorphic + && isInternalConverter + && converterStrategy == ConverterStrategy.Value; + } + private static JsonParameterInfo AddConstructorParameter( ParameterInfo parameterInfo, JsonPropertyInfo jsonPropertyInfo, @@ -557,56 +599,12 @@ private static JsonConverter GetConverter( Type type, Type? parentClassType, MemberInfo? memberInfo, - out Type runtimeType, JsonSerializerOptions options) { Debug.Assert(type != null); ValidateType(type, parentClassType, memberInfo, options); JsonConverter converter = options.DetermineConverter(parentClassType, type, memberInfo); - - // The runtimeType is the actual value being assigned to the property. - // There are three types to consider for the runtimeType: - // 1) The declared type (the actual property type). - // 2) The converter.TypeToConvert (the T value that the converter supports). - // 3) The converter.RuntimeType (used with interfaces such as IList). - - Type converterRuntimeType = converter.RuntimeType; - if (type == converterRuntimeType) - { - runtimeType = type; - } - else - { - if (type.IsInterface) - { - runtimeType = converterRuntimeType; - } - else if (converterRuntimeType.IsInterface) - { - runtimeType = type; - } - else - { - // Use the most derived version from the converter.RuntimeType or converter.TypeToConvert. - if (type.IsAssignableFrom(converterRuntimeType)) - { - runtimeType = converterRuntimeType; - } - else if (converterRuntimeType.IsAssignableFrom(type) || converter.TypeToConvert.IsAssignableFrom(type)) - { - runtimeType = type; - } - else - { - runtimeType = default!; - ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(type); - } - } - } - - Debug.Assert(!IsInvalidForSerialization(runtimeType)); - return converter; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/MemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/MemberAccessor.cs index d246b45bf9a003..0b0892b699cbdb 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/MemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/MemberAccessor.cs @@ -32,5 +32,19 @@ public abstract JsonTypeInfo.ParameterizedConstructorDelegate CreateFieldGetter(FieldInfo fieldInfo); public abstract Action CreateFieldSetter(FieldInfo fieldInfo); + + /// + /// Constructs a constructor delegate for declaring type using specified runtime type with default ctor. + /// + public static JsonTypeInfo.ConstructorDelegate? CreateConstructor(Type declaringType) + where TRuntimeType : new() + { + if (!declaringType.IsAssignableFrom(typeof(TRuntimeType))) + { + return null; + } + + return new JsonTypeInfo.ConstructorDelegate(() => new TRuntimeType()); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/TypeDiscriminatorResolver.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/TypeDiscriminatorResolver.cs new file mode 100644 index 00000000000000..27f1612ce0cf0b --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/TypeDiscriminatorResolver.cs @@ -0,0 +1,153 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Text.Json.Serialization.Metadata +{ + /// + /// Type used to hold tagged polymorphic serialization metadata for a given base type. + /// + internal sealed class TypeDiscriminatorResolver + { + private readonly Type _baseType; + // TypeId -> Type map; is not modified by the object + private readonly Dictionary _typeIdToType = new(); + // Type -> (KnownType, TypeId)? map; used as a cache for the subtype hierarchy so can be modified during the object's lifetime. + // `null` values denote a subtype that is not associated with any known type (we want to cache negative results as well). + private readonly ConcurrentDictionary _typeToTypeId = new(); + + private sealed class CachedTypeResolution + { + public CachedTypeResolution(Type type, string typeIdentifier, CachedTypeResolution? conflictingResolution) + { + Type = type; + TypeIdentifier = typeIdentifier; + ConflictingResolution = conflictingResolution; + } + + public Type Type { get; } + public string TypeIdentifier { get; } + public CachedTypeResolution? ConflictingResolution { get; } + } + + public TypeDiscriminatorResolver(TypeDiscriminatorConfiguration configuration) + { + Debug.Assert(!configuration.BaseType.IsValueType && !configuration.BaseType.IsSealed); + + _baseType = configuration.BaseType; + + foreach (KeyValuePair kvp in configuration) + { + _typeToTypeId[kvp.Key] = new CachedTypeResolution(kvp.Key, kvp.Value, conflictingResolution: null); + _typeIdToType.Add(kvp.Value, kvp.Key); + } + } + + public static TypeDiscriminatorResolver? CreateFromAttributes(Type baseType) + { + if (!TypeDiscriminatorConfiguration.TryCreateFromKnownTypeAttributes(baseType, out TypeDiscriminatorConfiguration? config)) + { + return null; + } + + return new TypeDiscriminatorResolver(config); + } + + /// + /// Used during polymorphic deserialization to recover the subtype corresponding to the supplied type id. + /// + public bool TryResolveTypeByTypeId(string typeId, [NotNullWhen(true)] out Type? result) => _typeIdToType.TryGetValue(typeId, out result); + + /// + /// Used during polymorphic serialization to recover the type identifier as well as the actual subtype to be used. + /// The converter type we end up using could be different to the runtime type, for instance given the types + /// Baz : Bar : Foo + /// with `Bar` being the only declared known type, then an instance of type `Baz` should be serialized using the `Bar` converter. + /// + public bool TryResolvePolymorphicSubtype(Type type, [NotNullWhen(true)] out Type? resolvedType, [NotNullWhen(true)] out string? typeIdentifier) + { + Debug.Assert(!type.IsInterface && !type.IsAbstract, "input should always be the reflected type of a serialized object."); + Debug.Assert(_baseType.IsAssignableFrom(type)); + + // check the cache for existing resolutions first + if (!_typeToTypeId.TryGetValue(type, out var result)) + { + result = ComputeResolution(type); + } + + // there is no type discriminator resolution + if (result is null) + { + resolvedType = null; + typeIdentifier = null; + return false; + } + + // there is a diamond in the type discriminator resolution + if (result.ConflictingResolution is not null) + { + throw new NotSupportedException($"Type implements conflicting type discriminators {result.TypeIdentifier} and {result.ConflictingResolution.TypeIdentifier}."); + } + + resolvedType = result.Type; + typeIdentifier = result.TypeIdentifier; + return true; + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern", + Justification = "The 'type' must exist and so trimmer kept it. In which case " + + "It also kept it on any type which implements it. The below call to GetInterfaces " + + "may return fewer results when trimmed but it will return the 'interfaceType' " + + "if the type implemented it, even after trimming.")] + private CachedTypeResolution? ComputeResolution(Type type) + { + // walk up the inheritance hierarchy until the + // nearest ancestor with a known type association is dicovered. + + CachedTypeResolution? result = null; + + for (Type? candidate = type.BaseType; _baseType.IsAssignableFrom(candidate); candidate = candidate.BaseType) + { + Debug.Assert(candidate != null); + + if (_typeToTypeId.TryGetValue(candidate, out result)) + { + break; + } + } + + // Interface hierarchies admit the possibility of diamond ambiguities in type discriminators + // iterate through all interface implementations and identify potential conflicts. + if (result?.ConflictingResolution is null && _baseType.IsInterface) + { + foreach (Type interfaceTy in type.GetInterfaces()) + { + if (_baseType.IsAssignableFrom(interfaceTy) && + _typeToTypeId.TryGetValue(interfaceTy, out var interfaceResult) && + interfaceResult is not null) + { + // interface inheritance hierarchies cannot contain diamonds + Debug.Assert(interfaceResult.ConflictingResolution is null); + + if (result is null) + { + result = interfaceResult; + } + else if (result.Type != interfaceResult.Type) + { + result = new CachedTypeResolution(result.Type, result.TypeIdentifier, interfaceResult); + } + } + } + } + + // cache the result for future use + _typeToTypeId[type] = result; + return result; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/MetadataPropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/MetadataPropertyName.cs index 4b9b0fa29a9422..b63a5c7beffe91 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/MetadataPropertyName.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/MetadataPropertyName.cs @@ -3,11 +3,13 @@ namespace System.Text.Json { + [Flags] internal enum MetadataPropertyName { - NoMetadata, - Values, - Id, - Ref, + NoMetadata = 0, + Values = 1, + Id = 2, + Ref = 4, + Type = 8, } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PolymorphicSerializationState.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PolymorphicSerializationState.cs new file mode 100644 index 00000000000000..735c10e22af990 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PolymorphicSerializationState.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 +{ + internal enum PolymorphicSerializationState : byte + { + None, + + /// + /// Dispatch to a polymorphic converter has been initiated. + /// + PolymorphicReEntryStarted, + + /// + /// Current frame is a continuation using a suspended polymorphic converter. + /// + PolymorphicReEntrySuspended + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs index 2f0347f68332ae..45abb5919e9a2c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs @@ -16,16 +16,32 @@ internal struct ReadStack internal static readonly char[] SpecialCharacters = { '.', ' ', '\'', '/', '"', '[', ']', '(', ')', '\t', '\n', '\r', '\f', '\b', '\\', '\u0085', '\u2028', '\u2029' }; /// - /// The number of stack frames when the continuation started. + /// Exposes the stackframe that is currently active. /// - private int _continuationCount; + public ReadStackFrame Current; + + /// + /// Stack buffer containing all frames in the stack. For performance it is only populated for serialization depths > 1. + /// + private ReadStackFrame[] _stack; /// - /// The number of stack frames including Current. _previous will contain _count-1 higher frames. + /// Tracks the current depth of the stack. /// private int _count; - private List _previous; + /// + /// If not zero, indicates that the stack is part of a re-entrant continuation of given depth. + /// + private int _continuationCount; + + /// + /// Offset used to derive the index of the current frame in the stack buffer from the current value of , + /// following the formula currentIndex := _count - _indexOffset. + /// Value can vary between 0 or 1 depending on whether we need to allocate a new frame on the first Push() operation, + /// which can happen if the root converter is polymorphic. + /// + private int _indexOffset; // State cache when deserializing objects with parameterized constructors. private List? _ctorArgStateCache; @@ -35,17 +51,21 @@ internal struct ReadStack /// public long BytesConsumed; - // A field is used instead of a property to avoid value semantics. - public ReadStackFrame Current; - + /// + /// Indicates that the state still contains suspended frames waiting re-entry. + /// public bool IsContinuation => _continuationCount != 0; - public bool IsLastContinuation => _continuationCount == _count; /// /// Internal flag to let us know that we need to read ahead in the inner read loop. /// public bool ReadAhead; + /// + /// Flag indicating that the reader should not advance to the next token on resumption of a continuation. + /// + public bool NoReaderAdvanceOnContinuation; + // The bag of preservable references. public ReferenceResolver ReferenceResolver; @@ -55,29 +75,25 @@ internal struct ReadStack public bool SupportContinuation; /// - /// Whether we can read without the need of saving state for stream and preserve references cases. + /// Global flag indicating whether we can read without the need of saving state for stream and preserve references cases. + /// Note that this flag should be consulted in conjunction with . /// public bool UseFastPath; - private void AddCurrent() + /// + /// Returns true if the current frame has been pushed by a polymorphic converter. + /// + public bool CanContainPolymorphismMetadata { - if (_previous == null) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { - _previous = new List(); - } - - if (_count > _previous.Count) - { - // Need to allocate a new array element. - _previous.Add(Current); - } - else - { - // Use a previously allocated slot. - _previous[_count - 1] = Current; + Debug.Assert(_count > 0); + int currentIndex = _count - _indexOffset; + return currentIndex > 0 && + _stack[currentIndex - 1].PolymorphicSerializationState == + PolymorphicSerializationState.PolymorphicReEntryStarted; } - - _count++; } public void Initialize(Type type, JsonSerializerOptions options, bool supportContinuation) @@ -89,10 +105,8 @@ public void Initialize(Type type, JsonSerializerOptions options, bool supportCon internal void Initialize(JsonTypeInfo jsonTypeInfo, bool supportContinuation = false) { Current.JsonTypeInfo = jsonTypeInfo; - // The initial JsonPropertyInfo will be used to obtain the converter. Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo; - Current.NumberHandling = Current.JsonPropertyInfo.NumberHandling; JsonSerializerOptions options = jsonTypeInfo.Options; @@ -106,45 +120,42 @@ internal void Initialize(JsonTypeInfo jsonTypeInfo, bool supportContinuation = f UseFastPath = !supportContinuation && !preserveReferences; } + /// + /// Used to obtain the JsonTypeInfo for the current converter _before_ the call to ReadStack.Push() has been made. + /// + public JsonTypeInfo PeekNextJsonTypeInfo() + { + if (_count == 0 && Current.PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted) + { + // We're peeking the root context, simply return the current JsonTypeInfo. + return Current.JsonTypeInfo; + } + + return Current.GetPolymorphicJsonTypeInfo(); + } + public void Push() { if (_continuationCount == 0) { - if (_count == 0) + Debug.Assert(Current.PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntrySuspended); + + if (_count == 0 && Current.PolymorphicSerializationState == PolymorphicSerializationState.None) { - // The first stack frame is held in Current. + // Perf enhancement: do not create a new stackframe on the first push operation + // unless the converter has primed the current frame for polymorphic dispatch. _count = 1; + _indexOffset = 1; // currentIndex := _count - 1; } else { - JsonTypeInfo jsonTypeInfo; + JsonTypeInfo jsonTypeInfo = Current.GetPolymorphicJsonTypeInfo(); JsonNumberHandling? numberHandling = Current.NumberHandling; - ConverterStrategy converterStrategy = Current.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterStrategy; - if (converterStrategy == ConverterStrategy.Object) - { - if (Current.JsonPropertyInfo != null) - { - jsonTypeInfo = Current.JsonPropertyInfo.RuntimeTypeInfo; - } - else - { - jsonTypeInfo = Current.CtorArgumentState!.JsonParameterInfo!.RuntimeTypeInfo; - } - } - else if (converterStrategy == ConverterStrategy.Value) - { - // Although ConverterStrategy.Value doesn't push, a custom custom converter may re-enter serialization. - jsonTypeInfo = Current.JsonPropertyInfo!.RuntimeTypeInfo; - } - else - { - Debug.Assert(((ConverterStrategy.Enumerable | ConverterStrategy.Dictionary) & converterStrategy) != 0); - jsonTypeInfo = Current.JsonTypeInfo.ElementTypeInfo!; - } - - AddCurrent(); - Current.Reset(); + EnsurePushCapacity(); + _stack[_count - _indexOffset] = Current; + Current = default; + _count++; Current.JsonTypeInfo = jsonTypeInfo; Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo; @@ -152,82 +163,87 @@ public void Push() Current.NumberHandling = numberHandling ?? Current.JsonPropertyInfo.NumberHandling; } } - else if (_continuationCount == 1) - { - // No need for a push since there is only one stack frame. - Debug.Assert(_count == 1); - _continuationCount = 0; - } else { - // A continuation; adjust the index. - Current = _previous[_count - 1]; + // We are re-entering a continuation, adjust indices accordingly. - // Check if we are done. - if (_count == _continuationCount) + if (_count++ > 0 || _indexOffset == 0) { - _continuationCount = 0; + Current = _stack[_count - _indexOffset]; } - else + + // check if we are done + if (_continuationCount == _count) { - _count++; + _continuationCount = 0; } } SetConstructorArgumentState(); + Debug.Assert(JsonPath() is not null); } public void Pop(bool success) { Debug.Assert(_count > 0); + Debug.Assert(JsonPath() is not null); if (!success) { // Check if we need to initialize the continuation. if (_continuationCount == 0) { - if (_count == 1) + if (_count == 1 && _indexOffset > 0) { - // No need for a continuation since there is only one stack frame. + // No need to copy any frames here. _continuationCount = 1; - } - else - { - AddCurrent(); - _count--; - _continuationCount = _count; - _count--; - Current = _previous[_count - 1]; + _count = 0; + return; } - return; + // Need to push the Current frame to the stack, + // ensure that we have sufficient capacity. + EnsurePushCapacity(); + _continuationCount = _count--; } - - if (_continuationCount == 1) + else if (--_count == 0 && _indexOffset > 0) { - // No need for a pop since there is only one stack frame. - Debug.Assert(_count == 1); + // No need to copy any frames here. return; } - // Update the list entry to the current value. - _previous[_count - 1] = Current; - - Debug.Assert(_count > 0); + int currentIndex = _count - _indexOffset; + _stack[currentIndex + 1] = Current; + Current = _stack[currentIndex]; } else { Debug.Assert(_continuationCount == 0); - } - if (_count > 1) - { - Current = _previous[--_count -1]; + if (--_count > 0 || _indexOffset == 0) + { + Current = _stack[_count - _indexOffset]; + } } SetConstructorArgumentState(); } + /// + /// Ensures that the stack buffer has sufficient capacity to hold an additional frame. + /// + private void EnsurePushCapacity() + { + if (_stack is null) + { + _stack = new ReadStackFrame[4]; + } + else if (_count - _indexOffset == _stack.Length) + { + Array.Resize(ref _stack, 2 * _stack.Length); + } + } + // Return a JSONPath using simple dot-notation when possible. When special characters are present, bracket-notation is used: // $.x.y[0].z // $['PropertyName.With.Special.Chars'] @@ -238,28 +254,27 @@ public string JsonPath() // If a continuation, always report back full stack. int count = Math.Max(_count, _continuationCount); - for (int i = 0; i < count - 1; i++) + for (int i = 1; i < count; i++) { - AppendStackFrame(sb, _previous[i]); + AppendStackFrame(sb, ref _stack[i - _indexOffset]); } if (_continuationCount == 0) { - AppendStackFrame(sb, Current); + AppendStackFrame(sb, ref Current); } return sb.ToString(); - static void AppendStackFrame(StringBuilder sb, in ReadStackFrame frame) + static void AppendStackFrame(StringBuilder sb, ref ReadStackFrame frame) { // Append the property name. - string? propertyName = GetPropertyName(frame); + string? propertyName = GetPropertyName(ref frame); AppendPropertyName(sb, propertyName); if (frame.JsonTypeInfo != null && frame.IsProcessingEnumerable()) { - IEnumerable? enumerable = (IEnumerable?)frame.ReturnValue; - if (enumerable == null) + if (frame.ReturnValue is not IEnumerable enumerable) { return; } @@ -311,7 +326,7 @@ static void AppendPropertyName(StringBuilder sb, string? propertyName) } } - static string? GetPropertyName(in ReadStackFrame frame) + static string? GetPropertyName(ref ReadStackFrame frame) { string? propertyName = null; @@ -350,11 +365,7 @@ private void SetConstructorArgumentState() // A zero index indicates a new stack frame. if (Current.CtorArgumentStateIndex == 0) { - if (_ctorArgStateCache == null) - { - _ctorArgStateCache = new List(); - } - + _ctorArgStateCache ??= new List(); var newState = new ArgumentState(); _ctorArgStateCache.Add(newState); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs index f4f64dcbb1a51d..1f9a9ca4f7fa93 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs @@ -34,6 +34,17 @@ internal struct ReadStackFrame public JsonTypeInfo JsonTypeInfo; public StackFrameObjectState ObjectState; // State tracking the current object. + private JsonPropertyInfo? CachedPolymorphicJsonPropertyInfo; + + /// + /// Dictates how is to be consumed. + /// + /// + /// If true we are dispatching serialization to a polymorphic converter that should consume it. + /// If false it is simply a value we are caching for performance. + /// + public PolymorphicSerializationState PolymorphicSerializationState; + // Validate EndObject token on array with preserve semantics. public bool ValidateEndTokenOnArray; @@ -48,6 +59,79 @@ internal struct ReadStackFrame // Whether to use custom number handling. public JsonNumberHandling? NumberHandling; + /// + /// Return the property that contains the correct polymorphic properties including + /// the ConverterStrategy and ConverterBase. + /// + public JsonTypeInfo GetPolymorphicJsonTypeInfo() + { + JsonTypeInfo? jsonTypeInfo; + + if (PolymorphicSerializationState == PolymorphicSerializationState.PolymorphicReEntryStarted) + { + Debug.Assert(CachedPolymorphicJsonPropertyInfo is not null); + jsonTypeInfo = CachedPolymorphicJsonPropertyInfo.RuntimeTypeInfo; + } + else + { + ConverterStrategy converterStrategy = JsonTypeInfo.PropertyInfoForTypeInfo.ConverterStrategy; + + if (converterStrategy == ConverterStrategy.Object) + { + if (JsonPropertyInfo is not null) + { + jsonTypeInfo = JsonPropertyInfo.RuntimeTypeInfo; + } + else + { + jsonTypeInfo = CtorArgumentState!.JsonParameterInfo!.RuntimeTypeInfo; + } + } + else if (converterStrategy == ConverterStrategy.Value) + { + // Although ConverterStrategy.Value doesn't push, a custom custom converter may re-enter serialization. + Debug.Assert(JsonPropertyInfo is not null); + jsonTypeInfo = JsonPropertyInfo.RuntimeTypeInfo; + } + else + { + Debug.Assert(((ConverterStrategy.Enumerable | ConverterStrategy.Dictionary) & converterStrategy) != 0); + Debug.Assert(JsonTypeInfo.ElementTypeInfo is not null); + jsonTypeInfo = JsonTypeInfo.ElementTypeInfo; + } + } + + return jsonTypeInfo; + } + + /// + /// Initializes the state for polymorphic cases and returns the appropriate converter. + /// + public JsonConverter InitializePolymorphicReEntry(Type type, JsonSerializerOptions options) + { + Debug.Assert(PolymorphicSerializationState == PolymorphicSerializationState.None); + + // For perf, avoid the dictionary lookup in GetOrAddClass() for every element of a collection + // if the current element is the same type as the previous element. + if (CachedPolymorphicJsonPropertyInfo?.RuntimePropertyType != type) + { + JsonTypeInfo typeInfo = options.GetOrAddClass(type); + CachedPolymorphicJsonPropertyInfo = typeInfo.PropertyInfoForTypeInfo; + } + + PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryStarted; + return CachedPolymorphicJsonPropertyInfo.ConverterBase; + } + + public JsonConverter ResumePolymorphicReEntry() + { + Debug.Assert(PolymorphicSerializationState == PolymorphicSerializationState.PolymorphicReEntrySuspended); + Debug.Assert(CachedPolymorphicJsonPropertyInfo is not null); + + PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryStarted; + return CachedPolymorphicJsonPropertyInfo.ConverterBase; + } + public void EndConstructorParameter() { CtorArgumentState!.JsonParameterInfo = null; @@ -62,6 +146,8 @@ public void EndProperty() JsonPropertyNameAsString = null; PropertyState = StackFramePropertyState.None; ValidateEndTokenOnArray = false; + CachedPolymorphicJsonPropertyInfo = null; + PolymorphicSerializationState = PolymorphicSerializationState.None; // No need to clear these since they are overwritten each time: // NumberHandling @@ -90,19 +176,24 @@ public bool IsProcessingEnumerable() return (JsonTypeInfo.PropertyInfoForTypeInfo.ConverterStrategy & ConverterStrategy.Enumerable) != 0; } - public void Reset() - { - CtorArgumentStateIndex = 0; - CtorArgumentState = null; - JsonTypeInfo = null!; - ObjectState = StackFrameObjectState.None; - OriginalDepth = 0; - OriginalTokenType = JsonTokenType.None; - PropertyIndex = 0; - PropertyRefCache = null; - ReturnValue = null; - - EndProperty(); - } + //public void Reset() + //{ + // CtorArgumentStateIndex = 0; + // CtorArgumentState = null; + // JsonTypeInfo = null!; + // ObjectState = StackFrameObjectState.None; + // OriginalDepth = 0; + // OriginalTokenType = JsonTokenType.None; + // PropertyIndex = 0; + // PropertyRefCache = null; + // ReturnValue = null; + // DictionaryKey = null; + // UseExtensionProperty = false; + // NumberHandling = null; + + // EndProperty(); + + // Debug.Assert(EqualityComparer.Default.Equals(this, default)); + //} } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFrameObjectState.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFrameObjectState.cs index dca579f83229fa..ee3091116cab55 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFrameObjectState.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFrameObjectState.cs @@ -18,8 +18,10 @@ internal enum StackFrameObjectState : byte ReadAheadIdValue, // Try to move the reader to the value for $id. ReadAheadRefValue, // Try to move the reader to the value for $ref. + ReadAheadTypeValue, // Try to move the reader to the value for $type. ReadIdValue, // Read value for $id. ReadRefValue, // Read value for $ref. + ReadTypeValue, // Read value for $type. ReadAheadRefEndObject, // Try to move the reader to the EndObject for $ref. ReadRefEndObject, // Read the EndObject for $ref. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/TypeDiscriminatorConfiguration.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/TypeDiscriminatorConfiguration.cs new file mode 100644 index 00000000000000..b19777cf5278eb --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/TypeDiscriminatorConfiguration.cs @@ -0,0 +1,143 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization.Metadata; + +namespace System.Text.Json.Serialization +{ + /// + /// Defines polymorphic type discriminator configuration for a given type. + /// + public class TypeDiscriminatorConfiguration : IReadOnlyCollection> + { + private Dictionary _knownTypes = new(); + + /// + /// Creates a new type discriminator configuration instance for a given base type. + /// + /// The base type for which to configure polymorphic serialization. + public TypeDiscriminatorConfiguration(Type baseType) + { + if (!SupportsTypeDiscriminators(baseType)) + { + throw new ArgumentException("The specified base type does not support known types configuration.", nameof(baseType)); + } + + BaseType = baseType; + } + + /// The base type for which type discriminator configuration is specified. + public Type BaseType { get; } + + /// + /// Associates specified derived type with supplied string identifier. + /// + /// The derived type with which to associate a type identifier. + /// The type identifier to use for the specified dervied type. + /// The same instance after it has been updated. + public TypeDiscriminatorConfiguration WithKnownType(Type derivedType, string identifier) + { + VerifyMutable(); + + if (!BaseType.IsAssignableFrom(derivedType)) + { + throw new ArgumentException("Specified type is not assignable to the base type.", nameof(derivedType)); + } + + // TODO: this check might be removed depending on final type discriminator semantics + if (derivedType == BaseType) + { + throw new ArgumentException("Specified type must be a proper subtype of the base type.", nameof(derivedType)); + } + + if (_knownTypes.ContainsKey(derivedType)) + { + throw new ArgumentException("Specified type has already been assigned as a known type.", nameof(derivedType)); + } + + // linear traversal is probably appropriate here, but might consider using a HashSet storing the id's + foreach (string id in _knownTypes.Values) + { + if (id == identifier) + { + throw new ArgumentException("A subtype with specified identifier has already been registered.", nameof(identifier)); + } + } + + _knownTypes.Add(derivedType, identifier); + + return this; + } + + internal static bool TryCreateFromKnownTypeAttributes(Type baseType, [NotNullWhen(true)] out TypeDiscriminatorConfiguration? config) + { + if (!SupportsTypeDiscriminators(baseType)) + { + config = null; + return false; + } + + object[] attributes = baseType.GetCustomAttributes(typeof(JsonKnownTypeAttribute), inherit: false); + + if (attributes.Length == 0) + { + config = null; + return false; + } + + var cfg = new TypeDiscriminatorConfiguration(baseType); + foreach (JsonKnownTypeAttribute attribute in attributes) + { + cfg.WithKnownType(attribute.Subtype, attribute.Identifier); + } + + config = cfg; + return true; + } + + internal bool IsAssignedToOptionsInstance { get; set; } + + private void VerifyMutable() + { + if (IsAssignedToOptionsInstance) + { + ThrowHelper.ThrowInvalidOperationException_SerializerOptionsImmutable(null); + } + } + + private static bool SupportsTypeDiscriminators(Type type) => !type.IsGenericTypeDefinition && !type.IsValueType && !type.IsSealed && type != JsonTypeInfo.ObjectType; + + int IReadOnlyCollection>.Count => _knownTypes.Count; + IEnumerator> IEnumerable>.GetEnumerator() => _knownTypes.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _knownTypes.GetEnumerator(); + } + + /// + /// Contains type discriminator metadata configuration for a given type. + /// + /// The type for which type discriminator configuration is provided. + public class TypeDiscriminatorConfiguration : TypeDiscriminatorConfiguration where TBaseType : class + { + /// + /// Creates a new type discriminator metadata configuration instance for a given type. + /// + public TypeDiscriminatorConfiguration() : base(typeof(TBaseType)) + { + } + + /// + /// Associates specified derived type with supplied string identifier. + /// + /// The derived type with which to associate a type identifier. + /// The type identifier to use for the specified dervied type. + /// The same instance after it has been updated. + public TypeDiscriminatorConfiguration WithKnownType(string identifier) where TDerivedType : TBaseType + { + WithKnownType(typeof(TDerivedType), identifier); + return this; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs index 22f67983ec3463..c1d272b71e9c03 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -16,15 +17,33 @@ namespace System.Text.Json internal struct WriteStack { /// - /// The number of stack frames when the continuation started. + /// Exposes the stackframe that is currently active. /// - private int _continuationCount; + public WriteStackFrame Current; + + /// + /// Stack buffer containing all frames in the stack. For performance it is only populated for serialization depths > 1. + /// + private WriteStackFrame[] _stack; /// - /// The number of stack frames including Current. _previous will contain _count-1 higher frames. + /// Tracks the current depth of the stack. /// private int _count; + /// + /// If not zero, indicates that the stack is part of a re-entrant continuation of given depth. + /// + private int _continuationCount; + + /// + /// Offset used to derive the index of the current frame in the stack buffer from the current value of , + /// following the formula currentIndex := _count - _indexOffset. + /// Value can vary between 0 or 1 depending on whether we need to allocate a new frame on the first Push() operation, + /// which can happen if the root converter is polymorphic. + /// + private int _indexOffset; + /// /// Cancellation token used by converters performing async serialization (e.g. IAsyncEnumerable) /// @@ -41,11 +60,6 @@ internal struct WriteStack /// public List? PendingAsyncDisposables; - private List _previous; - - // A field is used instead of a property to avoid value semantics. - public WriteStackFrame Current; - /// /// The amount of bytes to write before the underlying Stream should be flushed and the /// current buffer adjusted to remove the processed bytes. @@ -54,6 +68,11 @@ internal struct WriteStack public bool IsContinuation => _continuationCount != 0; + /// + /// Indicates that the next converter is polymorphic and must serialize a type discriminator. + /// + public string? PolymorphicTypeDiscriminator; + // The bag of preservable references. public ReferenceResolver ReferenceResolver; @@ -62,27 +81,6 @@ internal struct WriteStack /// public bool SupportContinuation; - private void AddCurrent() - { - if (_previous == null) - { - _previous = new List(); - } - - if (_count > _previous.Count) - { - // Need to allocate a new array element. - _previous.Add(Current); - } - else - { - // Use a previously allocated slot. - _previous[_count - 1] = Current; - } - - _count++; - } - /// /// Initialize the state without delayed initialization of the JsonTypeInfo. /// @@ -111,22 +109,42 @@ internal JsonConverter Initialize(JsonTypeInfo jsonTypeInfo, bool supportContinu return jsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase; } + /// + /// Used to obtain the JsonTypeInfo for the current converter _before_ the call to WriteStack.Push() has been made. + /// + public JsonTypeInfo PeekNextJsonTypeInfo() + { + if (_count == 0 && Current.PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted) + { + return Current.JsonTypeInfo; + } + + return Current.GetPolymorphicJsonPropertyInfo().RuntimeTypeInfo; + } + public void Push() { if (_continuationCount == 0) { - if (_count == 0) + Debug.Assert(Current.PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntrySuspended); + + if (_count == 0 && Current.PolymorphicSerializationState == PolymorphicSerializationState.None) { - // The first stack frame is held in Current. + // Perf enhancement: do not create a new stackframe on the first push operation + // unless the converter has primed the current frame for polymorphic dispatch. _count = 1; + _indexOffset = 1; // currentIndex := _count - 1; } else { - JsonTypeInfo jsonTypeInfo = Current.GetPolymorphicJsonPropertyInfo().RuntimeTypeInfo; + JsonPropertyInfo jsonPropertyInfo = Current.GetPolymorphicJsonPropertyInfo(); + JsonTypeInfo jsonTypeInfo = jsonPropertyInfo.RuntimeTypeInfo; JsonNumberHandling? numberHandling = Current.NumberHandling; - AddCurrent(); - Current.Reset(); + EnsurePushCapacity(); + _stack[_count - _indexOffset] = Current; + Current = default; + _count++; Current.JsonTypeInfo = jsonTypeInfo; Current.DeclaredJsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo; @@ -134,67 +152,58 @@ public void Push() Current.NumberHandling = numberHandling ?? Current.DeclaredJsonPropertyInfo.NumberHandling; } } - else if (_continuationCount == 1) - { - // No need for a push since there is only one stack frame. - Debug.Assert(_count == 1); - _continuationCount = 0; - } else { - // A continuation, adjust the index. - Current = _previous[_count - 1]; + // We are re-entering a continuation, adjust indices accordingly - // Check if we are done. - if (_count == _continuationCount) + if (_count++ > 0 || _indexOffset == 0) { - _continuationCount = 0; + Current = _stack[_count - _indexOffset]; } - else + + // check if we are done + if (_continuationCount == _count) { - _count++; + _continuationCount = 0; } } + + Debug.Assert(PropertyPath() is not null); } public void Pop(bool success) { Debug.Assert(_count > 0); + Debug.Assert(PropertyPath() is not null); if (!success) { // Check if we need to initialize the continuation. if (_continuationCount == 0) { - if (_count == 1) + if (_count == 1 && _indexOffset > 0) { - // No need for a continuation since there is only one stack frame. + // No need to copy any frames here. _continuationCount = 1; - _count = 1; - } - else - { - AddCurrent(); - _count--; - _continuationCount = _count; - _count--; - Current = _previous[_count - 1]; + _count = 0; + return; } - return; - } - if (_continuationCount == 1) + // Need to push the Current frame to the stack, + // ensure that we have sufficient capacity. + EnsurePushCapacity(); + _continuationCount = _count--; + } + else if (--_count == 0 && _indexOffset > 0) { - // No need for a pop since there is only one stack frame. - Debug.Assert(_count == 1); + // No need to copy any frames here. return; } - // Update the list entry to the current value. - _previous[_count - 1] = Current; - - Debug.Assert(_count > 0); + int currentIndex = _count - _indexOffset; + _stack[currentIndex + 1] = Current; + Current = _stack[currentIndex]; } else { @@ -207,11 +216,23 @@ public void Pop(bool success) PendingAsyncDisposables ??= new List(); PendingAsyncDisposables.Add(Current.AsyncEnumerator); } + + if (--_count > 0 || _indexOffset == 0) + { + Current = _stack[_count - _indexOffset]; + } } + } - if (_count > 1) + private void EnsurePushCapacity() + { + if (_stack is null) + { + _stack = new WriteStackFrame[4]; + } + else if (_count - _indexOffset == _stack.Length) { - Current = _previous[--_count - 1]; + Array.Resize(ref _stack, 2 * _stack.Length); } } @@ -253,13 +274,12 @@ public void DisposePendingDisposablesOnException() DisposeFrame(Current.CollectionEnumerator, ref exception); int stackSize = Math.Max(_count, _continuationCount); - if (stackSize > 1) + + for (int i = 1; i < stackSize; i++) { - for (int i = 0; i < stackSize - 1; i++) - { - Debug.Assert(_previous[i].AsyncEnumerator is null); - DisposeFrame(_previous[i].CollectionEnumerator, ref exception); - } + ref WriteStackFrame frame = ref _stack[i - _indexOffset]; + Debug.Assert(frame.AsyncEnumerator is null); + DisposeFrame(frame.CollectionEnumerator, ref exception); } if (exception is not null) @@ -294,12 +314,10 @@ public async ValueTask DisposePendingDisposablesOnExceptionAsync() exception = await DisposeFrame(Current.CollectionEnumerator, Current.AsyncEnumerator, exception).ConfigureAwait(false); int stackSize = Math.Max(_count, _continuationCount); - if (stackSize > 1) + for (int i = 1; i < stackSize; i++) { - for (int i = 0; i < stackSize - 1; i++) - { - exception = await DisposeFrame(_previous[i].CollectionEnumerator, _previous[i].AsyncEnumerator, exception).ConfigureAwait(false); - } + WriteStackFrame frame = _stack[i - _indexOffset]; + exception = await DisposeFrame(frame.CollectionEnumerator, frame.AsyncEnumerator, exception).ConfigureAwait(false); } if (exception is not null) @@ -341,19 +359,19 @@ public string PropertyPath() // If a continuation, always report back full stack. int count = Math.Max(_count, _continuationCount); - for (int i = 0; i < count - 1; i++) + for (int i = 1; i < count; i++) { - AppendStackFrame(sb, _previous[i]); + AppendStackFrame(sb, ref _stack[i - _indexOffset]); } if (_continuationCount == 0) { - AppendStackFrame(sb, Current); + AppendStackFrame(sb, ref Current); } return sb.ToString(); - void AppendStackFrame(StringBuilder sb, in WriteStackFrame frame) + static void AppendStackFrame(StringBuilder sb, ref WriteStackFrame frame) { // Append the property name. string? propertyName = frame.DeclaredJsonPropertyInfo?.MemberInfo?.Name; @@ -366,7 +384,7 @@ void AppendStackFrame(StringBuilder sb, in WriteStackFrame frame) AppendPropertyName(sb, propertyName); } - void AppendPropertyName(StringBuilder sb, string? propertyName) + static void AppendPropertyName(StringBuilder sb, string? propertyName) { if (propertyName != null) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs index b162b55709c300..595c20074bde02 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; +using System.Collections.Generic; using System.Diagnostics; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -27,6 +28,11 @@ internal struct WriteStackFrame /// public bool AsyncEnumeratorIsPendingCompletion; + /// + /// Flag indicating that the current value has been pushed for cycle detection. + /// + public bool IsCycleDetectionReferencePushed; + /// /// The original JsonPropertyInfo that is not changed. It contains all properties. /// @@ -78,7 +84,16 @@ internal struct WriteStackFrame /// For objects, it is the for the class and current property. /// For collections, it is the for the class and current element. /// - private JsonPropertyInfo? PolymorphicJsonPropertyInfo; + private JsonPropertyInfo? CachedPolymorphicJsonPropertyInfo; + + /// + /// Dictates how is to be consumed. + /// + /// + /// If true we are dispatching serialization to a polymorphic converter that should consume it. + /// If false it is simply a value we are caching for performance. + /// + public PolymorphicSerializationState PolymorphicSerializationState; // Whether to use custom number handling. public JsonNumberHandling? NumberHandling; @@ -92,8 +107,9 @@ public void EndProperty() { DeclaredJsonPropertyInfo = null!; JsonPropertyNameAsString = null; - PolymorphicJsonPropertyInfo = null; + CachedPolymorphicJsonPropertyInfo = null; PropertyState = StackFramePropertyState.None; + PolymorphicSerializationState = PolymorphicSerializationState.None; } /// @@ -102,38 +118,55 @@ public void EndProperty() /// public JsonPropertyInfo GetPolymorphicJsonPropertyInfo() { - return PolymorphicJsonPropertyInfo ?? DeclaredJsonPropertyInfo!; + return PolymorphicSerializationState == PolymorphicSerializationState.PolymorphicReEntryStarted ? CachedPolymorphicJsonPropertyInfo! : DeclaredJsonPropertyInfo!; } /// /// Initializes the state for polymorphic cases and returns the appropriate converter. /// - public JsonConverter InitializeReEntry(Type type, JsonSerializerOptions options) + public JsonConverter InitializePolymorphicReEntry(Type type, JsonSerializerOptions options) { + Debug.Assert(PolymorphicSerializationState == PolymorphicSerializationState.None); + // For perf, avoid the dictionary lookup in GetOrAddClass() for every element of a collection // if the current element is the same type as the previous element. - if (PolymorphicJsonPropertyInfo?.RuntimePropertyType != type) + if (CachedPolymorphicJsonPropertyInfo?.RuntimePropertyType != type) { JsonTypeInfo typeInfo = options.GetOrAddClass(type); - PolymorphicJsonPropertyInfo = typeInfo.PropertyInfoForTypeInfo; + CachedPolymorphicJsonPropertyInfo = typeInfo.PropertyInfoForTypeInfo; } - return PolymorphicJsonPropertyInfo.ConverterBase; + PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryStarted; + return CachedPolymorphicJsonPropertyInfo.ConverterBase; } - public void Reset() + public JsonConverter ResumePolymorphicReEntry() { - CollectionEnumerator = null; - EnumeratorIndex = 0; - AsyncEnumerator = null; - AsyncEnumeratorIsPendingCompletion = false; - IgnoreDictionaryKeyPolicy = false; - JsonTypeInfo = null!; - OriginalDepth = 0; - ProcessedStartToken = false; - ProcessedEndToken = false; - - EndProperty(); + Debug.Assert(PolymorphicSerializationState == PolymorphicSerializationState.PolymorphicReEntrySuspended); + Debug.Assert(CachedPolymorphicJsonPropertyInfo is not null); + + PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryStarted; + return CachedPolymorphicJsonPropertyInfo.ConverterBase; } + + //public void Reset() + //{ + // CollectionEnumerator = null; + // EnumeratorIndex = 0; + // AsyncEnumerator = null; + // AsyncEnumeratorIsPendingCompletion = false; + // IgnoreDictionaryKeyPolicy = false; + // JsonTypeInfo = null!; + // OriginalDepth = 0; + // ProcessedStartToken = false; + // ProcessedEndToken = false; + // IsCycleDetectionReferencePushed = false; + // MetadataPropertyName = MetadataPropertyName.NoMetadata; + // NumberHandling = null; + + // EndProperty(); + + // Debug.Assert(EqualityComparer.Default.Equals(this, default)); + //} } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs index 06747c8778626f..0ac4ecb76fc58d 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs @@ -574,6 +574,13 @@ private static JsonSerializerOptions GetFullyPopulatedOptionsInstance() options.Converters.Add(new JsonStringEnumConverter()); options.Converters.Add(new ConverterForInt32()); } + else if (propertyType == typeof(IList)) + { + options.TypeDiscriminatorConfigurations.Add( + new TypeDiscriminatorConfiguration() + .WithKnownType("point_with_array") + .WithKnownType("point_with_dictionary")); + } else if (propertyType == typeof(JavaScriptEncoder)) { options.Encoder = JavaScriptEncoder.Default; @@ -587,6 +594,10 @@ private static JsonSerializerOptions GetFullyPopulatedOptionsInstance() { options.ReferenceHandler = ReferenceHandler.Preserve; } + else if (propertyType == typeof(Func)) + { + options.SupportedPolymorphicTypes = static type => type == typeof(object); + } else if (propertyType.IsValueType) { options.ReadCommentHandling = JsonCommentHandling.Disallow; @@ -619,7 +630,7 @@ private static void VerifyOptionsEqual(JsonSerializerOptions options, JsonSerial { Assert.Equal((int)property.GetValue(options), (int)property.GetValue(newOptions)); } - else if (typeof(IEnumerable).IsAssignableFrom(propertyType)) + else if (propertyType == typeof(IList)) { var list1 = (IList)property.GetValue(options); var list2 = (IList)property.GetValue(newOptions); @@ -630,6 +641,17 @@ private static void VerifyOptionsEqual(JsonSerializerOptions options, JsonSerial Assert.Same(list1[i], list2[i]); } } + else if (propertyType == typeof(IList)) + { + var list1 = (IList)property.GetValue(options); + var list2 = (IList)property.GetValue(newOptions); + + Assert.Equal(list1.Count, list2.Count); + for (int i = 0; i < list1.Count; i++) + { + Assert.Same(list1[i], list2[i]); + } + } else if (propertyType.IsValueType) { if (property.Name == "ReadCommentHandling") diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.cs index 00cb5ad1946342..27be02d39c260b 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.cs @@ -512,6 +512,273 @@ public async Task AnonymousType() Assert.Equal(Expected, json); } + [Fact] + public async Task ConcreteClass_RootValue_PolymorphismDisabled_ShouldSerializeAsBase() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = _ => false }; + ConcreteClass value = new DerivedClass { Boolean = true, Number = 42 }; + string expectedJson = @"{""Boolean"":true}"; + + string actualJson = await Serializer.SerializeWrapper(value, options); + + Assert.Equal(expectedJson, actualJson); + } + + [Fact] + public async Task ConcreteClass_RootValue_PolymorphismEnabled_ShouldSerializePolymorphically() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = type => type == typeof(ConcreteClass) }; + ConcreteClass value = new DerivedClass { Boolean = true, Number = 42 }; + string expectedJson = @"{""Number"":42,""Boolean"":true}"; + + string actualJson = await Serializer.SerializeWrapper(value, options); + + Assert.Equal(expectedJson, actualJson); + } + + [Fact] + public void ConcreteClass_RootValue_PolymorphismEnabled_ShouldDeserializeAsBase() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = type => type == typeof(ConcreteClass) }; + string json = @"{""Number"":42,""Boolean"":true}"; + + ConcreteClass result = JsonSerializer.Deserialize(json, options); + + Assert.IsType(result); + Assert.True(result.Boolean); + } + + [Fact] + public async Task ConcreteClass_NestedValue_PolymorphismDisabled_ShouldSerializeAsBase() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = _ => false }; + List value = new() { new DerivedClass { Boolean = true, Number = 42 } }; + string expectedJson = @"[{""Boolean"":true}]"; + + string actualJson = await Serializer.SerializeWrapper(value); + Assert.Equal(expectedJson, actualJson); + } + + [Fact] + public async Task ConcreteClass_NestedValue_PolymorphismEnabled_ShouldSerializePolymorphically() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = type => type == typeof(ConcreteClass) }; + List value = new() { new DerivedClass { Boolean = true, Number = 42 } }; + string expectedJson = @"[{""Number"":42,""Boolean"":true}]"; + + string actualJson = await Serializer.SerializeWrapper(value, options); + Assert.Equal(expectedJson, actualJson); + } + + [Fact] + public void ConcreteClass_NestedValue_PolymorphismEnabled_ShouldDeserializeAsBase() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = type => type == typeof(ConcreteClass) }; + string json = @"[{""Number"":42,""Boolean"":true}]"; + + List result = JsonSerializer.Deserialize>(json, options); + + Assert.Equal(1, result.Count); + Assert.IsType(result[0]); + Assert.True(result[0].Boolean); + } + + [Fact] + public async Task AbstractClass_RootValue_PolymorphismDisabled_ShouldSerializeAsBase() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = _ => false }; + AbstractClass value = new ConcreteClass { Boolean = true }; + string expectedJson = @"{}"; + + string actualJson = await Serializer.SerializeWrapper(value, options); + + Assert.Equal(expectedJson, actualJson); + } + + [Fact] + public async Task AbstractClass_RootValue_PolymorphismEnabled_ShouldSerializePolymorphically() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = type => type == typeof(AbstractClass) }; + AbstractClass value = new ConcreteClass { Boolean = true }; + string expectedJson = @"{""Boolean"":true}"; + + string actualJson = await Serializer.SerializeWrapper(value, options); + + Assert.Equal(expectedJson, actualJson); + } + + [Fact] + public void AbstractClass_RootValue_PolymorphismEnabled_ShouldFailDeserialization() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = type => type == typeof(AbstractClass) }; + string json = @"{""Boolean"":true}"; + + Assert.Throws(() => JsonSerializer.Deserialize(json, options)); + } + + [Fact] + public async Task AbstractClass_NestedValue_PolymorphismDisabled_ShouldSerializeAsBase() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = _ => false }; + List value = new() { new ConcreteClass { Boolean = true } }; + string expectedJson = @"[{}]"; + + string actualJson = await Serializer.SerializeWrapper(value); + Assert.Equal(expectedJson, actualJson); + } + + [Fact] + public async Task AbstractClass_NestedValue_PolymorphismEnabled_ShouldSerializePolymorphically() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = type => type == typeof(AbstractClass) }; + List value = new () { new ConcreteClass { Boolean = true } }; + string expectedJson = @"[{""Boolean"":true}]"; + + string actualJson = await Serializer.SerializeWrapper(value, options); + Assert.Equal(expectedJson, actualJson); + } + + [Fact] + public void AbstractClass_NestedValue_PolymorphismEnabled_ShouldFailDeserialization() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = type => type == typeof(AbstractClass) }; + string json = @"[{""Boolean"":true}]"; + + Assert.Throws(() => JsonSerializer.Deserialize>(json, options)); + } + + [Fact] + public async Task Interface_RootValue_PolymorphismDisabled_ShouldSerializeAsBase() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = _ => false }; + IThing value = new MyOtherThing { Number = 42, OtherNumber = 24 }; + string expectedJson = @"{""Number"":42}"; + + string actualJson = await Serializer.SerializeWrapper(value, options); + Assert.Equal(expectedJson, actualJson); + } + + [Fact] + public async Task Interface_RootValue_PolymorphismEnabled_ShouldSerializePolymorphically() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = type => type == typeof(IThing) }; + IThing value = new MyOtherThing { Number = 42, OtherNumber = 24 }; + string expectedJson = @"{""Number"":42,""OtherNumber"":24}"; + + string actualJson = await Serializer.SerializeWrapper(value, options); + Assert.Equal(expectedJson, actualJson); + } + + [Theory] + [MemberData(nameof(GetObjectsContainingNestedIThingValues))] + public async Task Interface_NestedValues_PolymorphismDisabled_ShouldSerializeAsBase(TValue value, string expectedJson) + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = _ => false }; + string actualJson = await Serializer.SerializeWrapper(value, options); + Assert.NotEqual(expectedJson, actualJson); + } + + [Theory] + [MemberData(nameof(GetObjectsContainingNestedIThingValues))] + public async Task Interface_NestedValues_PolymorphismEnabled_ShouldSerializePolymorphically(TValue value, string expectedJson) + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = type => type == typeof(IThing) }; + string actualJson = await Serializer.SerializeWrapper(value, options); + Assert.Equal(expectedJson, actualJson); + } + + [Fact] + public void Interface_RootValue_PolymorphismEnabled_ShouldFailDeserialization() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = type => type == typeof(IThing) }; + string json = @"{""Number"":42,""OtherNumber"":24}"; + Assert.Throws(() => JsonSerializer.Deserialize(json, options)); + } + + [Theory] + [MemberData(nameof(GetObjectsContainingNestedIThingValues))] + public void Interface_NestedValues_PolymorphismEnabled_ShouldFailDeserialization(TValue value, string json) + { + _ = value; + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = type => type == typeof(IThing) }; + Assert.Throws(() => JsonSerializer.Deserialize(json, options)); + } + + [Fact] + public async Task PolymorphismEnabledForAllTypes_ShouldSerializeAllValuesPolymorphically() + { + var options = new JsonSerializerOptions { SupportedPolymorphicTypes = _ => true }; + object[] value = new object[] + { + 42, + "string", + CreateList(new MyOtherThing { Number = 42, OtherNumber = -1 }), + CreateList(new DerivedClass { Number = 42, Boolean = true }, new ConcreteClass { Boolean = false }), + CreateList(new DerivedClass { Number = 42, Boolean = true }), + }; + + string expectedJson = @"[42,""string"",[{""Number"":42,""OtherNumber"":-1}],[{""Number"":42,""Boolean"":true},{""Boolean"":false}],[{""Number"":42,""Boolean"":true}]]"; + string actualJson = await Serializer.SerializeWrapper(value, options); + + Assert.Equal(expectedJson, actualJson); + + static List CreateList(params T[] values) => new(values); + } + + [Fact] + public async Task JsonPolymorphicTypeAttribute_Class() + { + PolymorphicAnnotatedClass value = new DerivedFromPolymorphicAnnotatedClass { A = 1, B = 2, C = 3 }; + string expectedJson = @"{""A"":1,""B"":2,""C"":3}"; + + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual(expectedJson, json); + } + + [Fact] + public async Task JsonPolymorphicTypeAttribute_Interface() + { + IPolymorphicAnnotatedInterface value = new DerivedFromPolymorphicAnnotatedClass { A = 1, B = 2, C = 3 }; + string expectedJson = @"{""A"":1,""B"":2,""C"":3}"; + + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual(expectedJson, json); + } + + [Fact] + public async Task JsonPolymorphicTypeAttribute_IsNotInheritedByDerivedTypes() + { + DerivedFromPolymorphicAnnotatedClass value = new DerivedFromDerivedFromPolymorphicAnnotatedClass { A = 1, B = 2, C = 3, D = 4 }; + string expectedJson = @"{""A"":1,""B"":2,""C"":3}"; + + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual(expectedJson, json); + } + + public static IEnumerable GetObjectsContainingNestedIThingValues() + { + yield return WrapArgs( + new() { new MyThing { Number = -1 }, new MyOtherThing { Number = 42, OtherNumber = 24 } }, + @"[{""Number"":-1},{""Number"":42,""OtherNumber"":24}]" + ); + + yield return WrapArgs( + new() { Value = "value", Thing = new MyOtherThing { Number = 42, OtherNumber = 24 } }, + @"{""Value"":""value"",""Thing"":{""Number"":42,""OtherNumber"":24}}" + ); + + yield return WrapArgs( + new() + { + ["key1"] = new MyOtherThing { Number = 42, OtherNumber = 24 }, + ["key2"] = new MyThing { Number = -1 } + }, + @"{""key1"":{""Number"":42,""OtherNumber"":24},""key2"":{""Number"":-1}}" + ); + + static object[] WrapArgs(TValue value, string expectedJson) => new object[] { value, expectedJson }; + } + class MyClass { public string Value { get; set; } @@ -528,8 +795,51 @@ class MyThing : IThing public int Number { get; set; } } + class MyOtherThing : IThing + { + public int Number { get; set; } + public int OtherNumber { get; set; } + } + + abstract class AbstractClass + { + } + + class ConcreteClass : AbstractClass + { + public bool Boolean { get; set; } + } + + class DerivedClass : ConcreteClass + { + public int Number { get; set; } + } + class MyThingCollection : List { } class MyThingDictionary : Dictionary { } + + [JsonPolymorphicType] + public class IPolymorphicAnnotatedInterface + { + int A { get; set; } + } + + [JsonPolymorphicType] + public class PolymorphicAnnotatedClass : IPolymorphicAnnotatedInterface + { + public int A { get; set; } + public int B { get; set; } + } + + public class DerivedFromPolymorphicAnnotatedClass : PolymorphicAnnotatedClass + { + public int C { get; set; } + } + + public class DerivedFromDerivedFromPolymorphicAnnotatedClass : DerivedFromPolymorphicAnnotatedClass + { + public int D { get; set; } + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTypeDiscriminatorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTypeDiscriminatorTests.cs new file mode 100644 index 00000000000000..a73c86b19ea202 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTypeDiscriminatorTests.cs @@ -0,0 +1,1065 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public class PolymorphicTypeDiscriminatorTests_Sync : PolymorphicTypeDiscriminatorTests + { + protected override Task Serialize(T value, JsonSerializerOptions? options = null) + { + var result = JsonSerializer.Serialize(value, options); + return Task.FromResult(result); + } + + protected override Task Deserialize(string json, JsonSerializerOptions? options = null) + { + var result = JsonSerializer.Deserialize(json, options); + return Task.FromResult(result); + } + } + + public class PolymorphicTypeDiscriminatorTests_Async : PolymorphicTypeDiscriminatorTests + { + private static JsonSerializerOptions s_defaultOptions = new JsonSerializerOptions() { DefaultBufferSize = 1 }; + private static JsonSerializerOptions PrepareOptions(JsonSerializerOptions? options) + { + return options is null ? s_defaultOptions : new JsonSerializerOptions(options) { DefaultBufferSize = 1 }; + } + + protected override async Task Serialize(T value, JsonSerializerOptions? options = null) + { + using var stream = new MemoryStream(); + await JsonSerializer.SerializeAsync(stream, value, PrepareOptions(options)); + return Encoding.UTF8.GetString(stream.ToArray()); + } + + protected override async Task Deserialize(string json, JsonSerializerOptions? options = null) + { + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + return await JsonSerializer.DeserializeAsync(stream, PrepareOptions(options)); + } + } + + public abstract class PolymorphicTypeDiscriminatorTests + { + protected abstract Task Serialize(T value, JsonSerializerOptions? options = null); + protected abstract Task Deserialize(string json, JsonSerializerOptions? options = null); + + private readonly static JsonSerializerOptions s_jsonSerializerOptionsPreserveRefs = new JsonSerializerOptions + { + ReferenceHandler = ReferenceHandler.Preserve + }; + + + [Theory] + [MemberData(nameof(GetHappyPathValuesAndExpectedJson))] + public async Task HappyPath_Serialization_AsRootValue(HappyPath_BaseClass value, string expectedJson) + { + string actualJson = await Serialize(value); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Theory] + [MemberData(nameof(GetHappyPathValuesAndExpectedJson))] + public async Task HappyPath_Serialization_AsPoco(HappyPath_BaseClass value, string expectedJson) + { + string actualJson = await Serialize(new { Value = value }); + JsonTestHelper.AssertJsonEqual($@"{{""Value"":{expectedJson}}}", actualJson); + } + + [Theory] + [MemberData(nameof(GetHappyPathValuesAndExpectedJson))] + public async Task HappyPath_Serialization_AsListElement(HappyPath_BaseClass value, string expectedJson) + { + string actualJson = await Serialize(new List { value }); + JsonTestHelper.AssertJsonEqual($"[{expectedJson}]", actualJson); + } + + [Theory] + [MemberData(nameof(GetHappyPathValuesAndExpectedJson))] + public async Task HappyPath_Serialization_AsDictionaryValue(HappyPath_BaseClass value, string expectedJson) + { + string actualJson = await Serialize(new Dictionary { ["key"] = value }); + JsonTestHelper.AssertJsonEqual(@$"{{""key"":{expectedJson}}}", actualJson); + } + + [Fact] + public async Task HappyPath_Serialization_MultiValueArray() + { + // construct input array and expected json from list of inputs + var values = GetHappyPathValuesAndExpectedJson() + .Select(array => (value: (HappyPath_BaseClass)array[0], json: (string)array[1])) + .ToArray(); + + List inputList = values.Select(x => x.value).ToList(); + string expectedJson = $"[{string.Join(",", values.Select(x => x.json))}]"; + + string actualJson = await Serialize(inputList); + + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + public static IEnumerable GetHappyPathValuesAndExpectedJson() + { + yield return WrapArgs(new HappyPath_BaseClass { Number = 42 }, @"{""Number"":42}"); + + yield return WrapArgs( + new HappyPath_DerivedClass1 { Number = 42, Boolean1 = true }, + @"{""$type"":""derived1"",""Number"":42,""Boolean1"":true}"); + + yield return WrapArgs( + new HappyPath_DerivedClass2 { Number = 42, Boolean2 = true }, + @"{""Number"":42}"); + + yield return WrapArgs( + new HappyPath_DerivedEnumerable(), + @"{""$type"":""enumerable"",""$values"":[0,1,2,3,4]}"); + + yield return WrapArgs( + new HappyPath_DerivedWithCustomConverter(), + "{}"); + + yield return WrapArgs( + new HappyPath_DerivedDerivedClass1 { Number = 42, Boolean1 = true, String1 = "str" }, + @"{""$type"":""derived1"",""Number"":42,""Boolean1"":true}"); + + yield return WrapArgs( + new HappyPath_DerivedDerivedClass2 { Number = 42, Boolean2 = true, String2 = "str" }, + @"{""$type"":""derived2"",""Number"":42,""Boolean2"":true,""String2"":""str""}"); + + static object[] WrapArgs(HappyPath_BaseClass value, string expectedJson) + => new object[] { value, expectedJson }; + } + + [Theory] + [MemberData(nameof(GetHappyPathJsonAndExpectedDeserializationType))] + public async Task HappyPath_Deserialization_AsRootValue(string json, HappyPath_BaseClass? expectedDeserializedValue) + { + if (expectedDeserializedValue is null) + { + await Assert.ThrowsAsync(() => Deserialize(json)); + return; + } + + var result = await Deserialize(json); + + HappyPath_BaseClass.AssertEqual(expectedDeserializedValue, result); + } + + [Theory] + [MemberData(nameof(GetHappyPathJsonAndExpectedDeserializationType))] + public async Task HappyPath_Deserialization_AsPoco(string json, HappyPath_BaseClass? expectedDeserializedValue) + { + string input = @$"{{""Value"":{json}}}"; + + if (expectedDeserializedValue is null) + { + await Assert.ThrowsAsync(() => Deserialize>(input)); + return; + } + + var result = await Deserialize>(input); + + HappyPath_BaseClass.AssertEqual(expectedDeserializedValue, result.Value); + } + + [Theory] + [MemberData(nameof(GetHappyPathJsonAndExpectedDeserializationType))] + public async Task HappyPath_Deserialization_AsListElement(string json, HappyPath_BaseClass? expectedDeserializedValue) + { + string input = $"[{json}]"; + + if (expectedDeserializedValue is null) + { + await Assert.ThrowsAsync(() => Deserialize>(input)); + return; + } + + var result = await Deserialize>(input); + + Assert.Equal(1, result.Count); + HappyPath_BaseClass.AssertEqual(expectedDeserializedValue, result[0]); + } + + [Theory] + [MemberData(nameof(GetHappyPathJsonAndExpectedDeserializationType))] + public async Task HappyPath_Deserialization_AsDictionaryValue(string json, HappyPath_BaseClass? expectedDeserializedValue) + { + string input = @$"{{""key"":{json}}}"; + + if (expectedDeserializedValue is null) + { + await Assert.ThrowsAsync(() => Deserialize>(input)); + return; + } + + var result = await Deserialize>(input); + + Assert.Equal(1, result.Count); + HappyPath_BaseClass.AssertEqual(expectedDeserializedValue, result["key"]); + } + + [Fact] + public async Task HappyPath_Deserialization_MultiValueArray() + { + // construct input array and expected json from list of inputs + var values = GetHappyPathJsonAndExpectedDeserializationType() + .Select(array => (json: (string)array[0], expectedValue: (HappyPath_BaseClass)array[1])) + .Where(x => x.expectedValue is not null) // null denotes non-deserializable values + .ToArray(); + + List expectedValues = values.Select(x => x.expectedValue).ToList(); + string json = $"[{string.Join(",", values.Select(x => x.json))}]"; + + List actualValues = await Deserialize>(json); + + Assert.Equal(expectedValues.Count, actualValues.Count); + for (int i = 0; i < expectedValues.Count; i++) + { + HappyPath_BaseClass.AssertEqual(expectedValues[i], actualValues[i]); + } + } + + public static IEnumerable GetHappyPathJsonAndExpectedDeserializationType() + { + yield return WrapArgs(@"{""Number"":42}", new HappyPath_BaseClass { Number = 42 }); + + yield return WrapArgs( + @"{""$type"":""derived1"",""Number"":42,""Boolean1"":true, ""SomeOtherPropertyThatWeIgnore"" : ""longvalue aasdfasdfasdasd asdasd asd as""}", + new HappyPath_DerivedClass1 { Number = 42, Boolean1 = true } + ); + + yield return WrapArgs( + @"{""$type"":""enumerable"",""$values"":[0,1,2,3,4]}", + null // deserialization throws NotSupportedException due to derived type not being deserializable + ); + + yield return WrapArgs( + @"{""$type"":""derived2"",""Number"":42,""Boolean2"":true,""String2"":""str""}", + new HappyPath_DerivedDerivedClass2 { Number = 42, Boolean2 = true, String2 = "str" } + ); + + yield return WrapArgs( + @"{""$type"":""custom"",""Number"":42}", + new HappyPath_DerivedWithCustomConverter() + ); + + static object[] WrapArgs(string json, HappyPath_BaseClass expectedDeserializedValue) + => new object[] { json, expectedDeserializedValue }; + } + + [Theory] + [MemberData(nameof(GetHappyPathValuesAndExpectedJson_CustomConfiguration))] + public async Task HappyPath_Interface_SerializationWithCustomConfig_AsRootValue(HappyPath_BaseClass value, string expectedJson) + { + string actualJson = await Serialize(value, s_optionsWithCustomKnownTypeConfiguration); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Theory] + [MemberData(nameof(GetHappyPathValuesAndExpectedJson_CustomConfiguration))] + public async Task HappyPath_Interface_SerializationWithCustomConfig_AsPoco(HappyPath_BaseClass value, string expectedJson) + { + string actualJson = await Serialize(new { Value = value }, s_optionsWithCustomKnownTypeConfiguration); + JsonTestHelper.AssertJsonEqual($@"{{""Value"":{expectedJson}}}", actualJson); + } + + [Theory] + [MemberData(nameof(GetHappyPathValuesAndExpectedJson_CustomConfiguration))] + public async Task HappyPath_Interface_SerializationWithCustomConfig_AsListElement(HappyPath_BaseClass value, string expectedJson) + { + string actualJson = await Serialize(new List { value }, s_optionsWithCustomKnownTypeConfiguration); + JsonTestHelper.AssertJsonEqual($"[{expectedJson}]", actualJson); + } + + [Theory] + [MemberData(nameof(GetHappyPathValuesAndExpectedJson_CustomConfiguration))] + public async Task HappyPath_Interface_SerializationWithCustomConfig_AsDictionaryValue(HappyPath_BaseClass value, string expectedJson) + { + string actualJson = await Serialize(new Dictionary { ["key"] = value }, s_optionsWithCustomKnownTypeConfiguration); + JsonTestHelper.AssertJsonEqual(@$"{{""key"":{expectedJson}}}", actualJson); + } + + public static IEnumerable GetHappyPathValuesAndExpectedJson_CustomConfiguration() + { + yield return WrapArgs(new HappyPath_BaseClass { Number = 42 }, @"{""Number"":42}"); + + yield return WrapArgs( + new HappyPath_DerivedClass1 { Number = 42, Boolean1 = true }, + @"{""Number"":42}"); + + yield return WrapArgs( + new HappyPath_DerivedClass2 { Number = 42, Boolean2 = true }, + @"{""$type"":""derived_2"",""Number"":42,""Boolean2"":true}"); + + yield return WrapArgs( + new HappyPath_DerivedEnumerable(), + @"{""$type"":""enum"",""$values"":[0,1,2,3,4]}"); + + yield return WrapArgs( + new HappyPath_DerivedWithCustomConverter(), + @"{""Number"":0}"); + + yield return WrapArgs( + new HappyPath_DerivedDerivedClass1 { Number = 42, Boolean1 = true, String1 = "str" }, + @"{""$type"":""derived_1"",""Number"":42,""Boolean1"":true,""String1"":""str""}"); + + yield return WrapArgs( + new HappyPath_DerivedDerivedClass2 { Number = 42, Boolean2 = true, String2 = "str" }, + @"{""$type"":""derived_2"",""Number"":42,""Boolean2"":true}"); + + static object[] WrapArgs(HappyPath_BaseClass value, string expectedJson) => new object[] { value, expectedJson }; + } + + [Theory] + [MemberData(nameof(GetHappyPathJsonAndExpectedDeserializationType_CustomConfiguration))] + public async Task HappyPath_Interface_DeserializationWithCustomConfig_AsRootValue(string json, HappyPath_BaseClass? expectedDeserializedValue) + { + if (expectedDeserializedValue is null) + { + await Assert.ThrowsAsync(() => Deserialize(json, s_optionsWithCustomKnownTypeConfiguration)); + return; + } + + var result = await Deserialize(json, s_optionsWithCustomKnownTypeConfiguration); + + HappyPath_BaseClass.AssertEqual(expectedDeserializedValue, result); + } + + [Theory] + [MemberData(nameof(GetHappyPathJsonAndExpectedDeserializationType_CustomConfiguration))] + public async Task HappyPath_Interface_DeserializationWithCustomConfig_AsPoco(string json, HappyPath_BaseClass? expectedDeserializedValue) + { + string input = @$"{{""Value"":{json}}}"; + + if (expectedDeserializedValue is null) + { + await Assert.ThrowsAsync(() => Deserialize>(input, s_optionsWithCustomKnownTypeConfiguration)); + return; + } + + var result = await Deserialize>(input, s_optionsWithCustomKnownTypeConfiguration); + + HappyPath_BaseClass.AssertEqual(expectedDeserializedValue, result.Value); + } + + [Theory] + [MemberData(nameof(GetHappyPathJsonAndExpectedDeserializationType_CustomConfiguration))] + public async Task HappyPath_Interface_DeserializationWithCustomConfig_AsListElement(string json, HappyPath_BaseClass? expectedDeserializedValue) + { + string input = $"[{json}]"; + + if (expectedDeserializedValue is null) + { + await Assert.ThrowsAsync(() => Deserialize>(input, s_optionsWithCustomKnownTypeConfiguration)); + return; + } + + var result = await Deserialize>(input, s_optionsWithCustomKnownTypeConfiguration); + + Assert.Equal(1, result.Count); + HappyPath_BaseClass.AssertEqual(expectedDeserializedValue, result[0]); + } + + [Theory] + [MemberData(nameof(GetHappyPathJsonAndExpectedDeserializationType_CustomConfiguration))] + public async Task HappyPath_Interface_DeserializationWithCustomConfig_AsDictionaryValue(string json, HappyPath_BaseClass? expectedDeserializedValue) + { + string input = @$"{{""key"":{json}}}"; + + if (expectedDeserializedValue is null) + { + await Assert.ThrowsAsync(() => Deserialize>(input, s_optionsWithCustomKnownTypeConfiguration)); + return; + } + + var result = await Deserialize>(input, s_optionsWithCustomKnownTypeConfiguration); + + Assert.Equal(1, result.Count); + HappyPath_BaseClass.AssertEqual(expectedDeserializedValue, result["key"]); + } + + public static IEnumerable GetHappyPathJsonAndExpectedDeserializationType_CustomConfiguration() + { + yield return WrapArgs(@"{""Number"":42}", new HappyPath_BaseClass { Number = 42 }); + + yield return WrapArgs( + @"{""$type"":""derived_2"",""Number"":42,""Boolean2"":true}", + new HappyPath_DerivedClass2 { Number = 42, Boolean2 = true } + ); + + yield return WrapArgs( + @"{""$type"":""derived_1"",""Number"":42,""Boolean1"":true,""String1"":""str""}", + new HappyPath_DerivedDerivedClass1 { Number = 42, Boolean1 = true, String1 = "str" } + ); + + yield return WrapArgs( + @"{""$type"":""enum"",""$values"":[0,1,2,3,4]}", + null // deserialization throws NotSupportedException due to derived type not being deserializable + ); + + static object[] WrapArgs(string json, HappyPath_BaseClass expectedDeserializedValue) => new object[] { json, expectedDeserializedValue }; + } + + private static JsonSerializerOptions s_optionsWithCustomKnownTypeConfiguration = new JsonSerializerOptions + { + TypeDiscriminatorConfigurations = + { + new TypeDiscriminatorConfiguration() + .WithKnownType("derived_2") + .WithKnownType("derived_1") + .WithKnownType("enum") + } + }; + + [Theory] + [MemberData(nameof(GetHappyPathInterfaceValuesAndExpectedJson))] + public async Task HappyPath_Interface_Serialization_AsRootValue(HappyPath_Interface_BaseInterface value, string expectedJson) + { + string actualJson = await Serialize(value); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Theory] + [MemberData(nameof(GetHappyPathInterfaceValuesAndExpectedJson))] + public async Task HappyPath_Interface_Serialization_AsPoco(HappyPath_Interface_BaseInterface value, string expectedJson) + { + string actualJson = await Serialize(new { Value = value }); + JsonTestHelper.AssertJsonEqual($@"{{""Value"":{expectedJson}}}", actualJson); + } + + [Theory] + [MemberData(nameof(GetHappyPathInterfaceValuesAndExpectedJson))] + public async Task HappyPath_Interface_Serialization_AsListElement(HappyPath_Interface_BaseInterface value, string expectedJson) + { + string actualJson = await Serialize(new List { value }); + JsonTestHelper.AssertJsonEqual($"[{expectedJson}]", actualJson); + } + + [Theory] + [MemberData(nameof(GetHappyPathInterfaceValuesAndExpectedJson))] + public async Task HappyPath_Interface_Serialization_AsDictionaryValue(HappyPath_Interface_BaseInterface value, string expectedJson) + { + string actualJson = await Serialize(new Dictionary { ["key"] = value }); + JsonTestHelper.AssertJsonEqual(@$"{{""key"":{expectedJson}}}", actualJson); + } + + public static IEnumerable GetHappyPathInterfaceValuesAndExpectedJson() + { + yield return WrapArgs(new HappyPath_Interface_DerivedClass1 { Number = 42 }, @"{""$type"":""derived1"",""Number"":42}"); + yield return WrapArgs(new HappyPath_Interface_DerivedClass2 { String = "str" }, "{}"); + yield return WrapArgs( + new HappyPath_Interface_DerivedClassFromDerivedInterface { Boolean = true, Number = 42 }, + @"{""$type"":""derived_interface"",""Boolean"":true}"); + yield return WrapArgs( + new HappyPath_Interface_DerivedStruct { Number = 42 }, + @"{""$type"":""derived_struct"",""Number"":42}"); + + static object[] WrapArgs(HappyPath_Interface_BaseInterface value, string expectedJson) => new object[] { value, expectedJson }; + } + + [Theory] + [MemberData(nameof(GetHappyPathInterfaceJsonAndExpectedDeserializationType))] + public async Task HappyPath_Interface_Deserialization_AsRootValue(string json, HappyPath_Interface_BaseInterface? expectedDeserializedValue) + { + if (expectedDeserializedValue is null) + { + await Assert.ThrowsAsync(() => Deserialize(json)); + return; + } + + var result = await Deserialize(json); + HappyPath_Interface_BaseInterfaceHelpers.AssertEqual(expectedDeserializedValue, result); + } + + [Theory] + [MemberData(nameof(GetHappyPathInterfaceJsonAndExpectedDeserializationType))] + public async Task HappyPath_Interface_Deserialization_AsPoco(string json, HappyPath_Interface_BaseInterface? expectedDeserializedValue) + { + string input = @$"{{""Value"":{json}}}"; + + if (expectedDeserializedValue is null) + { + await Assert.ThrowsAsync(() => Deserialize>(input)); + return; + } + + var result = await Deserialize>(input); + HappyPath_Interface_BaseInterfaceHelpers.AssertEqual(expectedDeserializedValue, result.Value); + } + + [Theory] + [MemberData(nameof(GetHappyPathInterfaceJsonAndExpectedDeserializationType))] + public async Task HappyPath_Interface_Deserialization_AsListElement(string json, HappyPath_Interface_BaseInterface? expectedDeserializedValue) + { + string input = $"[{json}]"; + + if (expectedDeserializedValue is null) + { + await Assert.ThrowsAsync(() => Deserialize>(input)); + return; + } + + var result = await Deserialize>(input); + + Assert.Equal(1, result.Count); + HappyPath_Interface_BaseInterfaceHelpers.AssertEqual(expectedDeserializedValue, result[0]); + } + + [Theory] + [MemberData(nameof(GetHappyPathInterfaceJsonAndExpectedDeserializationType))] + public async Task HappyPath_Interface_Deserialization_AsDictionaryValue(string json, HappyPath_Interface_BaseInterface? expectedDeserializedValue) + { + string input = @$"{{""key"":{json}}}"; + + if (expectedDeserializedValue is null) + { + await Assert.ThrowsAsync(() => Deserialize>(input)); + return; + } + + var result = await Deserialize>(input); + + Assert.Equal(1, result.Count); + HappyPath_Interface_BaseInterfaceHelpers.AssertEqual(expectedDeserializedValue, result["key"]); + } + + public static IEnumerable GetHappyPathInterfaceJsonAndExpectedDeserializationType() + { + yield return WrapArgs(@"{""$type"":""derived1"",""Number"":42}", new HappyPath_Interface_DerivedClass1 { Number = 42 }); + yield return WrapArgs( + @"{""$type"":""derived_interface"",""Boolean"":true}", + null // deserialization throws NotSupportedException due to derived type not being deserializable + ); + yield return WrapArgs( + @"{""$type"":""derived_struct"",""Number"":42}", + new HappyPath_Interface_DerivedStruct { Number = 42 } + ); + + static object[] WrapArgs(string json, HappyPath_Interface_BaseInterface expectedDeserializedValue) + => new object[] { json, expectedDeserializedValue }; + } + + [Theory] + [InlineData(0, @"{""$type"":""zero""}")] + [InlineData(1, @"{""$type"":""succ"", ""value"":{""$type"":""zero""}}")] + [InlineData(3, @"{""$type"":""succ"", ""value"":{""$type"":""succ"",""value"":{""$type"":""succ"",""value"":{""$type"":""zero""}}}}")] + public async Task SimpleRecursiveTypeSerialization(int size, string expectedJson) + { + Peano peano = Peano.FromInteger(size); + string actualJson = await Serialize(peano); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Theory] + [InlineData(0, @"{""$type"":""zero""}")] + [InlineData(1, @"{""$type"":""succ"", ""value"":{""$type"":""zero""}}")] + [InlineData(3, @"{""$type"":""succ"", ""value"":{""$type"":""succ"",""value"":{""$type"":""succ"",""value"":{""$type"":""zero""}}}}")] + public async Task SimpleRecursiveTypeDeserialization(int expectedSize, string json) + { + Peano expected = Peano.FromInteger(expectedSize); + Peano actual = await Deserialize(json); + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(GetGenericRecursiveTypeValues))] + public async Task GenericRecursiveTypeSerialization(BinTree tree, string expectedJson) + { + string actualJson = await Serialize(tree, BinTree.Options); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Theory] + [MemberData(nameof(GetGenericRecursiveTypeValues))] + public async Task GenericRecursiveTypeDeserialization(BinTree expected, string json) + { + BinTree actual = await Deserialize>(json, BinTree.Options); + Assert.Equal(expected, actual); + } + + [Fact] + public async Task ReferencePreservationSerialization_SingleValue() + { + HappyPath_BaseClass value = new HappyPath_DerivedClass1 { Number = 42, Boolean1 = true }; + string expectedJson = @"{""$id"":""1"",""$type"":""derived1"",""Number"":42,""Boolean1"":true}"; + + string actualJson = await Serialize(value, s_jsonSerializerOptionsPreserveRefs); + + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Fact] + public async Task ReferencePreservationDeserialization_SingleValue() + { + string json = @"{""$id"":""1"",""$type"":""derived1"",""Number"":42,""Boolean1"":true}"; + var expectedValue = new HappyPath_DerivedClass1 { Number = 42, Boolean1 = true }; + + var actualValue = await Deserialize(json, s_jsonSerializerOptionsPreserveRefs); + + HappyPath_BaseClass.AssertEqual(expectedValue, actualValue); + } + + [Fact] + public async Task ReferencePreservationSerialization_RepeatingValue() + { + HappyPath_BaseClass value = new HappyPath_DerivedClass1 { Number = 42, Boolean1 = true }; + List input = new() { value, value }; + string expectedJson = + @"{""$id"":""1"", ""$values"":[ + {""$id"":""2"",""$type"":""derived1"",""Number"":42,""Boolean1"":true}, + {""$ref"":""2""}] + }"; + + string actualJson = await Serialize(input, s_jsonSerializerOptionsPreserveRefs); + + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Fact] + public async Task ReferencePreservationDeserialization_RepeatingValue() + { + string json = + @"{""$id"":""1"", ""$values"":[ + {""$id"":""2"",""$type"":""derived1"",""Number"":42,""Boolean1"":true}, + {""$ref"":""2""}] + }"; + + HappyPath_BaseClass expectedValue = new HappyPath_DerivedClass1 { Number = 42, Boolean1 = true }; + + var result = await Deserialize>(json, s_jsonSerializerOptionsPreserveRefs); + + Assert.Equal(2, result.Count); + HappyPath_BaseClass.AssertEqual(expectedValue, result[0]); + Assert.Same(result[0], result[1]); + } + + public static IEnumerable GetGenericRecursiveTypeValues() + { + yield return WrapArgs(new BinTree.Leaf(), @"{""$type"":""leaf""}"); + yield return WrapArgs( + new BinTree.Node(false, + new BinTree.Leaf(), + new BinTree.Leaf()), + @"{""$type"":""node"",""value"":false,""left"":{""$type"":""leaf""},""right"":{""$type"":""leaf""}}"); + + yield return WrapArgs( + new BinTree.Node("A", + new BinTree.Leaf(), + new BinTree.Node("B", + new BinTree.Leaf(), + new BinTree.Leaf())), + @"{""$type"":""node"", ""value"":""A"", + ""left"":{""$type"":""leaf""}, + ""right"":{""$type"":""node"", ""value"":""B"", + ""left"":{""$type"":""leaf""}, + ""right"":{""$type"":""leaf""}}}"); + + static object[] WrapArgs(BinTree value, string expectedJson) => new object[] { value, expectedJson }; + } + + [Theory] + [InlineData("null")] + [InlineData("0")] + [InlineData("{}")] + [InlineData("[]")] + [InlineData("\"invalid tag\"")] + [InlineData("\"System.Diagnostics.Process\"")] + public async Task InvalidTypeDiscriminator_ShouldBeIgnored(string invalidTypeId) + { + string json = @$"{{""$type"":{invalidTypeId},""Number"":42}}"; + var expectedValue = new HappyPath_BaseClass { Number = 42 }; + + HappyPath_BaseClass actualValue = await Deserialize(json); + + HappyPath_BaseClass.AssertEqual(expectedValue, actualValue); + } + + [Fact] + public async Task ApplyTaggedPolymorphismToEnumerableTypes_Serialization() + { + var source = new int[] { 1, 2, 3 }; + var values = new IEnumerable[] { source, new List(source), new Queue(source), ImmutableArray.Create(source) }; + + string expectedJson = + @"[ { ""$type"":""array"", ""$values"":[1,2,3] }, + { ""$type"":""list"", ""$values"":[1,2,3] }, + { ""$type"":""set"", ""$values"":[1,2,3] }, + [1,2,3] ]"; + + string actualJson = await Serialize(values, s_optionsWithCollectionKnownTypes); + + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Fact] + public async Task ApplyTaggedPolymorphismToEnumerableTypes_Deserialization() + { + var source = new int[] { 1, 2, 3 }; + var expectedValues = new IEnumerable[] { source, new List(source), new Queue(source), new List(source) }; + + string json = + @"[ { ""$type"":""array"", ""$values"":[1,2,3] }, + { ""$type"":""list"", ""$values"":[1,2,3] }, + { ""$type"":""set"", ""$values"":[1,2,3] }, + [1,2,3] ]"; + + var actualValues = await Deserialize[]>(json, s_optionsWithCollectionKnownTypes); + + Assert.Equal(expectedValues.Length, actualValues.Length); + for (int i = 0; i < expectedValues.Length; i++) + { + Assert.Equal(expectedValues[i], actualValues[i]); + Assert.IsType(expectedValues[i].GetType(), actualValues[i]); + } + } + + [Fact] + public async Task ApplyTaggedPolymorphismToDictionaryTypes_Serialization() + { + var values = new IEnumerable>[] + { + new KeyValuePair[] { new KeyValuePair(0, 0) }, + new Dictionary { [42] = false }, + ImmutableDictionary.Create(), + new SortedDictionary { [0] = 1, [1] = 42 } + }; + + string expectedJson = + @"[ [ { ""Key"":0, ""Value"":0 } ], + { ""$type"" : ""dictionary"", ""42"" : false }, + { ""$type"" : ""immutableDictionary"" }, + { ""$type"" : ""readOnlyDictionary"", ""0"" : 1, ""1"" : 42 } ]"; + + string actualJson = await Serialize(values, s_optionsWithCollectionKnownTypes); + + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Fact] + public async Task ApplyTaggedPolymorphismToDictionaryTypes_Deserialization() + { + string json = + @"[ [ { ""Key"":0, ""Value"":0 } ], + { ""$type"" : ""dictionary"", ""42"" : false }, + { ""$type"" : ""immutableDictionary"" }, + { ""$type"" : ""readOnlyDictionary"", ""0"" : 1, ""1"" : 42 } ]"; + + var expectedValues = new IEnumerable>[] + { + new List> { new KeyValuePair(0, 0) }, + new Dictionary { [42] = false }, + ImmutableDictionary.Create(), + new Dictionary { [0] = 1, [1] = 42 } + }; + + var actualValues = await Deserialize>[]>(json, s_optionsWithCollectionKnownTypes); + + Assert.Equal(expectedValues.Length, actualValues.Length); + for (int i = 0; i < expectedValues.Length; i++) + { + Assert.Equal(expectedValues[i].Select(x => x.Key), actualValues[i].Select(x => x.Key)); + Assert.Equal(expectedValues[i].Select(x => x.Value.ToString()), actualValues[i].Select(x => x.Value.ToString())); + Assert.IsType(expectedValues[i].GetType(), actualValues[i]); + } + } + + private readonly static JsonSerializerOptions s_optionsWithCollectionKnownTypes = new JsonSerializerOptions + { + TypeDiscriminatorConfigurations = + { + new TypeDiscriminatorConfiguration>() + .WithKnownType("array") + .WithKnownType>("list") + .WithKnownType>("set") + , + + new TypeDiscriminatorConfiguration>>() + .WithKnownType>("dictionary") + .WithKnownType>("immutableDictionary") + .WithKnownType>("readOnlyDictionary") + } + }; + + [Theory] + [InlineData(typeof(InvalidConfig_KnownTypeIsInteger))] + [InlineData(typeof(InvalidConfig_KnownTypeIsString))] + [InlineData(typeof(InvalidConfig_KnownTypeNotASubclass))] + public async Task KnownTypeWithInvalidTypeParameter_ShouldThrowArgumentException(Type type) + { + object value = Activator.CreateInstance(type); + await Assert.ThrowsAsync(() => Serialize(value)); + } + + [Fact] + public async Task KnownTypeWithDuplicateType_ShouldThrowArgumentException() + { + var value = new InvalidConfig_DuplicateTypes(); + await Assert.ThrowsAsync(() => Serialize(value)); + } + + [Fact] + public async Task KnownTypeWithDuplicateTypeIds_ShouldThrowArgumentException() + { + var value = new InvalidConfig_DuplicateTypeIds(); + await Assert.ThrowsAsync(() => Serialize(value)); + } + + [Fact] + public void PolymorphicTypeDiscriminatorConfiguration_AddingKnownTypesAfterAssignmentToOptions_ShouldThrowInvalidOperationException() + { + var config = new TypeDiscriminatorConfiguration(typeof(HappyPath_BaseClass)); + config.WithKnownType(typeof(HappyPath_DerivedClass1), "derived1"); + _ = new JsonSerializerOptions { TypeDiscriminatorConfigurations = { config } }; + Assert.Throws(() => config.WithKnownType(typeof(HappyPath_DerivedClass2), "derived2")); + } + + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(string))] + [InlineData(typeof(Guid))] + [InlineData(typeof(object))] + [InlineData(typeof(BinTree<>))] + public void KnownTypeConfiguration_InvalidBaseType_ShouldThrowArgumentException(Type baseType) + { + Assert.Throws(() => new TypeDiscriminatorConfiguration(baseType)); + } + + [Theory] + [InlineData(typeof(HappyPath_BaseClass), typeof(int))] + [InlineData(typeof(HappyPath_BaseClass), typeof(string))] + [InlineData(typeof(HappyPath_BaseClass), typeof(Guid))] + [InlineData(typeof(HappyPath_BaseClass), typeof(object))] + [InlineData(typeof(HappyPath_BaseClass), typeof(BinTree<>))] + [InlineData(typeof(HappyPath_BaseClass), typeof(HappyPath_BaseClass))] + [InlineData(typeof(HappyPath_DerivedClass1), typeof(HappyPath_BaseClass))] + public void KnownTypeConfiguration_InvalidKnownType_ShouldThrowArgumentException(Type baseType, Type knownType) + { + var config = new TypeDiscriminatorConfiguration(baseType); + Assert.Throws(() => config.WithKnownType(knownType, "knownTypeId")); + } + + [Fact] + public void KnownTypeConfiguration_DuplicateTypeId_ShouldThrowArgumentException() + { + var config = new TypeDiscriminatorConfiguration(typeof(HappyPath_BaseClass)); + config.WithKnownType(typeof(HappyPath_DerivedClass1), "id1"); + Assert.Throws(() => config.WithKnownType(typeof(HappyPath_DerivedClass2), "id1")); + } + + [Fact] + public async Task KnownTypeConfiguration_ConflictingDiscriminators_ShouldThrowNotSupportedExceptions() + { + var options = new JsonSerializerOptions + { + TypeDiscriminatorConfigurations = + { + new TypeDiscriminatorConfiguration>() + .WithKnownType>("collection") + .WithKnownType>("readonlycollection") + } + }; + + IEnumerable value = new List(); // implements both ICollection and IReadOnlyCollection + + await Assert.ThrowsAsync(() => Serialize(value, options)); + } + + //---------- + + [JsonKnownType(typeof(HappyPath_DerivedClass1), "derived1")] + [JsonKnownType(typeof(HappyPath_DerivedDerivedClass2), "derived2")] + [JsonKnownType(typeof(HappyPath_DerivedEnumerable), "enumerable")] + [JsonKnownType(typeof(HappyPath_DerivedWithCustomConverter), "custom")] + public class HappyPath_BaseClass + { + public int Number { get; set; } + + public static void AssertEqual(HappyPath_BaseClass expected, HappyPath_BaseClass actual) + { + Type expectedType = expected.GetType(); + Assert.IsType(expectedType, actual); + + // check for property equality + foreach (var propInfo in expectedType.GetProperties()) + { + Assert.Equal(propInfo.GetValue(expected), propInfo.GetValue(actual)); + } + } + } + + [JsonKnownType(typeof(HappyPath_DerivedDerivedClass1), "derived_derived1")] + public class HappyPath_DerivedClass1 : HappyPath_BaseClass + { + public bool Boolean1 { get; set; } + } + + public class HappyPath_DerivedClass2 : HappyPath_BaseClass + { + public bool Boolean2 { get; set; } + } + + public class HappyPath_DerivedEnumerable : HappyPath_BaseClass, IEnumerable + { + public IEnumerator GetEnumerator() + { + for (int i = 0; i < 5; i++) yield return i; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + [JsonConverter(typeof(Converter))] + public class HappyPath_DerivedWithCustomConverter : HappyPath_BaseClass + { + public class Converter : JsonConverter + { + public override HappyPath_DerivedWithCustomConverter? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + reader.Skip(); + return new HappyPath_DerivedWithCustomConverter(); + } + + public override void Write(Utf8JsonWriter writer, HappyPath_DerivedWithCustomConverter value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + } + } + } + + public class HappyPath_DerivedDerivedClass1 : HappyPath_DerivedClass1 + { + public string String1 { get; set; } + } + + public class HappyPath_DerivedDerivedClass2 : HappyPath_DerivedClass2 + { + public string String2 { get; set; } + } + + [JsonKnownType(typeof(HappyPath_Interface_DerivedClass1), "derived1")] + [JsonKnownType(typeof(HappyPath_Interface_DerivedInterface), "derived_interface")] + [JsonKnownType(typeof(HappyPath_Interface_DerivedStruct), "derived_struct")] + public interface HappyPath_Interface_BaseInterface { } + + public static class HappyPath_Interface_BaseInterfaceHelpers + { + public static void AssertEqual(HappyPath_Interface_BaseInterface expected, HappyPath_Interface_BaseInterface actual) + { + if (expected is null) + { + Assert.Equal(expected, actual); + return; + } + + Type expectedType = expected.GetType(); + Assert.IsType(expectedType, actual); + + // check for property equality + foreach (var propInfo in expectedType.GetProperties()) + { + Assert.Equal(propInfo.GetValue(expected), propInfo.GetValue(actual)); + } + } + } + + + public class HappyPath_Interface_DerivedClass1 : HappyPath_Interface_BaseInterface + { + public int Number { get; set; } + } + + public class HappyPath_Interface_DerivedClass2 : HappyPath_Interface_BaseInterface + { + public string String { get; set; } + } + + public interface HappyPath_Interface_DerivedInterface : HappyPath_Interface_BaseInterface + { + public bool Boolean { get; set; } + } + + public class HappyPath_Interface_DerivedClassFromDerivedInterface : HappyPath_Interface_DerivedInterface + { + public bool Boolean { get; set; } + public int Number { get; set; } + } + + public struct HappyPath_Interface_DerivedStruct : HappyPath_Interface_BaseInterface + { + public int Number { get; set; } + } + + /// A Peano arithmetic encoding. + [JsonKnownType(typeof(Zero), "zero")] + [JsonKnownType(typeof(Succ), "succ")] + public abstract record Peano + { + public static Peano FromInteger(int value) => value == 0 ? new Zero() : new Succ(FromInteger(value - 1)); + + public record Zero : Peano; + public record Succ(Peano value) : Peano; + } + + public abstract record BinTree + { + public record Leaf : BinTree; + public record Node(T value, BinTree left, BinTree right) : BinTree; + + public static JsonSerializerOptions Options { get; } = + new JsonSerializerOptions + { + TypeDiscriminatorConfigurations = + { + new TypeDiscriminatorConfiguration>() + .WithKnownType("leaf") + .WithKnownType("node") + } + }; + } + + [JsonKnownType(typeof(int), "integer")] + public class InvalidConfig_KnownTypeIsInteger + { + } + + [JsonKnownType(typeof(int), "string")] + public class InvalidConfig_KnownTypeIsString + { + } + + [JsonKnownType(typeof(HappyPath_BaseClass), "baseclass")] + public class InvalidConfig_KnownTypeNotASubclass + { + } + + [JsonKnownType(typeof(Subtype), "A")] + [JsonKnownType(typeof(Subtype), "B")] + public class InvalidConfig_DuplicateTypes + { + public class Subtype : InvalidConfig_DuplicateTypes { } + } + + [JsonKnownType(typeof(A), "duplicateId")] + [JsonKnownType(typeof(B), "duplicateId")] + public class InvalidConfig_DuplicateTypeIds + { + public class A : InvalidConfig_DuplicateTypes { } + public class B : InvalidConfig_DuplicateTypes { } + } + + public class GenericPoco + { + public T Value { get; set; } + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ReferenceHandlerTests.IgnoreCycles.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ReferenceHandlerTests.IgnoreCycles.cs index 8157f9bbdf3c99..3f7d51c5a45ba5 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ReferenceHandlerTests.IgnoreCycles.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ReferenceHandlerTests.IgnoreCycles.cs @@ -14,7 +14,7 @@ namespace System.Text.Json.Serialization.Tests public class ReferenceHandlerTests_IgnoreCycles { private static readonly JsonSerializerOptions s_optionsIgnoreCycles = - new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.IgnoreCycles }; + new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.IgnoreCycles, DefaultBufferSize = 1 }; [Fact] public async Task IgnoreCycles_OnObject() @@ -401,6 +401,30 @@ public async void IgnoreCycles_BoxedValueShouldNotBeIgnored() await Test_Serialize_And_SerializeAsync_Contains(root, expectedSubstring: @"""DayOfBirth"":15", expectedTimes: 2, s_optionsIgnoreCycles); } + [Fact] + public async Task CycleDetectionStatePersistsAcrossContinuations() + { + string expectedValueJson = @"{""LargePropertyName"":""A large-ish string to force continuations"",""Nested"":null}"; + var recVal = new RecursiveValue { LargePropertyName = "A large-ish string to force continuations" }; + recVal.Nested = recVal; + + var value = new List { recVal, recVal }; + string expectedJson = $"[{expectedValueJson},{expectedValueJson}]"; + + var options = new JsonSerializerOptions(s_optionsIgnoreCycles) + { + DefaultBufferSize = 1 + }; + + await Test_Serialize_And_SerializeAsync(value, expectedJson, options); + } + + public class RecursiveValue + { + public string LargePropertyName { get; set; } + public RecursiveValue? Nested { get; set; } + } + private async Task Test_Serialize_And_SerializeAsync(T obj, string expected, JsonSerializerOptions options) { string json; 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 ee45bdc5e97e9d..c6160435cca97e 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 @@ -158,6 +158,7 @@ +