diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index f1d11f8a343c21..226f99b2b3927c 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -354,11 +354,11 @@ The type '{0}' cannot have more than one property that has the attribute '{1}'. - - The collection type '{0}' is not supported. + + The type '{0}' is not supported. - - The collection type '{0}' on '{1}' is not supported. + + The type '{0}' on '{1}' is not supported. '{0}' is invalid after '/' at the beginning of the comment. Expected either '/' or '*'. @@ -485,4 +485,4 @@ Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references by setting ReferenceHandling to ReferenceHandling.Default. - + \ No newline at end of file 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 43c1fa300ec804..7e7ba368e04b28 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -51,11 +51,35 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -64,7 +88,6 @@ - @@ -72,6 +95,8 @@ + + @@ -80,48 +105,37 @@ - - - + + - - + - - - - + - - + - - - - - - + @@ -129,6 +143,7 @@ + @@ -139,6 +154,8 @@ + + @@ -232,4 +249,4 @@ - \ No newline at end of file + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs index e9fbe8fc2b9db7..ce687afa263edb 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs @@ -2,13 +2,24 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Runtime.CompilerServices; namespace System.Text.Json { internal static partial class JsonHelpers { + /// + /// Returns the span for the given reader. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan GetSpan(this ref Utf8JsonReader reader) + { + return reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; + } + #if !BUILDING_INBOX_LIBRARY /// /// Returns if is a valid Unicode scalar @@ -64,6 +75,17 @@ public static bool IsInRangeInclusive(JsonTokenType value, JsonTokenType lowerBo /// public static bool IsDigit(byte value) => (uint)(value - '0') <= '9' - '0'; + /// + /// Perform a Read() with a Debug.Assert verifying the reader did not return false. + /// This should be called when the Read() return value is not used, such as non-Stream cases where there is only one buffer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadWithVerify(this ref Utf8JsonReader reader) + { + bool result = reader.Read(); + Debug.Assert(result); + } + /// /// Calls Encoding.UTF8.GetString that supports netstandard. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ClassType.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ClassType.cs index e4dad76cb6acf6..69448f1ba83336 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ClassType.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ClassType.cs @@ -13,15 +13,17 @@ namespace System.Text.Json /// internal enum ClassType : byte { - // typeof(object) - Unknown = 0x1, - // POCO or rich data type - Object = 0x2, - // Value or object with a converter. - Value = 0x4, - // IEnumerable + // JsonObjectConverter<> - objects with properties. + Object = 0x1, + // JsonConverter<> - simple values. + Value = 0x2, + // JsonValueConverter<> - simple values that need to re-enter the serializer such as KeyValuePair. + NewValue = 0x4, + // JsonIEnumerbleConverter<> - all enumerable collections except dictionaries. Enumerable = 0x8, - // IDictionary + // JsonDictionaryConverter<,> - dictionary types. Dictionary = 0x10, + // Invalid (not used directly for serialization) + Invalid = 0x20 } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultArrayConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultArrayConverter.cs deleted file mode 100644 index a7e4602c1fb9af..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultArrayConverter.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections; - -namespace System.Text.Json.Serialization.Converters -{ - internal sealed class DefaultArrayConverter : JsonEnumerableConverter - { - public override IEnumerable CreateFromList(ref ReadStack state, IList sourceList, JsonSerializerOptions options) - { - Type elementType = state.Current.GetElementType(); - - Array array; - - if (sourceList.Count > 0 && sourceList[0] is Array probe) - { - array = Array.CreateInstance(probe.GetType(), sourceList.Count); - - int i = 0; - foreach (IList? child in sourceList) - { - if (child is Array childArray) - { - array.SetValue(childArray, i++); - } - } - } - else - { - array = Array.CreateInstance(elementType, sourceList.Count); - sourceList.CopyTo(array, 0); - } - - return array; - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultEnumerableConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultEnumerableConverter.cs deleted file mode 100644 index 5f4635bc905082..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultEnumerableConverter.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections; -using System.Collections.Generic; - -namespace System.Text.Json.Serialization.Converters -{ - internal class JsonEnumerableT : ICollection, IEnumerable, IList, IReadOnlyCollection, IReadOnlyList - { - List _list; - - public JsonEnumerableT(IList sourceList) - { - // TODO: Change sourceList from IList to List so we can do a direct assignment here. - _list = new List(); - - foreach (object item in sourceList) - { - _list.Add((T)item); - } - } - - public T this[int index] { get => (T)_list[index]; set => _list[index] = value; } - - public int Count => _list.Count; - - public bool IsReadOnly => false; - - public void Add(T item) - { - _list.Add(item); - } - - public void Clear() - { - _list.Clear(); - } - - public bool Contains(T item) - { - return _list.Contains(item); - } - - public void CopyTo(T[] array, int arrayIndex) - { - _list.CopyTo(array, arrayIndex); - } - - public IEnumerator GetEnumerator() - { - return _list.GetEnumerator(); - } - - public int IndexOf(T item) - { - return _list.IndexOf(item); - } - - public void Insert(int index, T item) - { - _list.Insert(index, item); - } - - public bool Remove(T item) - { - return _list.Remove(item); - } - - public void RemoveAt(int index) - { - _list.RemoveAt(index); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } - - internal sealed class DefaultEnumerableConverter : JsonEnumerableConverter - { - public override IEnumerable CreateFromList(ref ReadStack state, IList sourceList, JsonSerializerOptions options) - { - Type elementType = state.Current.GetElementType(); - - Type t = typeof(JsonEnumerableT<>).MakeGenericType(elementType); - return (IEnumerable)Activator.CreateInstance(t, sourceList); - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultImmutableDictionaryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultImmutableDictionaryConverter.cs deleted file mode 100644 index e19a5f51d75473..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultImmutableDictionaryConverter.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections; -using System.Diagnostics; - -namespace System.Text.Json.Serialization.Converters -{ - internal sealed class DefaultImmutableDictionaryConverter : JsonDictionaryConverter - { - public const string ImmutableDictionaryTypeName = "System.Collections.Immutable.ImmutableDictionary"; - public const string ImmutableDictionaryGenericTypeName = "System.Collections.Immutable.ImmutableDictionary`2"; - public const string ImmutableDictionaryGenericInterfaceTypeName = "System.Collections.Immutable.IImmutableDictionary`2"; - - public const string ImmutableSortedDictionaryTypeName = "System.Collections.Immutable.ImmutableSortedDictionary"; - public const string ImmutableSortedDictionaryGenericTypeName = "System.Collections.Immutable.ImmutableSortedDictionary`2"; - - public static void RegisterImmutableDictionary(Type immutableCollectionType, Type elementType, JsonSerializerOptions options) - { - // Get a unique identifier for a delegate which will point to the appropiate CreateRange method. - string delegateKey = DefaultImmutableEnumerableConverter.GetDelegateKey(immutableCollectionType, elementType, out Type underlyingType, out string constructingTypeName); - - // Exit if we have registered this immutable dictionary type. - if (options.CreateRangeDelegatesContainsKey(delegateKey)) - { - return; - } - - // Get the constructing type. - Type constructingType = underlyingType.Assembly.GetType(constructingTypeName)!; - - // Create a delegate which will point to the CreateRange method. - ImmutableCollectionCreator createRangeDelegate = options.MemberAccessorStrategy.ImmutableDictionaryCreateRange(constructingType, immutableCollectionType, elementType); - - // Cache the delegate - options.TryAddCreateRangeDelegate(delegateKey, createRangeDelegate); - } - - public static bool IsImmutableDictionary(Type type) - { - if (!type.IsGenericType) - { - return false; - } - - switch (type.GetGenericTypeDefinition().FullName) - { - case ImmutableDictionaryGenericTypeName: - case ImmutableDictionaryGenericInterfaceTypeName: - case ImmutableSortedDictionaryGenericTypeName: - return true; - default: - return false; - } - } - - public override object CreateFromDictionary(ref ReadStack state, IDictionary sourceDictionary, JsonSerializerOptions options) - { - Type immutableCollectionType = state.Current.JsonPropertyInfo!.RuntimePropertyType; - - JsonClassInfo elementClassInfo = state.Current.JsonPropertyInfo.ElementClassInfo!; - Type elementType = elementClassInfo.Type; - - string delegateKey = DefaultImmutableEnumerableConverter.GetDelegateKey(immutableCollectionType, elementType, out _, out _); - - JsonPropertyInfo propertyInfo = elementClassInfo.PolicyProperty ?? elementClassInfo.CreateRootProperty(options); - Debug.Assert(propertyInfo != null); - return propertyInfo.CreateImmutableDictionaryInstance(ref state, immutableCollectionType, delegateKey, sourceDictionary, options); - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultImmutableEnumerableConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultImmutableEnumerableConverter.cs deleted file mode 100644 index b347bb59ea1bdf..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultImmutableEnumerableConverter.cs +++ /dev/null @@ -1,143 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections; -using System.Diagnostics; - -namespace System.Text.Json.Serialization.Converters -{ - // This converter returns enumerables in the System.Collections.Immutable namespace. - internal sealed class DefaultImmutableEnumerableConverter : JsonEnumerableConverter - { - public const string ImmutableArrayTypeName = "System.Collections.Immutable.ImmutableArray"; - public const string ImmutableArrayGenericTypeName = "System.Collections.Immutable.ImmutableArray`1"; - - private const string ImmutableListTypeName = "System.Collections.Immutable.ImmutableList"; - public const string ImmutableListGenericTypeName = "System.Collections.Immutable.ImmutableList`1"; - public const string ImmutableListGenericInterfaceTypeName = "System.Collections.Immutable.IImmutableList`1"; - - private const string ImmutableStackTypeName = "System.Collections.Immutable.ImmutableStack"; - public const string ImmutableStackGenericTypeName = "System.Collections.Immutable.ImmutableStack`1"; - public const string ImmutableStackGenericInterfaceTypeName = "System.Collections.Immutable.IImmutableStack`1"; - - private const string ImmutableQueueTypeName = "System.Collections.Immutable.ImmutableQueue"; - public const string ImmutableQueueGenericTypeName = "System.Collections.Immutable.ImmutableQueue`1"; - public const string ImmutableQueueGenericInterfaceTypeName = "System.Collections.Immutable.IImmutableQueue`1"; - - public const string ImmutableSortedSetTypeName = "System.Collections.Immutable.ImmutableSortedSet"; - public const string ImmutableSortedSetGenericTypeName = "System.Collections.Immutable.ImmutableSortedSet`1"; - - private const string ImmutableHashSetTypeName = "System.Collections.Immutable.ImmutableHashSet"; - public const string ImmutableHashSetGenericTypeName = "System.Collections.Immutable.ImmutableHashSet`1"; - public const string ImmutableSetGenericInterfaceTypeName = "System.Collections.Immutable.IImmutableSet`1"; - - public static string GetDelegateKey( - Type immutableCollectionType, - Type elementType, - out Type underlyingType, - out string constructingTypeName) - { - // Use the generic type definition of the immutable collection to determine an appropriate constructing type, - // i.e. a type that we can invoke the `CreateRange` method on, which returns an assignable immutable collection. - underlyingType = immutableCollectionType.GetGenericTypeDefinition(); - - switch (underlyingType.FullName) - { - case ImmutableArrayGenericTypeName: - constructingTypeName = ImmutableArrayTypeName; - break; - case ImmutableListGenericTypeName: - case ImmutableListGenericInterfaceTypeName: - constructingTypeName = ImmutableListTypeName; - break; - case ImmutableStackGenericTypeName: - case ImmutableStackGenericInterfaceTypeName: - constructingTypeName = ImmutableStackTypeName; - break; - case ImmutableQueueGenericTypeName: - case ImmutableQueueGenericInterfaceTypeName: - constructingTypeName = ImmutableQueueTypeName; - break; - case ImmutableSortedSetGenericTypeName: - constructingTypeName = ImmutableSortedSetTypeName; - break; - case ImmutableHashSetGenericTypeName: - case ImmutableSetGenericInterfaceTypeName: - constructingTypeName = ImmutableHashSetTypeName; - break; - case DefaultImmutableDictionaryConverter.ImmutableDictionaryGenericTypeName: - case DefaultImmutableDictionaryConverter.ImmutableDictionaryGenericInterfaceTypeName: - constructingTypeName = DefaultImmutableDictionaryConverter.ImmutableDictionaryTypeName; - break; - case DefaultImmutableDictionaryConverter.ImmutableSortedDictionaryGenericTypeName: - constructingTypeName = DefaultImmutableDictionaryConverter.ImmutableSortedDictionaryTypeName; - break; - default: - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(immutableCollectionType, null, null); - } - - return $"{constructingTypeName}:{elementType.FullName}"; - } - - public static void RegisterImmutableCollection(Type immutableCollectionType, Type elementType, JsonSerializerOptions options) - { - // Get a unique identifier for a delegate which will point to the appropiate CreateRange method. - string delegateKey = GetDelegateKey(immutableCollectionType, elementType, out Type underlyingType, out string constructingTypeName); - - // Exit if we have registered this immutable collection type. - if (options.CreateRangeDelegatesContainsKey(delegateKey)) - { - return; - } - - // Get the constructing type. - Type constructingType = underlyingType.Assembly.GetType(constructingTypeName)!; - - // Create a delegate which will point to the CreateRange method. - ImmutableCollectionCreator createRangeDelegate = options.MemberAccessorStrategy.ImmutableCollectionCreateRange(constructingType, immutableCollectionType, elementType); - - // Cache the delegate - options.TryAddCreateRangeDelegate(delegateKey, createRangeDelegate); - } - - public static bool IsImmutableEnumerable(Type type) - { - if (!type.IsGenericType) - { - return false; - } - - switch (type.GetGenericTypeDefinition().FullName) - { - case ImmutableArrayGenericTypeName: - case ImmutableListGenericTypeName: - case ImmutableListGenericInterfaceTypeName: - case ImmutableStackGenericTypeName: - case ImmutableStackGenericInterfaceTypeName: - case ImmutableQueueGenericTypeName: - case ImmutableQueueGenericInterfaceTypeName: - case ImmutableSortedSetGenericTypeName: - case ImmutableHashSetGenericTypeName: - case ImmutableSetGenericInterfaceTypeName: - return true; - default: - return false; - } - } - - public override IEnumerable CreateFromList(ref ReadStack state, IList sourceList, JsonSerializerOptions options) - { - Type immutableCollectionType = state.Current.JsonPropertyInfo!.RuntimePropertyType; - - JsonClassInfo elementClassInfo = state.Current.JsonPropertyInfo.ElementClassInfo!; - Type elementType = elementClassInfo.Type; - - string delegateKey = GetDelegateKey(immutableCollectionType, elementType, out _, out _); - - JsonPropertyInfo propertyInfo = elementClassInfo.PolicyProperty ?? elementClassInfo.CreateRootProperty(options); - Debug.Assert(propertyInfo != null); - return propertyInfo.CreateImmutableCollectionInstance(ref state, immutableCollectionType, delegateKey, sourceList, options); - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonArrayConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonArrayConverter.cs new file mode 100644 index 00000000000000..4d3e2fc2cd9e98 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonArrayConverter.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Converter for System.Array. + /// + internal sealed class JsonArrayConverter + : JsonIEnumerableDefaultConverter + where TCollection: IEnumerable + { + internal override bool CanHaveIdMetadata => false; + + protected override void Add(TElement value, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is List); + ((List)state.Current.ReturnValue!).Add(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + state.Current.ReturnValue = new List(); + } + + protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) + { + Debug.Assert(state.Current.ReturnValue is List); + List list = (List)state.Current.ReturnValue!; + state.Current.ReturnValue = list.ToArray(); + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + Debug.Assert(value is TElement[]); + TElement[] array = (TElement[])(IEnumerable)value; + + int index = state.Current.EnumeratorIndex; + + JsonConverter elementConverter = GetElementConverter(ref state); + if (elementConverter.CanUseDirectReadOrWrite) + { + // Fast path that avoids validation and extra indirection. + for (; index < array.Length; index++) + { + elementConverter.Write(writer, array[index], options); + } + } + else + { + for (; index < array.Length; index++) + { + TElement element = array[index]; + if (!elementConverter.TryWrite(writer, element, options, ref state)) + { + state.Current.EnumeratorIndex = index; + return false; + } + + if (ShouldFlush(writer, ref state)) + { + state.Current.EnumeratorIndex = ++index; + return false; + } + } + } + + return true; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonConcurrentQueueOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonConcurrentQueueOfTConverter.cs new file mode 100644 index 00000000000000..4a4a10e8e2e369 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonConcurrentQueueOfTConverter.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class JsonConcurrentQueueOfTConverter : JsonIEnumerableDefaultConverter + where TCollection : ConcurrentQueue + { + protected override void Add(TElement value, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is TCollection); + ((TCollection)state.Current.ReturnValue!).Enqueue(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + if (state.Current.JsonClassInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(state.Current.JsonClassInfo.Type); + } + + state.Current.ReturnValue = state.Current.JsonClassInfo.CreateObject(); + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); + enumerator = (IEnumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetElementConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + TElement element = enumerator.Current; + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + } while (enumerator.MoveNext()); + + return true; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonConcurrentStackOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonConcurrentStackOfTConverter.cs new file mode 100644 index 00000000000000..5321d8d7161460 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonConcurrentStackOfTConverter.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class JsonConcurrentStackOfTConverter : JsonIEnumerableDefaultConverter + where TCollection : ConcurrentStack + { + protected override void Add(TElement value, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is TCollection); + ((TCollection)state.Current.ReturnValue!).Push(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + if (state.Current.JsonClassInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(state.Current.JsonClassInfo.Type); + } + + state.Current.ReturnValue = state.Current.JsonClassInfo.CreateObject(); + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); + enumerator = (IEnumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetElementConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + TElement element = enumerator.Current; + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + } while (enumerator.MoveNext()); + + return true; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonDictionaryDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonDictionaryDefaultConverter.cs new file mode 100644 index 00000000000000..62f64bfc8be1ad --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonDictionaryDefaultConverter.cs @@ -0,0 +1,307 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Default base class implementation of JsonDictionaryConverter{TCollection} . + /// + internal abstract class JsonDictionaryDefaultConverter + : JsonDictionaryConverter + { + /// + /// When overridden, adds the value to the collection. + /// + protected abstract void Add(TValue value, JsonSerializerOptions options, ref ReadStack state); + + /// + /// When overridden, converts the temporary collection held in state.Current.ReturnValue to the final collection. + /// This is used with immutable collections. + /// + protected virtual void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) { } + + /// + /// When overridden, create the collection. It may be a temporary collection or the final collection. + /// + protected virtual void CreateCollection(ref ReadStack state) { } + + internal override Type ElementType => typeof(TValue); + + protected static JsonConverter GetElementConverter(ref ReadStack state) + { + JsonConverter converter = (JsonConverter)state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!.ConverterBase; + Debug.Assert(converter != null); // It should not be possible to have a null converter at this point. + + return converter; + } + + protected string GetKeyName(string key, ref WriteStack state, JsonSerializerOptions options) + { + if (options.DictionaryKeyPolicy != null && !state.Current.IgnoreDictionaryKeyPolicy) + { + key = options.DictionaryKeyPolicy.ConvertName(key); + + if (key == null) + { + ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(options.DictionaryKeyPolicy.GetType()); + } + } + + return key; + } + + protected static JsonConverter GetValueConverter(ref WriteStack state) + { + JsonConverter converter = (JsonConverter)state.Current.DeclaredJsonPropertyInfo!.ConverterBase; + Debug.Assert(converter != null); // It should not be possible to have a null converter at this point. + + return converter; + } + + internal sealed override bool OnTryRead( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options, + ref ReadStack state, + out TCollection value) + { + bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences(); + + if (!state.SupportContinuation && !shouldReadPreservedReferences) + { + // Fast path that avoids maintaining state variables and dealing with preserved references. + + if (reader.TokenType != JsonTokenType.StartObject) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + CreateCollection(ref state); + + JsonConverter elementConverter = GetElementConverter(ref state); + if (elementConverter.CanUseDirectReadOrWrite) + { + // Process all elements. + while (true) + { + // Read the key name. + reader.ReadWithVerify(); + + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + state.Current.JsonPropertyNameAsString = reader.GetString(); + + // Read the value and add. + reader.ReadWithVerify(); + TValue element = elementConverter.Read(ref reader, typeof(TValue), options); + Add(element, options, ref state); + } + } + else + { + // Process all elements. + while (true) + { + // Read the key name. + reader.ReadWithVerify(); + + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + state.Current.JsonPropertyNameAsString = reader.GetString(); + + reader.ReadWithVerify(); + + // Get the value from the converter and add it. + elementConverter.TryRead(ref reader, typeof(TValue), options, ref state, out TValue element); + Add(element, options, ref state); + } + } + } + else + { + // Slower path that supports continuation and preserved references. + + if (state.Current.ObjectState == StackFrameObjectState.None) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + state.Current.ObjectState = StackFrameObjectState.StartToken; + } + + // Handle the metadata properties. + if (shouldReadPreservedReferences && state.Current.ObjectState < StackFrameObjectState.MetadataPropertyValue) + { + if (JsonSerializer.ResolveMetadata(this, ref reader, ref state)) + { + if (state.Current.ObjectState == StackFrameObjectState.MetadataRefPropertyEndObject) + { + value = (TCollection)state.Current.ReturnValue!; + return true; + } + } + else + { + value = default!; + return false; + } + } + + // Create the dictionary. + if (state.Current.ObjectState < StackFrameObjectState.CreatedObject) + { + CreateCollection(ref state); + + if (state.Current.MetadataId != null) + { + Debug.Assert(CanHaveIdMetadata); + + value = (TCollection)state.Current.ReturnValue!; + if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, value)) + { + ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId, ref state); + } + } + + state.Current.ObjectState = StackFrameObjectState.CreatedObject; + } + + // Process all elements. + JsonConverter elementConverter = GetElementConverter(ref state); + while (true) + { + if (state.Current.PropertyState == StackFramePropertyState.None) + { + state.Current.PropertyState = StackFramePropertyState.ReadName; + + // Read the key name. + if (!reader.Read()) + { + value = default!; + return false; + } + } + + // Determine the property. + if (state.Current.PropertyState < StackFramePropertyState.Name) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + state.Current.PropertyState = StackFramePropertyState.Name; + + // Verify property doesn't contain metadata. + if (shouldReadPreservedReferences) + { + ReadOnlySpan propertyName = reader.GetSpan(); + if (propertyName.Length > 0 && propertyName[0] == '$') + { + ThrowHelper.ThrowUnexpectedMetadataException(propertyName, ref reader, ref state); + } + } + + state.Current.JsonPropertyNameAsString = reader.GetString(); + } + + if (state.Current.PropertyState < StackFramePropertyState.ReadValue) + { + state.Current.PropertyState = StackFramePropertyState.ReadValue; + + if (!SingleValueReadWithReadAhead(elementConverter.ClassType, ref reader, ref state)) + { + value = default!; + return false; + } + } + + if (state.Current.PropertyState < StackFramePropertyState.TryRead) + { + // Get the value from the converter and add it. + bool success = elementConverter.TryRead(ref reader, typeof(TValue), options, ref state, out TValue element); + if (!success) + { + value = default!; + return false; + } + + Add(element, options, ref state); + state.Current.EndElement(); + } + } + } + + ConvertCollection(ref state, options); + value = (TCollection)state.Current.ReturnValue!; + return true; + } + + internal sealed override bool OnTryWrite( + Utf8JsonWriter writer, + TCollection dictionary, + JsonSerializerOptions options, + ref WriteStack state) + { + if (dictionary == null) + { + writer.WriteNullValue(); + return true; + } + + if (!state.Current.ProcessedStartToken) + { + state.Current.ProcessedStartToken = true; + writer.WriteStartObject(); + + if (options.ReferenceHandling.ShouldWritePreservedReferences()) + { + if (JsonSerializer.WriteReferenceForObject(this, dictionary, ref state, writer) == MetadataPropertyName.Ref) + { + return true; + } + } + + state.Current.DeclaredJsonPropertyInfo = state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!; + } + + bool success = OnWriteResume(writer, dictionary, options, ref state); + if (success) + { + if (!state.Current.ProcessedEndToken) + { + state.Current.ProcessedEndToken = true; + writer.WriteEndObject(); + } + } + + return success; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonDictionaryOfStringTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonDictionaryOfStringTValueConverter.cs new file mode 100644 index 00000000000000..ef539d201b9d6a --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonDictionaryOfStringTValueConverter.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Converter for Dictionary{string, TValue} that (de)serializes as a JSON object with properties + /// representing the dictionary element key and value. + /// + internal sealed class JsonDictionaryOfStringTValueConverter + : JsonDictionaryDefaultConverter + where TCollection : Dictionary + { + protected override void Add(TValue value, JsonSerializerOptions options, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is TCollection); + + string key = state.Current.JsonPropertyNameAsString!; + ((TCollection)state.Current.ReturnValue!)[key] = value; + } + + protected override void CreateCollection(ref ReadStack state) + { + if (state.Current.JsonClassInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(state.Current.JsonClassInfo.Type); + } + + state.Current.ReturnValue = state.Current.JsonClassInfo.CreateObject(); + } + + protected internal override bool OnWriteResume( + Utf8JsonWriter writer, + TCollection value, + JsonSerializerOptions options, + ref WriteStack state) + { + Dictionary.Enumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + Debug.Assert(state.Current.CollectionEnumerator is Dictionary.Enumerator); + enumerator = (Dictionary.Enumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetValueConverter(ref state); + if (!state.SupportContinuation && converter.CanUseDirectReadOrWrite) + { + // Fast path that avoids validation and extra indirection. + do + { + string key = GetKeyName(enumerator.Current.Key, ref state, options); + writer.WritePropertyName(key); + converter.Write(writer, enumerator.Current.Value, options); + } while (enumerator.MoveNext()); + } + else + { + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + TValue element = enumerator.Current.Value; + if (state.Current.PropertyState < StackFramePropertyState.Name) + { + state.Current.PropertyState = StackFramePropertyState.Name; + string key = GetKeyName(enumerator.Current.Key, ref state, options); + writer.WritePropertyName(key); + } + + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + state.Current.EndDictionaryElement(); + } while (enumerator.MoveNext()); + } + + return true; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonICollectionOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonICollectionOfTConverter.cs new file mode 100644 index 00000000000000..7a63ae126bcd1b --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonICollectionOfTConverter.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Converter for System.Collections.Generic.ICollection{TElement}. + /// + internal sealed class JsonICollectionOfTConverter + : JsonIEnumerableDefaultConverter + where TCollection : ICollection + { + protected override void Add(TElement value, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is ICollection); + ((ICollection)state.Current.ReturnValue!).Add(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + JsonClassInfo classInfo = state.Current.JsonClassInfo; + + if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) + { + if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + state.Current.ReturnValue = new List(); + } + else + { + if (classInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + TCollection returnValue = (TCollection)classInfo.CreateObject()!; + + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(TypeToConvert); + } + + state.Current.ReturnValue = returnValue; + } + } + + protected override bool OnWriteResume( + Utf8JsonWriter writer, + TCollection value, + JsonSerializerOptions options, + ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); + enumerator = (IEnumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetElementConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + TElement element = enumerator.Current; + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType + { + get + { + if (TypeToConvert.IsAbstract || TypeToConvert.IsInterface) + { + return typeof(List); + } + + return TypeToConvert; + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIDictionaryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIDictionaryConverter.cs new file mode 100644 index 00000000000000..814e1b9cc62d78 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIDictionaryConverter.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Converter for System.Collections.IDictionary that (de)serializes as a JSON object with properties + /// representing the dictionary element key and value. + /// + internal sealed class JsonIDictionaryConverter + : JsonDictionaryDefaultConverter + where TCollection : IDictionary + { + protected override void Add(object? value, JsonSerializerOptions options, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is IDictionary); + + string key = state.Current.JsonPropertyNameAsString!; + ((IDictionary)state.Current.ReturnValue!)[key] = value; + } + + protected override void CreateCollection(ref ReadStack state) + { + JsonClassInfo classInfo = state.Current.JsonClassInfo; + + if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) + { + if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + state.Current.ReturnValue = new Dictionary(); + } + else + { + if (classInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + TCollection returnValue = (TCollection)classInfo.CreateObject()!; + + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(TypeToConvert); + } + + state.Current.ReturnValue = returnValue; + } + } + + protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + IDictionaryEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + Debug.Assert(state.Current.CollectionEnumerator is IDictionaryEnumerator); + enumerator = (IDictionaryEnumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetValueConverter(ref state); + do + { + if (enumerator.Key is string key) + { + key = GetKeyName(key, ref state, options); + writer.WritePropertyName(key); + + object? element = enumerator.Value; + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + state.Current.EndDictionaryElement(); + } + else + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.DeclaredJsonPropertyInfo!.RuntimePropertyType!); + } + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType + { + get + { + if (TypeToConvert.IsAbstract || TypeToConvert.IsInterface) + { + return typeof(Dictionary); + } + + return TypeToConvert; + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIDictionaryOfStringTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIDictionaryOfStringTValueConverter.cs new file mode 100644 index 00000000000000..41e03f178d1fe4 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIDictionaryOfStringTValueConverter.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Converter for System.Collections.Generic.IDictionary{string, TValue} that + /// (de)serializes as a JSON object with properties representing the dictionary element key and value. + /// + internal sealed class JsonIDictionaryOfStringTValueConverter + : JsonDictionaryDefaultConverter + where TCollection : IDictionary + { + protected override void Add(TValue value, JsonSerializerOptions options, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is TCollection); + + string key = state.Current.JsonPropertyNameAsString!; + ((TCollection)state.Current.ReturnValue!)[key] = value; + } + + protected override void CreateCollection(ref ReadStack state) + { + JsonClassInfo classInfo = state.Current.JsonClassInfo; + + if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) + { + if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + state.Current.ReturnValue = new Dictionary(); + } + else + { + if (classInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + TCollection returnValue = (TCollection)classInfo.CreateObject()!; + + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(TypeToConvert); + } + + state.Current.ReturnValue = returnValue; + } + } + + protected internal override bool OnWriteResume( + Utf8JsonWriter writer, + TCollection value, + JsonSerializerOptions options, + ref WriteStack state) + { + IEnumerator> enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator>); + enumerator = (IEnumerator>)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetValueConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + string key = GetKeyName(enumerator.Current.Key, ref state, options); + writer.WritePropertyName(key); + + TValue element = enumerator.Current.Value; + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + state.Current.EndDictionaryElement(); + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType + { + get + { + if (TypeToConvert.IsAbstract || TypeToConvert.IsInterface) + { + return typeof(Dictionary); + } + + return TypeToConvert; + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableConverter.cs new file mode 100644 index 00000000000000..2712385e03ec44 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableConverter.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Converter for System.Collections.IEnumerable. + /// + /// + internal sealed class JsonIEnumerableConverter + : JsonIEnumerableDefaultConverter + where TCollection : IEnumerable + { + protected override void Add(object? value, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is List); + ((List)state.Current.ReturnValue!).Add(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + state.Current.ReturnValue = new List(); + } + + // Consider overriding ConvertCollection to convert the list to an array since a List is mutable. + // However, converting from the temporary list to an array will be slower. + + protected override bool OnWriteResume( + Utf8JsonWriter writer, + TCollection value, + JsonSerializerOptions options, + ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + enumerator = state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetElementConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + if (!converter.TryWrite(writer, enumerator.Current, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType => typeof(List); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableConverterFactory.cs new file mode 100644 index 00000000000000..0081359d7165bc --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableConverterFactory.cs @@ -0,0 +1,239 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Converter factory for all IEnumerable types. + /// + internal class JsonIEnumerableConverterFactory : JsonConverterFactory + { + private static readonly JsonIDictionaryConverter s_converterForIDictionary = new JsonIDictionaryConverter(); + private static readonly JsonIEnumerableConverter s_converterForIEnumerable = new JsonIEnumerableConverter(); + private static readonly JsonIListConverter s_converterForIList = new JsonIListConverter(); + + public override bool CanConvert(Type typeToConvert) + { + return typeof(IEnumerable).IsAssignableFrom(typeToConvert); + } + + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonArrayConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonConcurrentQueueOfTConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonConcurrentStackOfTConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonDefaultArrayConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonDictionaryOfStringTValueConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonICollectionOfTConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonIDictionaryOfStringTValueConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonIEnumerableOfTConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonIEnumerableWithAddMethodConverter`1")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonIListConverter`1")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonIListOfTConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonImmutableDictionaryOfStringTValueConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonImmutableEnumerableOfTConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonIReadOnlyDictionaryOfStringTValueConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonISetOfTConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonListOfTConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonQueueOfTConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonStackOfTConverter`2")] + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + JsonConverter? converter = null; + Type converterType; + Type[] genericArgs; + Type? elementType = null; + Type? actualTypeToConvert; + + // Array + if (typeToConvert.IsArray) + { + // Verify that we don't have a multidimensional array. + if (typeToConvert.GetArrayRank() > 1) + { + return null; + } + + converterType = typeof(JsonArrayConverter<,>); + elementType = typeToConvert.GetElementType(); + } + // List<> or deriving from List<> + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericBaseClass(typeof(List<>))) != null) + { + converterType = typeof(JsonListOfTConverter<,>); + elementType = actualTypeToConvert.GetGenericArguments()[0]; + } + // Dictionary or deriving from Dictionary + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericBaseClass(typeof(Dictionary<,>))) != null) + { + genericArgs = actualTypeToConvert.GetGenericArguments(); + if (genericArgs[0] == typeof(string)) + { + converterType = typeof(JsonDictionaryOfStringTValueConverter<,>); + elementType = genericArgs[1]; + } + else + { + return null; + } + } + // Immutable dictionaries from System.Collections.Immutable, e.g. ImmutableDictionary + else if (typeToConvert.IsImmutableDictionaryType()) + { + genericArgs = typeToConvert.GetGenericArguments(); + if (genericArgs[0] == typeof(string)) + { + converterType = typeof(JsonImmutableDictionaryOfStringTValueConverter<,>); + elementType = genericArgs[1]; + } + else + { + return null; + } + } + // IDictionary or deriving from IDictionary + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IDictionary<,>))) != null) + { + genericArgs = actualTypeToConvert.GetGenericArguments(); + if (genericArgs[0] == typeof(string)) + { + converterType = typeof(JsonIDictionaryOfStringTValueConverter<,>); + elementType = genericArgs[1]; + } + else + { + return null; + } + } + // IReadOnlyDictionary or deriving from IReadOnlyDictionary + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IReadOnlyDictionary<,>))) != null) + { + genericArgs = actualTypeToConvert.GetGenericArguments(); + if (genericArgs[0] == typeof(string)) + { + converterType = typeof(JsonIReadOnlyDictionaryOfStringTValueConverter<,>); + elementType = genericArgs[1]; + } + else + { + return null; + } + } + // Immutable non-dictionaries from System.Collections.Immutable, e.g. ImmutableStack + else if (typeToConvert.IsImmutableEnumerableType()) + { + converterType = typeof(JsonImmutableEnumerableOfTConverter<,>); + elementType = typeToConvert.GetGenericArguments()[0]; + } + // IList<> + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IList<>))) != null) + { + converterType = typeof(JsonIListOfTConverter<,>); + elementType = actualTypeToConvert.GetGenericArguments()[0]; + } + // ISet<> + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(ISet<>))) != null) + { + converterType = typeof(JsonISetOfTConverter<,>); + elementType = actualTypeToConvert.GetGenericArguments()[0]; + } + // ICollection<> + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(ICollection<>))) != null) + { + converterType = typeof(JsonICollectionOfTConverter<,>); + elementType = actualTypeToConvert.GetGenericArguments()[0]; + } + // Stack<> or deriving from Stack<> + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericBaseClass(typeof(Stack<>))) != null) + { + converterType = typeof(JsonStackOfTConverter<,>); + elementType = actualTypeToConvert.GetGenericArguments()[0]; + } + // Queue<> or deriving from Queue<> + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericBaseClass(typeof(Queue<>))) != null) + { + converterType = typeof(JsonQueueOfTConverter<,>); + elementType = actualTypeToConvert.GetGenericArguments()[0]; + } + // ConcurrentStack<> or deriving from ConcurrentStack<> + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericBaseClass(typeof(ConcurrentStack<>))) != null) + { + converterType = typeof(JsonConcurrentStackOfTConverter<,>); + elementType = actualTypeToConvert.GetGenericArguments()[0]; + } + // ConcurrentQueue<> or deriving from ConcurrentQueue<> + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericBaseClass(typeof(ConcurrentQueue<>))) != null) + { + converterType = typeof(JsonConcurrentQueueOfTConverter<,>); + elementType = actualTypeToConvert.GetGenericArguments()[0]; + } + // IEnumerable<>, types assignable from List<> + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IEnumerable<>))) != null) + { + converterType = typeof(JsonIEnumerableOfTConverter<,>); + elementType = actualTypeToConvert.GetGenericArguments()[0]; + } + // Check for non-generics after checking for generics. + else if (typeof(IDictionary).IsAssignableFrom(typeToConvert)) + { + if (typeToConvert == typeof(IDictionary)) + { + return s_converterForIDictionary; + } + + converterType = typeof(JsonIDictionaryConverter<>); + } + else if (typeof(IList).IsAssignableFrom(typeToConvert)) + { + if (typeToConvert == typeof(IList)) + { + return s_converterForIList; + } + + converterType = typeof(JsonIListConverter<>); + } + else if (typeToConvert.IsNonGenericStackOrQueue()) + { + converterType = typeof(JsonIEnumerableWithAddMethodConverter<>); + } + else + { + Debug.Assert(typeof(IEnumerable).IsAssignableFrom(typeToConvert)); + if (typeToConvert == typeof(IEnumerable)) + { + return s_converterForIEnumerable; + } + + converterType = typeof(JsonIEnumerableConverter<>); + } + + if (converterType != null) + { + Type genericType; + if (converterType.GetGenericArguments().Length == 1) + { + genericType = converterType.MakeGenericType(typeToConvert); + } + else + { + genericType = converterType.MakeGenericType(typeToConvert, elementType!); + } + + converter = (JsonConverter)Activator.CreateInstance( + genericType, + BindingFlags.Instance | BindingFlags.Public, + binder: null, + args: null, + culture: null)!; + } + + return converter; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableDefaultConverter.cs new file mode 100644 index 00000000000000..b4911e45cbc656 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableDefaultConverter.cs @@ -0,0 +1,294 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Default base class implementation of JsonIEnumerableConverter{TCollection, TElement}. + /// + internal abstract class JsonIEnumerableDefaultConverter : JsonCollectionConverter + { + protected abstract void Add(TElement value, ref ReadStack state); + protected abstract void CreateCollection(ref ReadStack state, JsonSerializerOptions options); + protected virtual void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) { } + + protected static JsonConverter GetElementConverter(ref ReadStack state) + { + JsonConverter converter = (JsonConverter)state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!.ConverterBase; + Debug.Assert(converter != null); // It should not be possible to have a null converter at this point. + + return converter; + } + + protected static JsonConverter GetElementConverter(ref WriteStack state) + { + JsonConverter converter = (JsonConverter)state.Current.DeclaredJsonPropertyInfo!.ConverterBase; + Debug.Assert(converter != null); // It should not be possible to have a null converter at this point. + + return converter; + } + + internal override bool OnTryRead( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options, + ref ReadStack state, + out TCollection value) + { + bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences(); + + if (!state.SupportContinuation && !shouldReadPreservedReferences) + { + // Fast path that avoids maintaining state variables and dealing with preserved references. + + if (reader.TokenType != JsonTokenType.StartArray) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + CreateCollection(ref state, options); + + JsonConverter elementConverter = GetElementConverter(ref state); + if (elementConverter.CanUseDirectReadOrWrite) + { + // Fast path that avoids validation and extra indirection. + while (true) + { + reader.ReadWithVerify(); + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + + // Obtain the CLR value from the JSON and apply to the object. + TElement element = elementConverter.Read(ref reader, elementConverter.TypeToConvert, options); + Add(element, ref state); + } + } + else + { + // Process all elements. + while (true) + { + reader.ReadWithVerify(); + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + + // Get the value from the converter and add it. + elementConverter.TryRead(ref reader, typeof(TElement), options, ref state, out TElement element); + Add(element, ref state); + } + } + } + else + { + // Slower path that supports continuation and preserved references. + + if (state.Current.ObjectState == StackFrameObjectState.None) + { + if (reader.TokenType == JsonTokenType.StartArray) + { + state.Current.ObjectState = StackFrameObjectState.MetadataPropertyValue; + } + else if (shouldReadPreservedReferences) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + state.Current.ObjectState = StackFrameObjectState.StartToken; + } + else + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + } + + // Handle the metadata properties. + if (shouldReadPreservedReferences && state.Current.ObjectState < StackFrameObjectState.MetadataPropertyValue) + { + if (JsonSerializer.ResolveMetadata(this, ref reader, ref state)) + { + if (state.Current.ObjectState == StackFrameObjectState.MetadataRefPropertyEndObject) + { + value = (TCollection)state.Current.ReturnValue!; + return true; + } + } + else + { + value = default!; + return false; + } + } + + if (state.Current.ObjectState < StackFrameObjectState.CreatedObject) + { + CreateCollection(ref state, options); + + if (state.Current.MetadataId != null) + { + value = (TCollection)state.Current.ReturnValue!; + if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, value)) + { + ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId, ref state); + } + } + + state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!; + state.Current.ObjectState = StackFrameObjectState.CreatedObject; + } + + if (state.Current.ObjectState < StackFrameObjectState.ReadElements) + { + JsonConverter elementConverter = GetElementConverter(ref state); + + // Process all elements. + while (true) + { + if (state.Current.PropertyState < StackFramePropertyState.ReadValue) + { + state.Current.PropertyState = StackFramePropertyState.ReadValue; + + if (!SingleValueReadWithReadAhead(elementConverter.ClassType, ref reader, ref state)) + { + value = default!; + return false; + } + } + + if (state.Current.PropertyState < StackFramePropertyState.ReadValueIsEnd) + { + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + + state.Current.PropertyState = StackFramePropertyState.ReadValueIsEnd; + } + + if (state.Current.PropertyState < StackFramePropertyState.TryRead) + { + // Get the value from the converter and add it. + if (!elementConverter.TryRead(ref reader, typeof(TElement), options, ref state, out TElement element)) + { + value = default!; + return false; + } + + Add(element, ref state); + + // No need to set PropertyState to TryRead since we're done with this element now. + state.Current.EndElement(); + } + } + + state.Current.ObjectState = StackFrameObjectState.ReadElements; + } + + if (state.Current.ObjectState < StackFrameObjectState.EndToken) + { + state.Current.ObjectState = StackFrameObjectState.EndToken; + + // Read the EndObject for $values. + if (state.Current.MetadataId != null) + { + if (!reader.Read()) + { + value = default!; + return false; + } + + if (reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + state.Current.JsonPropertyName = reader.GetSpan().ToArray(); + } + + ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(typeToConvert, reader); + } + } + } + + if (state.Current.ObjectState < StackFrameObjectState.EndTokenValidation) + { + if (state.Current.MetadataId != null) + { + if (reader.TokenType != JsonTokenType.EndObject) + { + ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(typeToConvert, reader); + } + } + } + } + + ConvertCollection(ref state, options); + value = (TCollection)state.Current.ReturnValue!; + return true; + } + + internal sealed override bool OnTryWrite(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + bool success; + + if (value == null) + { + writer.WriteNullValue(); + success = true; + } + else + { + bool shouldWritePreservedReferences = options.ReferenceHandling.ShouldWritePreservedReferences(); + + if (!state.Current.ProcessedStartToken) + { + state.Current.ProcessedStartToken = true; + + if (!shouldWritePreservedReferences) + { + writer.WriteStartArray(); + } + else + { + MetadataPropertyName metadata = JsonSerializer.WriteReferenceForCollection(this, value, ref state, writer); + if (metadata == MetadataPropertyName.Ref) + { + return true; + } + + state.Current.MetadataPropertyName = metadata; + } + + state.Current.DeclaredJsonPropertyInfo = state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!; + } + + success = OnWriteResume(writer, value, options, ref state); + if (success) + { + if (!state.Current.ProcessedEndToken) + { + state.Current.ProcessedEndToken = true; + writer.WriteEndArray(); + + if (state.Current.MetadataPropertyName == MetadataPropertyName.Id) + { + // Write the EndObject for $values. + writer.WriteEndObject(); + } + } + } + } + + return success; + } + + protected abstract bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableOfTConverter.cs new file mode 100644 index 00000000000000..d1f2269366f9c5 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableOfTConverter.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Converter for System.Collections.Generic.IEnumerable{TElement}. + /// + internal sealed class JsonIEnumerableOfTConverter : JsonIEnumerableDefaultConverter + where TCollection : IEnumerable + { + protected override void Add(TElement value, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is List); + ((List)state.Current.ReturnValue!).Add(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + state.Current.ReturnValue = new List(); + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); + enumerator = (IEnumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetElementConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + TElement element = enumerator.Current; + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType => typeof(List); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableWithAddMethodConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableWithAddMethodConverter.cs new file mode 100644 index 00000000000000..55bd1b27352d49 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableWithAddMethodConverter.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class JsonIEnumerableWithAddMethodConverter : + JsonIEnumerableDefaultConverter + where TCollection : IEnumerable + { + protected override void Add(object? value, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is TCollection); + Debug.Assert(state.Current.AddMethodDelegate != null); + ((Action)state.Current.AddMethodDelegate)((TCollection)state.Current.ReturnValue!, value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + JsonClassInfo.ConstructorDelegate? constructorDelegate = state.Current.JsonClassInfo.CreateObject; + + if (constructorDelegate == null) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + state.Current.ReturnValue = constructorDelegate(); + state.Current.AddMethodDelegate = GetAddMethodDelegate(options); + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + enumerator = state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetElementConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + if (!converter.TryWrite(writer, enumerator.Current, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + } while (enumerator.MoveNext()); + + return true; + } + + private Action? _addMethodDelegate; + + internal Action GetAddMethodDelegate(JsonSerializerOptions options) + { + if (_addMethodDelegate == null) + { + // We verified this exists when we created the converter in the enumerable converter factory. + _addMethodDelegate = options.MemberAccessorStrategy.CreateAddMethodDelegate(); + } + + return _addMethodDelegate; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIListConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIListConverter.cs new file mode 100644 index 00000000000000..71785fdc2a2f20 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIListConverter.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + /// Converter for System.Collections.IList. + internal sealed class JsonIListConverter : JsonIEnumerableDefaultConverter + where TCollection : IList + { + protected override void Add(object? value, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is IList); + ((IList)state.Current.ReturnValue).Add(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + JsonClassInfo classInfo = state.Current.JsonClassInfo; + + if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) + { + if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + state.Current.ReturnValue = new List(); + } + else + { + if (classInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + TCollection returnValue = (TCollection)classInfo.CreateObject()!; + + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(TypeToConvert); + } + + state.Current.ReturnValue = returnValue; + } + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + enumerator = state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetElementConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + object? element = enumerator.Current; + + if (!converter.TryWrite(writer, element!, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType + { + get + { + if (TypeToConvert.IsAbstract || TypeToConvert.IsInterface) + { + return typeof(List); + } + + return TypeToConvert; + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIListOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIListOfTConverter.cs new file mode 100644 index 00000000000000..f227857cbb265d --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIListOfTConverter.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Converter for System.Collections.Generic.IList{TElement}. + /// + internal sealed class JsonIListOfTConverter : JsonIEnumerableDefaultConverter + where TCollection : IList + { + protected override void Add(TElement value, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is TCollection); + ((TCollection)state.Current.ReturnValue!).Add(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + JsonClassInfo classInfo = state.Current.JsonClassInfo; + + if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) + { + if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + state.Current.ReturnValue = new List(); + } + else + { + if (classInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + TCollection returnValue = (TCollection)classInfo.CreateObject()!; + + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(TypeToConvert); + } + + state.Current.ReturnValue = returnValue; + } + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); + enumerator = (IEnumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetElementConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + TElement element = enumerator.Current; + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType + { + get + { + if (TypeToConvert.IsAbstract || TypeToConvert.IsInterface) + { + return typeof(List); + } + + return TypeToConvert; + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIReadOnlyDictionaryOfStringTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIReadOnlyDictionaryOfStringTValueConverter.cs new file mode 100644 index 00000000000000..5dfcc36e064f5c --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIReadOnlyDictionaryOfStringTValueConverter.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class JsonIReadOnlyDictionaryOfStringTValueConverter : JsonDictionaryDefaultConverter + where TCollection : IReadOnlyDictionary + { + protected override void Add(TValue value, JsonSerializerOptions options, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is Dictionary); + + string key = state.Current.JsonPropertyNameAsString!; + ((Dictionary)state.Current.ReturnValue!)[key] = value; + } + + protected override void CreateCollection(ref ReadStack state) + { + if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + state.Current.ReturnValue = new Dictionary(); + } + + protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator> enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + Debug.Assert(state.Current.CollectionEnumerator is Dictionary.Enumerator); + enumerator = (Dictionary.Enumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetValueConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + string key = GetKeyName(enumerator.Current.Key, ref state, options); + writer.WritePropertyName(key); + + TValue element = enumerator.Current.Value; + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + state.Current.EndDictionaryElement(); + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType => typeof(Dictionary); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonISetOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonISetOfTConverter.cs new file mode 100644 index 00000000000000..eb9b79e00a210c --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonISetOfTConverter.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class JsonISetOfTConverter : JsonIEnumerableDefaultConverter + where TCollection : ISet + { + protected override void Add(TElement value, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is TCollection); + ((TCollection)state.Current.ReturnValue!).Add(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + JsonClassInfo classInfo = state.Current.JsonClassInfo; + + if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) + { + if (!TypeToConvert.IsAssignableFrom(RuntimeType)) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + state.Current.ReturnValue = new HashSet(); + } + else + { + if (classInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); + } + + TCollection returnValue = (TCollection)classInfo.CreateObject()!; + + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(TypeToConvert); + } + + state.Current.ReturnValue = returnValue; + } + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); + enumerator = (IEnumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetElementConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + TElement element = enumerator.Current; + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType + { + get + { + if (TypeToConvert.IsAbstract || TypeToConvert.IsInterface) + { + return typeof(HashSet); + } + + return TypeToConvert; + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonImmutableDictionaryOfStringTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonImmutableDictionaryOfStringTValueConverter.cs new file mode 100644 index 00000000000000..0e12a8e87f446e --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonImmutableDictionaryOfStringTValueConverter.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class JsonImmutableDictionaryOfStringTValueConverter : JsonDictionaryDefaultConverter + where TCollection : IReadOnlyDictionary + { + protected override void Add(TValue value, JsonSerializerOptions options, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is Dictionary); + + string key = state.Current.JsonPropertyNameAsString!; + ((Dictionary)state.Current.ReturnValue!)[key] = value; + } + + internal override bool CanHaveIdMetadata => false; + + protected override void CreateCollection(ref ReadStack state) + { + state.Current.ReturnValue = new Dictionary(); + } + + protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) + { + state.Current.ReturnValue = GetCreatorDelegate(options)((Dictionary)state.Current.ReturnValue!); + } + + protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator> enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + Debug.Assert(state.Current.CollectionEnumerator is Dictionary.Enumerator); + enumerator = (Dictionary.Enumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetValueConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + string key = GetKeyName(enumerator.Current.Key, ref state, options); + writer.WritePropertyName(key); + + TValue element = enumerator.Current.Value; + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + state.Current.EndDictionaryElement(); + } while (enumerator.MoveNext()); + + return true; + } + + private Func>, TCollection>? _creatorDelegate; + + private Func>, TCollection> GetCreatorDelegate(JsonSerializerOptions options) + { + if (_creatorDelegate == null) + { + _creatorDelegate = options.MemberAccessorStrategy.CreateImmutableDictionaryCreateRangeDelegate(); + } + + return _creatorDelegate; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonImmutableEnumerableOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonImmutableEnumerableOfTConverter.cs new file mode 100644 index 00000000000000..d3cd9bcd45512f --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonImmutableEnumerableOfTConverter.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class JsonImmutableEnumerableOfTConverter : JsonIEnumerableDefaultConverter + where TCollection : IEnumerable + { + protected override void Add(TElement value, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is List); + ((List)state.Current.ReturnValue!).Add(value); + } + + internal override bool CanHaveIdMetadata => false; + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + state.Current.ReturnValue = new List(); + } + + protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) + { + state.Current.ReturnValue = GetCreatorDelegate(options)((List)state.Current.ReturnValue!); + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); + enumerator = (IEnumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetElementConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + TElement element = enumerator.Current; + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + } while (enumerator.MoveNext()); + + return true; + } + + private Func, TCollection>? _creatorDelegate; + + private Func, TCollection> GetCreatorDelegate(JsonSerializerOptions options) + { + if (_creatorDelegate == null) + { + _creatorDelegate = options.MemberAccessorStrategy.CreateImmutableEnumerableCreateRangeDelegate(); + } + + return _creatorDelegate; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonListOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonListOfTConverter.cs new file mode 100644 index 00000000000000..31c94131fb03ad --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonListOfTConverter.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + /// Converter for System.Collections.Generic.List{TElement}. + internal sealed class JsonListOfTConverter + : JsonIEnumerableDefaultConverter + where TCollection: List + { + protected override void Add(TElement value, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is TCollection); + ((TCollection)state.Current.ReturnValue!).Add(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + if (state.Current.JsonClassInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(state.Current.JsonClassInfo.Type); + } + + state.Current.ReturnValue = state.Current.JsonClassInfo.CreateObject(); + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + List list = value; + + // Using an index is 2x faster than using an enumerator. + int index = state.Current.EnumeratorIndex; + JsonConverter elementConverter = GetElementConverter(ref state); + + if (elementConverter.CanUseDirectReadOrWrite) + { + // Fast path that avoids validation and extra indirection. + for (; index < list.Count; index++) + { + elementConverter.Write(writer, list[index], options); + } + } + else + { + for (; index < list.Count; index++) + { + TElement element = list[index]; + if (!elementConverter.TryWrite(writer, element, options, ref state)) + { + state.Current.EnumeratorIndex = index; + return false; + } + + if (ShouldFlush(writer, ref state)) + { + state.Current.EnumeratorIndex = ++index; + return false; + } + } + } + + return true; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectConverterFactory.cs new file mode 100644 index 00000000000000..00d267a7e01faa --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectConverterFactory.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Diagnostics; +using System.Reflection; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Converter factory for all object-based types (non-enumerable and non-primitive). + /// + internal class JsonObjectConverterFactory : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) + { + // This is the last built-in factory converter, so if the IEnumerableConverterFactory doesn't + // support it, then it is not IEnumerable. + Debug.Assert(!typeof(IEnumerable).IsAssignableFrom(typeToConvert)); + return true; + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + JsonConverter converter = (JsonConverter)Activator.CreateInstance( + typeof(JsonObjectDefaultConverter<>).MakeGenericType(typeToConvert), + BindingFlags.Instance | BindingFlags.Public, + binder: null, + args: null, + culture: null)!; + + return converter; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectDefaultConverter.cs new file mode 100644 index 00000000000000..49c701a3348d4e --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectDefaultConverter.cs @@ -0,0 +1,411 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Default base class implementation of JsonObjectConverter{T}. + /// + internal sealed class JsonObjectDefaultConverter : JsonObjectConverter + { + internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T value) + { + bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences(); + object obj; + + if (!state.SupportContinuation && !shouldReadPreservedReferences) + { + // Fast path that avoids maintaining state variables and dealing with preserved references. + + if (reader.TokenType != JsonTokenType.StartObject) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + if (state.Current.JsonClassInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(state.Current.JsonClassInfo.Type); + } + + obj = state.Current.JsonClassInfo.CreateObject!()!; + + // Process all properties. + while (true) + { + // Read the property name or EndObject. + reader.ReadWithVerify(); + + JsonTokenType tokenType = reader.TokenType; + if (tokenType == JsonTokenType.EndObject) + { + break; + } + + if (tokenType != JsonTokenType.PropertyName) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + JsonPropertyInfo jsonPropertyInfo = JsonSerializer.LookupProperty( + obj, + ref reader, + options, + ref state, + out bool useExtensionProperty); + + // Skip the property if not found. + if (!jsonPropertyInfo.ShouldDeserialize) + { + reader.Skip(); + state.Current.EndProperty(); + continue; + } + + // Set the property value. + reader.ReadWithVerify(); + + if (!useExtensionProperty) + { + jsonPropertyInfo.ReadJsonAndSetMember(obj, ref state, ref reader); + } + else + { + jsonPropertyInfo.ReadJsonAndAddExtensionProperty(obj, ref state, ref reader); + } + + // Ensure any exception thrown in the next read does not have a property in its JsonPath. + state.Current.EndProperty(); + } + } + else + { + // Slower path that supports continuation and preserved references. + + if (state.Current.ObjectState == StackFrameObjectState.None) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + state.Current.ObjectState = StackFrameObjectState.StartToken; + } + + // Handle the metadata properties. + if (state.Current.ObjectState < StackFrameObjectState.MetadataPropertyValue) + { + if (shouldReadPreservedReferences) + { + if (JsonSerializer.ResolveMetadata(this, ref reader, ref state)) + { + if (state.Current.ObjectState == StackFrameObjectState.MetadataRefPropertyEndObject) + { + value = (T)state.Current.ReturnValue!; + return true; + } + } + else + { + value = default!; + return false; + } + } + + state.Current.ObjectState = StackFrameObjectState.MetadataPropertyValue; + } + + if (state.Current.ObjectState < StackFrameObjectState.CreatedObject) + { + if (state.Current.JsonClassInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(state.Current.JsonClassInfo.Type); + } + + obj = state.Current.JsonClassInfo.CreateObject!()!; + if (state.Current.MetadataId != null) + { + if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, obj)) + { + ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId, ref state); + } + } + + state.Current.ReturnValue = obj; + state.Current.ObjectState = StackFrameObjectState.CreatedObject; + } + else + { + obj = state.Current.ReturnValue!; + Debug.Assert(obj != null); + } + + // Process all properties. + while (true) + { + // Determine the property. + if (state.Current.PropertyState == StackFramePropertyState.None) + { + state.Current.PropertyState = StackFramePropertyState.ReadName; + + if (!reader.Read()) + { + // The read-ahead functionality will do the Read(). + state.Current.ReturnValue = obj; + value = default!; + return false; + } + } + + JsonPropertyInfo jsonPropertyInfo; + + if (state.Current.PropertyState < StackFramePropertyState.Name) + { + state.Current.PropertyState = StackFramePropertyState.Name; + + JsonTokenType tokenType = reader.TokenType; + if (tokenType == JsonTokenType.EndObject) + { + // We are done reading properties. + break; + } + else if (tokenType != JsonTokenType.PropertyName) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + jsonPropertyInfo = JsonSerializer.LookupProperty( + obj, + ref reader, + options, + ref state, + out bool useExtensionProperty); + + state.Current.UseExtensionProperty = useExtensionProperty; + } + else + { + Debug.Assert(state.Current.JsonPropertyInfo != null); + jsonPropertyInfo = state.Current.JsonPropertyInfo!; + } + + if (state.Current.PropertyState < StackFramePropertyState.ReadValue) + { + if (!jsonPropertyInfo.ShouldDeserialize) + { + if (!reader.TrySkip()) + { + state.Current.ReturnValue = obj; + value = default!; + return false; + } + + state.Current.EndProperty(); + continue; + } + + // Returning false below will cause the read-ahead functionality to finish the read. + state.Current.PropertyState = StackFramePropertyState.ReadValue; + + if (!state.Current.UseExtensionProperty) + { + if (!SingleValueReadWithReadAhead(jsonPropertyInfo.ConverterBase.ClassType, ref reader, ref state)) + { + state.Current.ReturnValue = obj; + value = default!; + return false; + } + } + else + { + // The actual converter is JsonElement, so force a read-ahead. + if (!SingleValueReadWithReadAhead(ClassType.Value, ref reader, ref state)) + { + state.Current.ReturnValue = obj; + value = default!; + return false; + } + } + } + + if (state.Current.PropertyState < StackFramePropertyState.TryRead) + { + // Obtain the CLR value from the JSON and set the member. + if (!state.Current.UseExtensionProperty) + { + if (!jsonPropertyInfo.ReadJsonAndSetMember(obj, ref state, ref reader)) + { + state.Current.ReturnValue = obj; + value = default!; + return false; + } + } + else + { + if (!jsonPropertyInfo.ReadJsonAndAddExtensionProperty(obj, ref state, ref reader)) + { + // No need to set 'value' here since JsonElement must be read in full. + state.Current.ReturnValue = obj; + value = default!; + return false; + } + } + + state.Current.EndProperty(); + } + } + } + + // Check if we are trying to build the sorted cache. + if (state.Current.PropertyRefCache != null) + { + state.Current.JsonClassInfo.UpdateSortedPropertyCache(ref state.Current); + } + + value = (T)obj; + + return true; + } + + internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state) + { + // Minimize boxing for structs by only boxing once here + object? objectValue = value; + + if (!state.SupportContinuation) + { + if (objectValue == null) + { + writer.WriteNullValue(); + return true; + } + + writer.WriteStartObject(); + + if (options.ReferenceHandling.ShouldWritePreservedReferences()) + { + if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref) + { + return true; + } + } + + JsonPropertyInfo? dataExtensionProperty = state.Current.JsonClassInfo.DataExtensionProperty; + + int propertyCount = 0; + JsonPropertyInfo[]? propertyCacheArray = state.Current.JsonClassInfo.PropertyCacheArray; + if (propertyCacheArray != null) + { + propertyCount = propertyCacheArray.Length; + } + + for (int i = 0; i < propertyCount; i++) + { + JsonPropertyInfo jsonPropertyInfo = propertyCacheArray![i]; + + // Remember the current property for JsonPath support if an exception is thrown. + state.Current.DeclaredJsonPropertyInfo = jsonPropertyInfo; + + if (jsonPropertyInfo.ShouldSerialize) + { + if (jsonPropertyInfo == dataExtensionProperty) + { + if (!jsonPropertyInfo.GetMemberAndWriteJsonExtensionData(objectValue, ref state, writer)) + { + return false; + } + } + else + { + if (!jsonPropertyInfo.GetMemberAndWriteJson(objectValue, ref state, writer)) + { + Debug.Assert(jsonPropertyInfo.ConverterBase.ClassType != ClassType.Value); + return false; + } + } + } + + state.Current.EndProperty(); + } + + writer.WriteEndObject(); + return true; + } + else + { + if (!state.Current.ProcessedStartToken) + { + if (objectValue == null) + { + writer.WriteNullValue(); + return true; + } + + writer.WriteStartObject(); + + if (options.ReferenceHandling.ShouldWritePreservedReferences()) + { + if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref) + { + return true; + } + } + + state.Current.ProcessedStartToken = true; + } + + JsonPropertyInfo? dataExtensionProperty = state.Current.JsonClassInfo.DataExtensionProperty; + + int propertyCount = 0; + JsonPropertyInfo[]? propertyCacheArray = state.Current.JsonClassInfo.PropertyCacheArray; + if (propertyCacheArray != null) + { + propertyCount = propertyCacheArray.Length; + } + + while (propertyCount > state.Current.EnumeratorIndex) + { + JsonPropertyInfo jsonPropertyInfo = propertyCacheArray![state.Current.EnumeratorIndex]; + state.Current.DeclaredJsonPropertyInfo = jsonPropertyInfo; + + if (jsonPropertyInfo.ShouldSerialize) + { + if (jsonPropertyInfo == dataExtensionProperty) + { + if (!jsonPropertyInfo.GetMemberAndWriteJsonExtensionData(objectValue!, ref state, writer)) + { + return false; + } + } + else + { + if (!jsonPropertyInfo.GetMemberAndWriteJson(objectValue!, ref state, writer)) + { + Debug.Assert(jsonPropertyInfo.ConverterBase.ClassType != ClassType.Value); + return false; + } + } + } + + state.Current.EndProperty(); + state.Current.EnumeratorIndex++; + + if (ShouldFlush(writer, ref state)) + { + return false; + } + } + + if (!state.Current.ProcessedEndToken) + { + state.Current.ProcessedEndToken = true; + writer.WriteEndObject(); + } + + return true; + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonQueueOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonQueueOfTConverter.cs new file mode 100644 index 00000000000000..a0cc38d40abb21 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonQueueOfTConverter.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class JsonQueueOfTConverter : JsonIEnumerableDefaultConverter + where TCollection : Queue + { + protected override void Add(TElement value, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is TCollection); + ((TCollection)state.Current.ReturnValue!).Enqueue(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + if (state.Current.JsonClassInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(state.Current.JsonClassInfo.Type); + } + + state.Current.ReturnValue = state.Current.JsonClassInfo.CreateObject(); + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); + enumerator = (IEnumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetElementConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + TElement element = enumerator.Current; + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + } while (enumerator.MoveNext()); + + return true; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonStackOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonStackOfTConverter.cs new file mode 100644 index 00000000000000..31dac18efe1a2a --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonStackOfTConverter.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class JsonStackOfTConverter : JsonIEnumerableDefaultConverter + where TCollection : Stack + { + protected override void Add(TElement value, ref ReadStack state) + { + Debug.Assert(state.Current.ReturnValue is TCollection); + ((TCollection)state.Current.ReturnValue!).Push(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + if (state.Current.JsonClassInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(state.Current.JsonClassInfo.Type); + } + + state.Current.ReturnValue = state.Current.JsonClassInfo.CreateObject(); + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return true; + } + } + else + { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); + enumerator = (IEnumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetElementConverter(ref state); + do + { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + + TElement element = enumerator.Current; + if (!converter.TryWrite(writer, element, options, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + } while (enumerator.MoveNext()); + + return true; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterKeyValuePair.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterKeyValuePair.cs index a5ea1db7e23d63..70eb76e1102f90 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterKeyValuePair.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterKeyValuePair.cs @@ -3,22 +3,26 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; namespace System.Text.Json.Serialization.Converters { - internal sealed class JsonKeyValuePairConverter : JsonConverter> + internal sealed class JsonKeyValuePairConverter : JsonValueConverter> { private const string KeyName = "Key"; private const string ValueName = "Value"; - // "encoder: null" is used since the literal values of "Key" and "Value" should not normally be escaped - // unless a custom encoder is used that escapes these ASCII characters (rare). - // Also by not specifying an encoder allows the values to be cached statically here. + // todo: move these to JsonSerializerOptions and use the proper encoding. private static readonly JsonEncodedText _keyName = JsonEncodedText.Encode(KeyName, encoder: null); private static readonly JsonEncodedText _valueName = JsonEncodedText.Encode(ValueName, encoder: null); - public override KeyValuePair Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + // todo: it is possible to cache the underlying converters since this is an internal converter and + // an instance is created only once for each JsonSerializerOptions instance. + + internal override bool OnTryRead( + ref Utf8JsonReader reader, + Type typeToConvert, JsonSerializerOptions options, + ref ReadStack state, + out KeyValuePair value) { if (reader.TokenType != JsonTokenType.StartObject) { @@ -32,7 +36,7 @@ public override KeyValuePair Read(ref Utf8JsonReader reader, Type bool valueSet = false; // Get the first property. - reader.Read(); + reader.ReadWithVerify(); if (reader.TokenType != JsonTokenType.PropertyName) { ThrowHelper.ThrowJsonException(); @@ -41,12 +45,14 @@ public override KeyValuePair Read(ref Utf8JsonReader reader, Type string propertyName = reader.GetString()!; if (propertyName == KeyName) { - k = ReadProperty(ref reader, typeToConvert, options); + reader.ReadWithVerify(); + k = JsonSerializer.Deserialize(ref reader, options, ref state, KeyName); keySet = true; } else if (propertyName == ValueName) { - v = ReadProperty(ref reader, typeToConvert, options); + reader.ReadWithVerify(); + v = JsonSerializer.Deserialize(ref reader, options, ref state, ValueName); valueSet = true; } else @@ -55,22 +61,24 @@ public override KeyValuePair Read(ref Utf8JsonReader reader, Type } // Get the second property. - reader.Read(); + reader.ReadWithVerify(); if (reader.TokenType != JsonTokenType.PropertyName) { ThrowHelper.ThrowJsonException(); } propertyName = reader.GetString()!; - if (propertyName == ValueName) + if (propertyName == KeyName) { - v = ReadProperty(ref reader, typeToConvert, options); - valueSet = true; + reader.ReadWithVerify(); + k = JsonSerializer.Deserialize(ref reader, options, ref state, KeyName); + keySet = true; } - else if (propertyName == KeyName) + else if (propertyName == ValueName) { - k = ReadProperty(ref reader, typeToConvert, options); - keySet = true; + reader.ReadWithVerify(); + v = JsonSerializer.Deserialize(ref reader, options, ref state, ValueName); + valueSet = true; } else { @@ -82,59 +90,29 @@ public override KeyValuePair Read(ref Utf8JsonReader reader, Type ThrowHelper.ThrowJsonException(); } - reader.Read(); + reader.ReadWithVerify(); if (reader.TokenType != JsonTokenType.EndObject) { ThrowHelper.ThrowJsonException(); } - return new KeyValuePair(k, v); - } - - private T ReadProperty(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - T k; - - // Attempt to use existing converter first before re-entering through JsonSerializer.Deserialize(). - // The default converter for objects does not parse null objects as null, so it is not used here. - if (typeToConvert != typeof(object) && (options?.GetConverter(typeToConvert) is JsonConverter keyConverter)) - { - reader.Read(); - k = keyConverter.Read(ref reader, typeToConvert, options); - } - else - { - k = JsonSerializer.Deserialize(ref reader, options); - } - - return k!; + value = new KeyValuePair(k, v); + return true; } - private void WriteProperty(Utf8JsonWriter writer, T value, JsonEncodedText name, JsonSerializerOptions? options) + internal override bool OnTryWrite(Utf8JsonWriter writer, KeyValuePair value, JsonSerializerOptions options, ref WriteStack state) { - Type typeToConvert = typeof(T); + writer.WriteStartObject(); - writer.WritePropertyName(name); + writer.WritePropertyName(_keyName); + JsonSerializer.Serialize(writer, value.Key, options, ref state, KeyName); - // Attempt to use existing converter first before re-entering through JsonSerializer.Serialize(). - // The default converter for object does not support writing. - if (typeToConvert != typeof(object) && (options?.GetConverter(typeToConvert) is JsonConverter keyConverter)) - { - keyConverter.Write(writer, value, options); - } - else - { - JsonSerializer.Serialize(writer, value, options); - } - } + writer.WritePropertyName(_valueName); + JsonSerializer.Serialize(writer, value.Value, options, ref state, ValueName); - public override void Write(Utf8JsonWriter writer, KeyValuePair value, JsonSerializerOptions? options) - { - writer.WriteStartObject(); - WriteProperty(writer, value.Key, _keyName, options); - WriteProperty(writer, value.Value, _valueName, options); writer.WriteEndObject(); + return true; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterNullable.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterNullable.cs new file mode 100644 index 00000000000000..525793d5e0565f --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterNullable.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Text.Json.Serialization +{ + internal class JsonValueConverterNullable : JsonConverter where T : struct + { + // It is possible to cache the underlying converter since this is an internal converter and + // an instance is created only once for each JsonSerializerOptions instance. + private readonly JsonConverter _converter; + + public JsonValueConverterNullable(JsonConverter converter) + { + _converter = converter; + } + + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + T value = _converter.Read(ref reader, typeof(T), options); + return value; + } + + public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) + { + if (!value.HasValue) + { + writer.WriteNullValue(); + } + else + { + _converter.Write(writer, value.Value, options); + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterNullableFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterNullableFactory.cs new file mode 100644 index 00000000000000..742b1843f9f74a --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterNullableFactory.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Reflection; + +namespace System.Text.Json.Serialization +{ + internal class JsonValueConverterNullableFactory : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) + { + return Nullable.GetUnderlyingType(typeToConvert) != null; + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Debug.Assert(typeToConvert.GetGenericArguments().Length > 0); + + Type valueTypeToConvert = typeToConvert.GetGenericArguments()[0]; + + JsonConverter? valueConverter = options.GetConverter(valueTypeToConvert); + if (valueConverter == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(valueTypeToConvert); + } + + JsonConverter converter = (JsonConverter)Activator.CreateInstance( + typeof(JsonValueConverterNullable<>).MakeGenericType(valueTypeToConvert), + BindingFlags.Instance | BindingFlags.Public, + binder: null, + args: new object[] { valueConverter }, + culture: null)!; + + return converter; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultIDictionaryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultIDictionaryConverter.cs deleted file mode 100644 index 720562f5482e47..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultIDictionaryConverter.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections; -using System.Text.Json.Serialization.Policies; - -namespace System.Text.Json.Serialization.Converters -{ - internal sealed class DefaultIDictionaryConverter : JsonDictionaryConverter - { - public override IDictionary CreateFromDictionary(ref ReadStack state, IDictionary sourceDictionary, JsonSerializerOptions options) - { - Type enumerableType = state.Current.JsonPropertyInfo.RuntimePropertyType; - return (IDictionary)Activator.CreateInstance(enumerableType, sourceDictionary); - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultReferenceResolver.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultReferenceResolver.cs index 4f18e6ad0e2709..c6a5c3dd09315c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultReferenceResolver.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultReferenceResolver.cs @@ -36,19 +36,16 @@ public DefaultReferenceResolver(bool writing) } } - /// /// Adds an entry to the bag of references using the specified id and value. /// This method gets called when an $id metadata property from a JSON object is read. /// /// The identifier of the respective JSON object or array. /// The value of the respective CLR reference type object that results from parsing the JSON object. - public void AddReferenceOnDeserialize(string referenceId, object value) + /// True if the value was successfully added, false otherwise. + public bool AddReferenceOnDeserialize(string referenceId, object value) { - if (!JsonHelpers.TryAdd(_referenceIdToObjectMap!, referenceId, value)) - { - ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(referenceId); - } + return JsonHelpers.TryAdd(_referenceIdToObjectMap!, referenceId, value); } /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ExtensionDataWriteStatus.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ExtensionDataWriteStatus.cs deleted file mode 100644 index f18b11134aef28..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ExtensionDataWriteStatus.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace System.Text.Json.Serialization -{ - internal enum ExtensionDataWriteStatus : byte - { - NotStarted = 0, - Writing = 1, - Finished = 2 - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ExtensionMethods.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ExtensionMethods.cs new file mode 100644 index 00000000000000..703ba02be213de --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ExtensionMethods.cs @@ -0,0 +1,267 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers; +using System.Diagnostics; +using System.Reflection; + +namespace System.Text.Json.Serialization +{ + internal static class ExtensionMethods + { + private const string ImmutableArrayTypeName = "System.Collections.Immutable.ImmutableArray"; + private const string ImmutableArrayGenericTypeName = "System.Collections.Immutable.ImmutableArray`1"; + + private const string ImmutableListTypeName = "System.Collections.Immutable.ImmutableList"; + private const string ImmutableListGenericTypeName = "System.Collections.Immutable.ImmutableList`1"; + private const string ImmutableListGenericInterfaceTypeName = "System.Collections.Immutable.IImmutableList`1"; + + private const string ImmutableStackTypeName = "System.Collections.Immutable.ImmutableStack"; + private const string ImmutableStackGenericTypeName = "System.Collections.Immutable.ImmutableStack`1"; + private const string ImmutableStackGenericInterfaceTypeName = "System.Collections.Immutable.IImmutableStack`1"; + + private const string ImmutableQueueTypeName = "System.Collections.Immutable.ImmutableQueue"; + private const string ImmutableQueueGenericTypeName = "System.Collections.Immutable.ImmutableQueue`1"; + private const string ImmutableQueueGenericInterfaceTypeName = "System.Collections.Immutable.IImmutableQueue`1"; + + private const string ImmutableSortedSetTypeName = "System.Collections.Immutable.ImmutableSortedSet"; + private const string ImmutableSortedSetGenericTypeName = "System.Collections.Immutable.ImmutableSortedSet`1"; + + private const string ImmutableHashSetTypeName = "System.Collections.Immutable.ImmutableHashSet"; + private const string ImmutableHashSetGenericTypeName = "System.Collections.Immutable.ImmutableHashSet`1"; + private const string ImmutableSetGenericInterfaceTypeName = "System.Collections.Immutable.IImmutableSet`1"; + + private const string ImmutableDictionaryTypeName = "System.Collections.Immutable.ImmutableDictionary"; + private const string ImmutableDictionaryGenericTypeName = "System.Collections.Immutable.ImmutableDictionary`2"; + private const string ImmutableDictionaryGenericInterfaceTypeName = "System.Collections.Immutable.IImmutableDictionary`2"; + + private const string ImmutableSortedDictionaryTypeName = "System.Collections.Immutable.ImmutableSortedDictionary"; + private const string ImmutableSortedDictionaryGenericTypeName = "System.Collections.Immutable.ImmutableSortedDictionary`2"; + + internal static Type? GetCompatibleGenericBaseClass(this Type type, Type baseType) + { + Debug.Assert(baseType.IsGenericType); + Debug.Assert(!baseType.IsInterface); + Debug.Assert(baseType == baseType.GetGenericTypeDefinition()); + + Type? baseTypeToCheck = type; + + while (baseTypeToCheck != null && baseTypeToCheck != typeof(object)) + { + if (baseTypeToCheck.IsGenericType) + { + Type genericTypeToCheck = baseTypeToCheck.GetGenericTypeDefinition(); + if (genericTypeToCheck == baseType) + { + return baseTypeToCheck; + } + } + + baseTypeToCheck = baseTypeToCheck.BaseType; + } + + return null; + } + + internal static Type? GetCompatibleGenericInterface(this Type type, Type interfaceType) + { + Debug.Assert(interfaceType.IsGenericType); + Debug.Assert(interfaceType.IsInterface); + Debug.Assert(interfaceType == interfaceType.GetGenericTypeDefinition()); + + Type interfaceToCheck = type; + + if (interfaceToCheck.IsGenericType) + { + interfaceToCheck = interfaceToCheck.GetGenericTypeDefinition(); + } + + if (interfaceToCheck == interfaceType) + { + return type; + } + + foreach (Type typeToCheck in type.GetInterfaces()) + { + if (typeToCheck.IsGenericType) + { + Type genericInterfaceToCheck = typeToCheck.GetGenericTypeDefinition(); + if (genericInterfaceToCheck == interfaceType) + { + return typeToCheck; + } + } + } + + return null; + } + + public static bool IsImmutableDictionaryType(this Type type) + { + if (!type.IsGenericType || !type.Assembly.FullName!.StartsWith("System.Collections.Immutable,")) + { + return false; + } + + switch (type.GetGenericTypeDefinition().FullName) + { + case ImmutableDictionaryGenericTypeName: + case ImmutableDictionaryGenericInterfaceTypeName: + case ImmutableSortedDictionaryGenericTypeName: + return true; + default: + return false; + } + } + + public static bool IsImmutableEnumerableType(this Type type) + { + if (!type.IsGenericType|| !type.Assembly.FullName!.StartsWith("System.Collections.Immutable,")) + { + return false; + } + + switch (type.GetGenericTypeDefinition().FullName) + { + case ImmutableArrayGenericTypeName: + case ImmutableListGenericTypeName: + case ImmutableListGenericInterfaceTypeName: + case ImmutableStackGenericTypeName: + case ImmutableStackGenericInterfaceTypeName: + case ImmutableQueueGenericTypeName: + case ImmutableQueueGenericInterfaceTypeName: + case ImmutableSortedSetGenericTypeName: + case ImmutableHashSetGenericTypeName: + case ImmutableSetGenericInterfaceTypeName: + return true; + default: + return false; + } + } + + public static MethodInfo GetImmutableEnumerableCreateRangeMethod(this Type type, Type elementType) + { + Type constructingType = GetImmutableEnumerableConstructingType(type); + + MethodInfo[] constructingTypeMethods = constructingType.GetMethods(); + foreach (MethodInfo method in constructingTypeMethods) + { + if (method.Name == "CreateRange" + && method.GetParameters().Length == 1 + && method.IsGenericMethod + && method.GetGenericArguments().Length == 1) + { + return method.MakeGenericMethod(elementType); + } + } + + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(type); + return null!; + } + + public static MethodInfo GetImmutableDictionaryCreateRangeMethod(this Type type, Type elementType) + { + Type constructingType = GetImmutableDictionaryConstructingType(type); + + MethodInfo[] constructingTypeMethods = constructingType.GetMethods(); + foreach (MethodInfo method in constructingTypeMethods) + { + if (method.Name == "CreateRange" + && method.GetParameters().Length == 1 + && method.IsGenericMethod + && method.GetGenericArguments().Length == 2) + { + return method.MakeGenericMethod(typeof(string), elementType); + } + } + + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(type); + return null!; + } + + private static Type GetImmutableEnumerableConstructingType(Type type) + { + Debug.Assert(type.IsImmutableEnumerableType()); + + // Use the generic type definition of the immutable collection to determine + // an appropriate constructing type, i.e. a type that we can invoke the + // `CreateRange` method on, which returns the desired immutable collection. + Type underlyingType = type.GetGenericTypeDefinition(); + string constructingTypeName; + + switch (underlyingType.FullName) + { + case ImmutableArrayGenericTypeName: + constructingTypeName = ImmutableArrayTypeName; + break; + case ImmutableListGenericTypeName: + case ImmutableListGenericInterfaceTypeName: + constructingTypeName = ImmutableListTypeName; + break; + case ImmutableStackGenericTypeName: + case ImmutableStackGenericInterfaceTypeName: + constructingTypeName = ImmutableStackTypeName; + break; + case ImmutableQueueGenericTypeName: + case ImmutableQueueGenericInterfaceTypeName: + constructingTypeName = ImmutableQueueTypeName; + break; + case ImmutableSortedSetGenericTypeName: + constructingTypeName = ImmutableSortedSetTypeName; + break; + case ImmutableHashSetGenericTypeName: + case ImmutableSetGenericInterfaceTypeName: + constructingTypeName = ImmutableHashSetTypeName; + break; + default: + // We verified that the type is an immutable collection, so the + // generic definition is one of the above. + return null!; + } + + // This won't be null because we verified the assembly is actually System.Collections.Immutable. + return underlyingType.Assembly.GetType(constructingTypeName)!; + } + + private static Type GetImmutableDictionaryConstructingType(Type type) + { + Debug.Assert(type.IsImmutableDictionaryType()); + + // Use the generic type definition of the immutable collection to determine + // an appropriate constructing type, i.e. a type that we can invoke the + // `CreateRange` method on, which returns the desired immutable collection. + Type underlyingType = type.GetGenericTypeDefinition(); + string constructingTypeName; + + switch (underlyingType.FullName) + { + case ImmutableDictionaryGenericTypeName: + case ImmutableDictionaryGenericInterfaceTypeName: + constructingTypeName = ImmutableDictionaryTypeName; + break; + case ImmutableSortedDictionaryGenericTypeName: + constructingTypeName = ImmutableSortedDictionaryTypeName; + break; + default: + // We verified that the type is an immutable collection, so the + // generic definition is one of the above. + return null!; + } + + // This won't be null because we verified the assembly is actually System.Collections.Immutable. + return underlyingType.Assembly.GetType(constructingTypeName)!; + } + + public static bool IsNonGenericStackOrQueue(this Type type) + { + Type? typeOfStack = Type.GetType("System.Collections.Stack, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); + Type? typeOfQueue = Type.GetType("System.Collections.Queue, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); + + Debug.Assert(typeOfStack != null); + Debug.Assert(typeOfQueue != null); + + return typeOfStack.IsAssignableFrom(type) || typeOfQueue.IsAssignableFrom(type); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ImmutableCollectionCreator.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ImmutableCollectionCreator.cs deleted file mode 100644 index 8c093f0f54fed0..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ImmutableCollectionCreator.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - -namespace System.Text.Json -{ - internal abstract class ImmutableCollectionCreator - { - public abstract void RegisterCreatorDelegateFromMethod(MethodInfo creator); - public abstract bool CreateImmutableEnumerable(IList items, [NotNullWhen(true)] out IEnumerable? collection); - public abstract bool CreateImmutableDictionary(IDictionary items, [NotNullWhen(true)] out IDictionary? collection); - } - - internal sealed class ImmutableEnumerableCreator : ImmutableCollectionCreator - where TCollection : IEnumerable - { - private Func, TCollection>? _creatorDelegate; - - public override void RegisterCreatorDelegateFromMethod(MethodInfo creator) - { - Debug.Assert(_creatorDelegate == null); - _creatorDelegate = (Func, TCollection>)creator.CreateDelegate(typeof(Func, TCollection>)); - } - - public override bool CreateImmutableEnumerable(IList items, [NotNullWhen(true)] out IEnumerable collection) - { - Debug.Assert(_creatorDelegate != null); - collection = _creatorDelegate(CreateGenericTElementIEnumerable(items)); - return true; - } - - public override bool CreateImmutableDictionary(IDictionary items, [NotNullWhen(true)] out IDictionary? collection) - { - // Shouldn't be calling this method for immutable dictionaries. - collection = default; - return false; - } - - private IEnumerable CreateGenericTElementIEnumerable(IList sourceList) - { - foreach (object? item in sourceList) - { - yield return (TElement)item!; - } - } - } - - internal sealed class ImmutableDictionaryCreator : ImmutableCollectionCreator - where TCollection : IReadOnlyDictionary - { - private Func>, TCollection>? _creatorDelegate; - - public override void RegisterCreatorDelegateFromMethod(MethodInfo creator) - { - Debug.Assert(_creatorDelegate == null); - _creatorDelegate = (Func>, TCollection>)creator.CreateDelegate( - typeof(Func>, TCollection>)); - } - - public override bool CreateImmutableEnumerable(IList items, [NotNullWhen(true)] out IEnumerable? collection) - { - // Shouldn't be calling this method for immutable non-dictionaries. - collection = default; - return false; - } - - public override bool CreateImmutableDictionary(IDictionary items, [NotNullWhen(true)] out IDictionary collection) - { - Debug.Assert(_creatorDelegate != null); - collection = (IDictionary)_creatorDelegate(CreateGenericTElementIDictionary(items)); - return true; - } - - private IEnumerable> CreateGenericTElementIDictionary(IDictionary sourceDictionary) - { - foreach (DictionaryEntry? item in sourceDictionary) - { - if (item.HasValue) - { - yield return new KeyValuePair((string)item.Value.Key, (TElement)item.Value.Value!); - } - } - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs index fb91bafd4e78ea..87d2a2e981c4cc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs @@ -2,12 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.Concurrent; -using System.Diagnostics; using System.Reflection; -using System.Runtime.CompilerServices; using System.Text.Json.Serialization; -using System.Threading; namespace System.Text.Json { @@ -21,89 +17,42 @@ private JsonPropertyInfo AddProperty(Type propertyType, PropertyInfo propertyInf return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(propertyInfo, options); } + JsonConverter? converter; ClassType classType = GetClassType( propertyType, parentClassType, propertyInfo, - out Type runtimeType, - out Type? elementType, - out Type? nullableUnderlyingType, - out _, - out JsonConverter? converter, - checkForAddMethod: false, + out Type? runtimeType, + out Type? _, + out converter, options); + if (converter == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(propertyType, parentClassType, propertyInfo); + } + return CreateProperty( declaredPropertyType: propertyType, runtimePropertyType: runtimeType, propertyInfo, parentClassType, - collectionElementType: elementType, - nullableUnderlyingType, converter, classType, options); } - [PreserveDependency(".ctor()", "System.Text.Json.JsonPropertyInfoNullable`2")] - [PreserveDependency(".ctor()", "System.Text.Json.Serialization.JsonPropertyInfoNotNullableContravariant`4")] internal static JsonPropertyInfo CreateProperty( Type declaredPropertyType, - Type runtimePropertyType, + Type? runtimePropertyType, PropertyInfo? propertyInfo, Type parentClassType, - Type? collectionElementType, - Type? nullableUnderlyingType, - JsonConverter? converter, + JsonConverter converter, ClassType classType, JsonSerializerOptions options) { - bool treatAsNullable = nullableUnderlyingType != null; - - // Obtain the type of the JsonPropertyInfo class to construct. - Type propertyInfoClassType; - - if (treatAsNullable && converter != null) - { - propertyInfoClassType = typeof(JsonPropertyInfoNullable<,>).MakeGenericType(parentClassType, nullableUnderlyingType!); - } - else - { - Type? typeToConvert = converter?.TypeToConvert; - if (typeToConvert == null) - { - typeToConvert = declaredPropertyType; - } - - // For the covariant case, create JsonPropertyInfoNotNullable. The generic constraints are "where TConverter : TDeclaredProperty". - if (runtimePropertyType.IsAssignableFrom(typeToConvert)) - { - propertyInfoClassType = typeof(JsonPropertyInfoNotNullable<,,,>).MakeGenericType( - parentClassType, - declaredPropertyType, - runtimePropertyType, - typeToConvert); - } - else - { - Debug.Assert(typeToConvert.IsAssignableFrom(runtimePropertyType)); - - // For the contravariant case, create JsonPropertyInfoNotNullableContravariant. The generic constraints are "where TDeclaredProperty : TConverter". - propertyInfoClassType = typeof(JsonPropertyInfoNotNullableContravariant<,,,>).MakeGenericType( - parentClassType, - declaredPropertyType, - runtimePropertyType, - typeToConvert); - } - } - // Create the JsonPropertyInfo instance. - JsonPropertyInfo jsonPropertyInfo = (JsonPropertyInfo)Activator.CreateInstance( - propertyInfoClassType, - BindingFlags.Instance | BindingFlags.Public, - binder: null, - args: null, - culture: null)!; + JsonPropertyInfo jsonPropertyInfo = converter.CreateJsonPropertyInfo(); jsonPropertyInfo.Initialize( parentClassType, @@ -111,9 +60,7 @@ internal static JsonPropertyInfo CreateProperty( runtimePropertyType, runtimeClassType: classType, propertyInfo, - collectionElementType, converter, - treatAsNullable, options); return jsonPropertyInfo; @@ -127,10 +74,8 @@ internal static JsonPropertyInfo CreateProperty( /// internal static JsonPropertyInfo CreatePolicyProperty( Type declaredPropertyType, - Type runtimePropertyType, - Type? elementType, - Type? nullableUnderlyingType, - JsonConverter? converter, + Type? runtimePropertyType, + JsonConverter converter, ClassType classType, JsonSerializerOptions options) { @@ -139,70 +84,9 @@ internal static JsonPropertyInfo CreatePolicyProperty( runtimePropertyType: runtimePropertyType, propertyInfo: null, // Not a real property so this is null. parentClassType: typeof(object), // a dummy value (not used) - collectionElementType : elementType, - nullableUnderlyingType, converter : converter, classType : classType, options); } - - /// - /// Create a for a given Type. - /// - internal JsonPropertyInfo CreateRootProperty(JsonSerializerOptions options) - { - JsonConverter? converter = options.DetermineConverterForProperty(Type, Type, propertyInfo: null); - - return CreateProperty( - declaredPropertyType: Type, - runtimePropertyType: Type, - propertyInfo: null, - parentClassType: typeof(object), // a dummy value (not used) - ElementType, - Nullable.GetUnderlyingType(Type), - converter, - ClassType, - options); - } - - internal JsonPropertyInfo GetOrAddPolymorphicProperty(JsonPropertyInfo property, Type runtimePropertyType, JsonSerializerOptions options) - { - static JsonPropertyInfo CreateRuntimeProperty((JsonPropertyInfo property, Type runtimePropertyType) key, (JsonSerializerOptions options, Type classType) arg) - { - ClassType classType = GetClassType( - key.runtimePropertyType, - arg.classType, - key.property.PropertyInfo, - out _, - out Type? elementType, - out Type? nullableType, - out _, - out JsonConverter? converter, - checkForAddMethod: false, - arg.options); - - JsonPropertyInfo runtimeProperty = CreateProperty( - key.property.DeclaredPropertyType, - key.runtimePropertyType, - key.property.PropertyInfo, - parentClassType: arg.classType, - collectionElementType: elementType, - nullableType, - converter, - classType, - options: arg.options); - key.property.CopyRuntimeSettingsTo(runtimeProperty); - - return runtimeProperty; - } - - ConcurrentDictionary<(JsonPropertyInfo, Type), JsonPropertyInfo> cache = - LazyInitializer.EnsureInitialized(ref RuntimePropertyCache, () => new ConcurrentDictionary<(JsonPropertyInfo, Type), JsonPropertyInfo>()); -#if BUILDING_INBOX_LIBRARY - return cache.GetOrAdd((property, runtimePropertyType), (key, arg) => CreateRuntimeProperty(key, arg), (options, Type)); -#else - return cache.GetOrAdd((property, runtimePropertyType), key => CreateRuntimeProperty(key, (options, Type))); -#endif - } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs index e6fc4cc6cce7f8..a6f6961c523241 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -28,9 +27,6 @@ internal sealed partial class JsonClassInfo // All of the serializable properties on a POCO (except the optional extension property) keyed on property name. public volatile Dictionary? PropertyCache; - // Serializable runtime/polymorphic properties, keyed on property and runtime type. - public ConcurrentDictionary<(JsonPropertyInfo, Type), JsonPropertyInfo>? RuntimePropertyCache; - // All of the serializable properties on a POCO including the optional extension property. // Used for performance during serialization instead of 'PropertyCache' above. public volatile JsonPropertyInfo[]? PropertyCacheArray; @@ -123,15 +119,11 @@ public JsonClassInfo(Type type, JsonSerializerOptions options) type, parentClassType: type, propertyInfo: null, - out Type runtimeType, + out Type? runtimeType, out Type? elementType, - out Type? nullableUnderlyingType, - out MethodInfo? addMethod, out JsonConverter? converter, - checkForAddMethod: true, options); - // Ignore properties on enumerable. switch (ClassType) { case ClassType.Object: @@ -196,29 +188,29 @@ public JsonClassInfo(Type type, JsonSerializerOptions options) PropertyCache = cache; cache.Values.CopyTo(cacheArray, 0); PropertyCacheArray = cacheArray; + + // Create the policy property. + PolicyProperty = CreatePolicyProperty(type, runtimeType, converter!, ClassType, options); } break; case ClassType.Enumerable: case ClassType.Dictionary: { ElementType = elementType; - AddItemToObject = addMethod; - PolicyProperty = CreatePolicyProperty(type, runtimeType, elementType, nullableUnderlyingType, converter: null, ClassType, options); - CreateObject = options.MemberAccessorStrategy.CreateConstructor(PolicyProperty.RuntimePropertyType); + PolicyProperty = CreatePolicyProperty(type, runtimeType, converter!, ClassType, options); + CreateObject = options.MemberAccessorStrategy.CreateConstructor(PolicyProperty.RuntimePropertyType!); } break; case ClassType.Value: + case ClassType.NewValue: { CreateObject = options.MemberAccessorStrategy.CreateConstructor(type); - PolicyProperty = CreatePolicyProperty(type, runtimeType, elementType: null, nullableUnderlyingType, converter, ClassType, options); + PolicyProperty = CreatePolicyProperty(type, runtimeType, converter!, ClassType, options); } break; - case ClassType.Unknown: + case ClassType.Invalid: { - CreateObject = options.MemberAccessorStrategy.CreateConstructor(type); - PolicyProperty = CreatePolicyProperty(type, runtimeType, elementType: null, nullableUnderlyingType, converter, ClassType, options); - PropertyCache = new Dictionary(); - PropertyCacheArray = Array.Empty(); + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(type); } break; default: @@ -233,13 +225,22 @@ private bool DetermineExtensionDataProperty(Dictionary if (jsonPropertyInfo != null) { Type declaredPropertyType = jsonPropertyInfo.DeclaredPropertyType; - if (!typeof(IDictionary).IsAssignableFrom(declaredPropertyType) && - !typeof(IDictionary).IsAssignableFrom(declaredPropertyType)) + if (typeof(IDictionary).IsAssignableFrom(declaredPropertyType) || + typeof(IDictionary).IsAssignableFrom(declaredPropertyType)) + { + JsonConverter? converter = Options.GetConverter(declaredPropertyType); + if (converter == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(declaredPropertyType); + } + } + else { ThrowHelper.ThrowInvalidOperationException_SerializationDataExtensionPropertyInvalid(this, jsonPropertyInfo); } DataExtensionProperty = jsonPropertyInfo; + return true; } @@ -400,8 +401,6 @@ private Dictionary CreatePropertyCache(int capacity) public JsonPropertyInfo? PolicyProperty { get; private set; } - public MethodInfo? AddItemToObject { get; private set; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryIsPropertyRefEqual(in PropertyRef propertyRef, ReadOnlySpan propertyName, ulong key, [NotNullWhen(true)] ref JsonPropertyInfo? info) { @@ -423,6 +422,8 @@ private static bool TryIsPropertyRefEqual(in PropertyRef propertyRef, ReadOnlySp /// Get a key from the property name. /// The key consists of the first 7 bytes of the property name and then the length. /// + // AggressiveInlining used since this method is only called from two locations and is on a hot path. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong GetKey(ReadOnlySpan propertyName) { const int BitsInByte = 8; @@ -514,206 +515,68 @@ public static ulong GetKey(ReadOnlySpan propertyName) // - class type, // - runtime type, // - element type (if the type is a collection), - // - the underlying type (if the type is nullable type e.g. int?), - // - the "add" method (if the type is a non-dictionary collection which doesn't implement IList - // e.g. typeof(Stack), where we retrieve the void Push(string) method), and // - the converter (either native or custom), if one exists. public static ClassType GetClassType( Type type, Type parentClassType, PropertyInfo? propertyInfo, - out Type runtimeType, + out Type? runtimeType, out Type? elementType, - out Type? nullableUnderlyingType, - out MethodInfo? addMethod, out JsonConverter? converter, - bool checkForAddMethod, JsonSerializerOptions options) { Debug.Assert(type != null); - runtimeType = type; - - nullableUnderlyingType = Nullable.GetUnderlyingType(type); - - // Type is nullable e.g. typeof(int?). - if (nullableUnderlyingType != null) - { - // Check if there's a converter for this nullable type, e.g. do we have a converter that implements - // JsonConverter if the type is typeof(int?)? - converter = options.DetermineConverterForProperty(parentClassType, type, propertyInfo); - - if (converter == null) - { - // No converter. We'll check below if there's a converter for the non-nullable type e.g. - // one that implements JsonConverter, given the type is typeof(int?). - type = nullableUnderlyingType; - } - else - { - elementType = default; - addMethod = default; - // Don't treat the type as a Nullable when creating the property info later on, since we have a converter for it. - nullableUnderlyingType = default; - return ClassType.Value; - } - } - - converter = options.DetermineConverterForProperty(parentClassType, type, propertyInfo); - - if (converter != null) - { - elementType = default; - addMethod = default; - return type == typeof(object) ? ClassType.Unknown : ClassType.Value; - } - - runtimeType = type; - - if (!(typeof(IEnumerable)).IsAssignableFrom(type)) + converter = options.DetermineConverter(parentClassType, type, propertyInfo); + if (converter == null) { + runtimeType = null; elementType = null; - addMethod = default; - return ClassType.Object; - } - - if (type.IsArray) - { - elementType = type.GetElementType(); - addMethod = default; - return ClassType.Enumerable; - } - - if (type.FullName != null) - { - if (type.FullName.StartsWith("System.Collections.Generic.IEnumerable`1")) - { - elementType = type.GetGenericArguments()[0]; - runtimeType = typeof(List<>).MakeGenericType(elementType); - addMethod = default; - return ClassType.Enumerable; - } - else if (type.FullName.StartsWith("System.Collections.Generic.IDictionary`2") || - type.FullName.StartsWith("System.Collections.Generic.IReadOnlyDictionary`2")) - { - Type[] genericTypes = type.GetGenericArguments(); - - elementType = genericTypes[1]; - runtimeType = typeof(Dictionary<,>).MakeGenericType(genericTypes[0], elementType); - addMethod = default; - return ClassType.Dictionary; - } - } - - - { - Type? genericIDictionaryType = type.GetInterface("System.Collections.Generic.IDictionary`2") ?? type.GetInterface("System.Collections.Generic.IReadOnlyDictionary`2"); - if (genericIDictionaryType != null) - { - Type[] genericTypes = genericIDictionaryType.GetGenericArguments(); - elementType = genericTypes[1]; - addMethod = default; - - if (type.IsInterface) - { - Type concreteDictionaryType = typeof(Dictionary<,>).MakeGenericType(genericTypes[0], genericTypes[1]); - - if (type.IsAssignableFrom(concreteDictionaryType)) - { - runtimeType = concreteDictionaryType; - } - } - - return ClassType.Dictionary; - } + return ClassType.Invalid; } - if (typeof(IDictionary).IsAssignableFrom(type)) - { - elementType = typeof(object); - addMethod = default; - - if (type.IsInterface) - { - Type concreteDictionaryType = typeof(Dictionary); - - if (type.IsAssignableFrom(concreteDictionaryType)) - { - runtimeType = concreteDictionaryType; - } - } - - return ClassType.Dictionary; - } + // 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) { - Type? genericIEnumerableType = type.GetInterface("System.Collections.Generic.IEnumerable`1"); - - if (genericIEnumerableType != null) - { - elementType = genericIEnumerableType.GetGenericArguments()[0]; - } - else - { - elementType = typeof(object); - } + runtimeType = type; } - - if (typeof(IList).IsAssignableFrom(type)) + else { - addMethod = default; - if (type.IsInterface) { - Type concreteListType = typeof(List<>).MakeGenericType(elementType); - if (type.IsAssignableFrom(concreteListType)) - { - runtimeType = concreteListType; - } + runtimeType = converterRuntimeType; } - } - else if (type.IsInterface) - { - addMethod = default; - - Type concreteType = typeof(List<>).MakeGenericType(elementType); - if (type.IsAssignableFrom(concreteType)) + else if (converterRuntimeType.IsInterface) { - runtimeType = concreteType; + runtimeType = type; } else { - concreteType = typeof(HashSet<>).MakeGenericType(elementType); - if (type.IsAssignableFrom(concreteType)) + // Use the most derived version from the converter.RuntimeType or converter.TypeToConvert. + if (type.IsAssignableFrom(converterRuntimeType)) { - runtimeType = concreteType; + runtimeType = converterRuntimeType; } - } - } - else - { - addMethod = default; - - if (checkForAddMethod) - { - Type? genericICollectionType = type.GetInterface("System.Collections.Generic.ICollection`1"); - if (genericICollectionType != null) + else if (converterRuntimeType.IsAssignableFrom(type) || converter.TypeToConvert.IsAssignableFrom(type)) { - addMethod = genericICollectionType.GetMethod("Add"); + runtimeType = type; } else { - // Non-immutable stack or queue. - MethodInfo? methodInfo = type.GetMethod("Push") ?? type.GetMethod("Enqueue"); - if (methodInfo?.ReturnType == typeof(void)) - { - addMethod = methodInfo; - } + throw ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(type, parentClassType, propertyInfo); } } } - return ClassType.Enumerable; + elementType = converter.ElementType; + + return converter.ClassType; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonCollectionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonCollectionConverter.cs new file mode 100644 index 00000000000000..054e38f83ea0ff --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonCollectionConverter.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Text.Json.Serialization +{ + /// + /// Base class for all collections. Collections are assumed to implement System.Collections.IEnumerable. + /// + internal abstract class JsonCollectionConverter : JsonResumableConverter + { + internal override ClassType ClassType => ClassType.Enumerable; + internal override Type ElementType => typeof(TElement); + } +} 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 new file mode 100644 index 00000000000000..da4021f81707a0 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.ReadAhead.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Text.Json.Serialization +{ + /// + /// Converts an object or value to or from JSON. + /// + public abstract partial class JsonConverter + { + /// + /// Perform a Read() and if read-ahead is required, also read-ahead (to the end of the current JSON level). + /// + // AggressiveInlining used since this method is on a hot path and short. The optionally called + // method DoSingleValueReadWithReadAhead is not inlined. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool SingleValueReadWithReadAhead(ClassType classType, ref Utf8JsonReader reader, ref ReadStack state) + { + bool readAhead = (state.ReadAhead && (classType & (ClassType.Value | ClassType.NewValue)) != 0); + if (!readAhead) + { + return reader.Read(); + } + + return DoSingleValueReadWithReadAhead(ref reader, ref state); + } + + internal static bool DoSingleValueReadWithReadAhead(ref Utf8JsonReader reader, ref ReadStack state) + { + // When we're reading ahead we always have to save the state as we don't know if the next token + // is an opening object or an array brace. + JsonReaderState initialReaderState = reader.CurrentState; + long initialReaderBytesConsumed = reader.BytesConsumed; + + if (!reader.Read()) + { + return false; + } + + // Perform the actual read-ahead. + JsonTokenType tokenType = reader.TokenType; + if (tokenType == JsonTokenType.StartObject || tokenType == JsonTokenType.StartArray) + { + // Attempt to skip to make sure we have all the data we need. + bool complete = reader.TrySkip(); + + // We need to restore the state in all cases as we need to be positioned back before + // the current token to either attempt to skip again or to actually read the value. + + reader = new Utf8JsonReader(reader.OriginalSpan.Slice(checked((int)initialReaderBytesConsumed)), + isFinalBlock: reader.IsFinalBlock, + state: initialReaderState); + + Debug.Assert(reader.BytesConsumed == 0); + state.BytesConsumed += initialReaderBytesConsumed; + + if (!complete) + { + // Couldn't read to the end of the object, exit out to get more data in the buffer. + return false; + } + + // Success, requeue the reader to the start token. + reader.ReadWithVerify(); + Debug.Assert(tokenType == reader.TokenType); + } + + return true; + } + } +} 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 ea06f03a3a42ad..1bc90ae29a17d6 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 @@ -7,7 +7,7 @@ namespace System.Text.Json.Serialization /// /// Converts an object or value to or from JSON. /// - public abstract class JsonConverter + public abstract partial class JsonConverter { internal JsonConverter() { } @@ -18,7 +18,48 @@ internal JsonConverter() { } /// True if the type can be converted, false otherwise. public abstract bool CanConvert(Type typeToConvert); + internal abstract ClassType ClassType { get; } + + // Whether the converter should handle the null value. + internal virtual bool HandleNullValue + { + get + { + // Allow a converter that can't be null to return a null value representation, such as JsonElement or Nullable<>. + // In other cases, this will likely cause an JsonException in the converter. + return TypeToConvert.IsValueType; + } + } + + /// + /// 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; } + + internal abstract JsonPropertyInfo CreateJsonPropertyInfo(); + + internal abstract Type? ElementType { get; } + + // For polymorphic cases, the concrete type to create. + internal virtual Type RuntimeType => TypeToConvert; + + internal bool ShouldFlush(Utf8JsonWriter writer, ref WriteStack state) + { + // If surpassed flush threshold then return false which will flush stream. + return (state.FlushThreshold > 0 && writer.BytesPending > state.FlushThreshold); + } + // This is used internally to quickly determine the type being converted for JsonConverter. - internal virtual Type? TypeToConvert => null; + internal abstract Type TypeToConvert { get; } + + internal abstract bool TryReadAsObject(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out object? value); + internal abstract bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs index d96ec3574ef2c7..352c32ed859c75 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs @@ -19,10 +19,12 @@ public abstract class JsonConverterFactory : JsonConverter /// protected JsonConverterFactory() { } - internal JsonConverter? GetConverterInternal(Type typeToConvert, JsonSerializerOptions options) + internal sealed override ClassType ClassType { - Debug.Assert(CanConvert(typeToConvert)); - return CreateConverter(typeToConvert, options); + get + { + return ClassType.Invalid; + } } /// @@ -32,7 +34,47 @@ protected JsonConverterFactory() { } /// The being used. /// /// An instance of a where T is compatible with . + /// If is returned, a will be thrown. /// - public abstract JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options); + public abstract JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options); + + internal override JsonPropertyInfo CreateJsonPropertyInfo() + { + // We should never get here. + Debug.Assert(false); + + throw new InvalidOperationException(); + } + + internal sealed override Type? ElementType => null; + + internal JsonConverter? GetConverterInternal(Type typeToConvert, JsonSerializerOptions options) + { + Debug.Assert(CanConvert(typeToConvert)); + return CreateConverter(typeToConvert, options); + } + + internal sealed override bool TryReadAsObject( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options, + ref ReadStack state, + out object? value) + { + // We should never get here. + Debug.Assert(false); + + throw new InvalidOperationException(); + } + + internal sealed override bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state) + { + // We should never get here. + Debug.Assert(false); + + throw new InvalidOperationException(); + } + + internal sealed override Type 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 df8577a2d3e6bf..13ff86dfb40e83 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 @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics; + namespace System.Text.Json.Serialization { /// @@ -13,7 +15,15 @@ public abstract class JsonConverter : JsonConverter /// /// When overidden, constructs a new instance. /// - protected internal 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 == typeof(object); + + IsInternalConverter = GetType().Assembly == typeof(JsonConverter).Assembly; + CanUseDirectReadOrWrite = !CanBePolymorphic && IsInternalConverter && ClassType == ClassType.Value; + } /// /// Determines whether the type can be converted. @@ -28,6 +38,57 @@ public override bool CanConvert(Type typeToConvert) return typeToConvert == typeof(T); } + internal override ClassType ClassType => ClassType.Value; + + internal sealed override JsonPropertyInfo CreateJsonPropertyInfo() + { + return new JsonPropertyInfo(); + } + + internal override Type? ElementType => null; + + /// + /// Is the converter built-in. + /// + internal bool IsInternalConverter { get; set; } + + // 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) + { + T valueOfT = (T)value!; + return TryWrite(writer, valueOfT, options, ref state); + } + + // Provide a default implementation for value converters. + internal virtual bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state) + { + Write(writer, value, options); + return true; + } + + // This non-generic API is sealed as it just forwards to the generic version. + internal sealed override bool TryReadAsObject(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out object? value) + { + bool success = TryRead(ref reader, typeToConvert, options, ref state, out T valueOfT); + if (success) + { + value = valueOfT; + } + else + { + value = default; + } + + return success; + } + + // Provide a default implementation for value converters. + internal virtual bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T value) + { + value = Read(ref reader, typeToConvert, options); + return true; + } + /// /// Read and convert the JSON to T. /// @@ -40,6 +101,287 @@ public override bool CanConvert(Type typeToConvert) /// The value that was converted. public abstract T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options); + internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T value) + { + if (ClassType == ClassType.Value) + { + // A value converter should never be within a continuation. + Debug.Assert(!state.IsContinuation); + + // For perf and converter simplicity, handle null here instead of forwarding to the converter. + if (reader.TokenType == JsonTokenType.Null && !HandleNullValue) + { + value = default!; + return true; + } + +#if !DEBUG + // For performance, only perform validation on internal converters on debug builds. + if (IsInternalConverter) + { + value = Read(ref reader, typeToConvert, options); + } + else +#endif + { + JsonTokenType originalPropertyTokenType = reader.TokenType; + int originalPropertyDepth = reader.CurrentDepth; + long originalPropertyBytesConsumed = reader.BytesConsumed; + + value = Read(ref reader, typeToConvert, options); + VerifyRead( + originalPropertyTokenType, + originalPropertyDepth, + originalPropertyBytesConsumed, + isValueConverter: true, + ref reader); + } + + return true; + } + + bool success; + + // Remember if we were a continuation here since Push() may affect IsContinuation. + bool wasContinuation = state.IsContinuation; + + state.Push(); + +#if !DEBUG + // For performance, only perform validation on internal converters on debug builds. + if (IsInternalConverter) + { + if (reader.TokenType == JsonTokenType.Null && !HandleNullValue && !wasContinuation) + { + // For perf and converter simplicity, handle null here instead of forwarding to the converter. + value = default!; + success = true; + } + else + { + success = OnTryRead(ref reader, typeToConvert, options, ref state, out value); + } + } + else +#endif + { + if (!wasContinuation) + { + // For perf and converter simplicity, handle null here instead of forwarding to the converter. + if (reader.TokenType == JsonTokenType.Null && !HandleNullValue) + { + value = default!; + state.Pop(true); + return true; + } + + Debug.Assert(state.Current.OriginalTokenType == JsonTokenType.None); + state.Current.OriginalTokenType = reader.TokenType; + + Debug.Assert(state.Current.OriginalDepth == 0); + state.Current.OriginalDepth = reader.CurrentDepth; + } + + success = OnTryRead(ref reader, typeToConvert, options, ref state, out value); + if (success) + { + if (state.IsContinuation) + { + // The resumable converter did not forward to the next converter that previously returned false. + ThrowHelper.ThrowJsonException_SerializationConverterRead(this); + } + + VerifyRead( + state.Current.OriginalTokenType, + state.Current.OriginalDepth, + bytesConsumed : 0, + isValueConverter: false, + ref reader); + + // No need to clear state.Current.* since a stack pop will occur. + } + } + + state.Pop(success); + return success; + } + + internal bool TryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state) + { + if (writer.CurrentDepth >= options.EffectiveMaxDepth) + { + ThrowHelper.ThrowInvalidOperationException_SerializerCycleDetected(options.MaxDepth); + } + + if (CanBePolymorphic) + { + if (value == null) + { + writer.WriteNullValue(); + return true; + } + + Type type = value.GetType(); + if (type == typeof(object)) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + return true; + } + + if (type != TypeToConvert) + { + // Handle polymorphic case and get the new converter. + JsonConverter jsonConverter = state.Current.InitializeReEntry(type, options); + if (jsonConverter != this) + { + // We found a different converter; forward to that. + return jsonConverter.TryWriteAsObject(writer, value, options, ref state); + } + } + } + + if (ClassType == ClassType.Value) + { + Debug.Assert(!state.IsContinuation); + + int originalPropertyDepth = writer.CurrentDepth; + + Write(writer, value, options); + VerifyWrite(originalPropertyDepth, writer); + + return true; + } + + bool isContinuation = state.IsContinuation; + + state.Push(); + + if (!isContinuation) + { + Debug.Assert(state.Current.OriginalDepth == 0); + state.Current.OriginalDepth = writer.CurrentDepth; + } + + 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); + + return success; + } + + internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state) + { + Debug.Assert(this is JsonDictionaryConverter); + + if (writer.CurrentDepth >= options.EffectiveMaxDepth) + { + ThrowHelper.ThrowInvalidOperationException_SerializerCycleDetected(options.MaxDepth); + } + + bool success; + JsonDictionaryConverter dictionaryConverter = (JsonDictionaryConverter)this; + + if (ClassType == ClassType.Value) + { + Debug.Assert(!state.IsContinuation); + + int originalPropertyDepth = writer.CurrentDepth; + + // Ignore the naming policy for extension data. + state.Current.IgnoreDictionaryKeyPolicy = true; + + success = dictionaryConverter.OnWriteResume(writer, value, options, ref state); + if (success) + { + VerifyWrite(originalPropertyDepth, writer); + } + } + else + { + bool isContinuation = state.IsContinuation; + + state.Push(); + + if (!isContinuation) + { + Debug.Assert(state.Current.OriginalDepth == 0); + state.Current.OriginalDepth = writer.CurrentDepth; + } + + // Ignore the naming policy for extension data. + state.Current.IgnoreDictionaryKeyPolicy = true; + + success = dictionaryConverter.OnWriteResume(writer, value, options, ref state); + if (success) + { + VerifyWrite(state.Current.OriginalDepth, writer); + } + + state.Pop(success); + } + + return success; + } + + internal sealed override Type TypeToConvert => typeof(T); + + internal void VerifyRead(JsonTokenType tokenType, int depth, long bytesConsumed, bool isValueConverter, ref Utf8JsonReader reader) + { + switch (tokenType) + { + case JsonTokenType.StartArray: + if (reader.TokenType != JsonTokenType.EndArray) + { + ThrowHelper.ThrowJsonException_SerializationConverterRead(this); + } + else if (depth != reader.CurrentDepth) + { + ThrowHelper.ThrowJsonException_SerializationConverterRead(this); + } + + break; + + case JsonTokenType.StartObject: + if (reader.TokenType != JsonTokenType.EndObject) + { + ThrowHelper.ThrowJsonException_SerializationConverterRead(this); + } + else if (depth != reader.CurrentDepth) + { + ThrowHelper.ThrowJsonException_SerializationConverterRead(this); + } + + break; + + default: + // A non-value converter (object or collection) should always have Start and End tokens. + // A value converter should not make any reads. + if (!isValueConverter || reader.BytesConsumed != bytesConsumed) + { + ThrowHelper.ThrowJsonException_SerializationConverterRead(this); + } + + // Should not be possible to change token type. + Debug.Assert(reader.TokenType == tokenType); + + break; + } + } + + internal void VerifyWrite(int originalDepth, Utf8JsonWriter writer) + { + if (originalDepth != writer.CurrentDepth) + { + ThrowHelper.ThrowJsonException_SerializationConverterWrite(this); + } + } + /// /// Write the value as JSON. /// @@ -51,7 +393,5 @@ public override bool CanConvert(Type typeToConvert) /// The value to convert. /// The being used. public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options); - - internal override Type TypeToConvert => typeof(T); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonDictionaryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonDictionaryConverter.cs index 547b840bd092d8..8123eda642a7ab 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonDictionaryConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonDictionaryConverter.cs @@ -2,19 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections; - -namespace System.Text.Json.Serialization.Converters +namespace System.Text.Json.Serialization { - // Helper to deserialize data into collections that store key-value pairs (not including KeyValuePair<,>) - // e.g. IDictionary, Hashtable, Dictionary<,> IDictionary<,>, SortedList etc. - // We'll call these collections "dictionaries". - // Note: the KeyValuePair<,> type has a value converter, so its deserialization flow will not reach here. - // Also, KeyValuePair<,> is sealed, so deserialization will flow here to support custom types that - // implement KeyValuePair<,>. - internal abstract class JsonDictionaryConverter + /// + /// Base class for dictionary converters such as IDictionary, Hashtable, Dictionary{,} IDictionary{,} and SortedList. + /// + internal abstract class JsonDictionaryConverter : JsonResumableConverter { - // Return type is object, not IDictionary as not all "dictionaries" implement IDictionary e.g. IDictionary. - public abstract object CreateFromDictionary(ref ReadStack state, IDictionary sourceDictionary, JsonSerializerOptions options); + internal override ClassType ClassType => ClassType.Dictionary; + protected internal abstract bool OnWriteResume(Utf8JsonWriter writer, T dictionary, JsonSerializerOptions options, ref WriteStack state); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonEnumerableConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonEnumerableConverter.cs deleted file mode 100644 index ea69e2699f487e..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonEnumerableConverter.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections; - -namespace System.Text.Json.Serialization.Converters -{ - internal abstract class JsonEnumerableConverter - { - public abstract IEnumerable CreateFromList(ref ReadStack state, IList sourceList, JsonSerializerOptions options); - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonObjectConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonObjectConverter.cs new file mode 100644 index 00000000000000..86965da892eec8 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonObjectConverter.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Text.Json.Serialization +{ + /// + /// Base class for non-enumerable, non-primitive objects where public properties + /// are (de)serialized as a JSON object. + /// + internal abstract class JsonObjectConverter : JsonResumableConverter + { + internal override ClassType ClassType => ClassType.Object; + internal sealed override Type? ElementType => null; + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs index 0a4b38f869aab6..5e45f08b3d59b9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs @@ -2,69 +2,39 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections; +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Converters; namespace System.Text.Json { - [DebuggerDisplay("PropertyInfo={PropertyInfo}, Element={ElementClassInfo}")] + [DebuggerDisplay("PropertyInfo={PropertyInfo}")] internal abstract class JsonPropertyInfo { - // Cache the converters so they don't get created for every enumerable property. - private static readonly JsonEnumerableConverter s_jsonArrayConverter = new DefaultArrayConverter(); - private static readonly JsonEnumerableConverter s_jsonImmutableEnumerableConverter = new DefaultImmutableEnumerableConverter(); - private static readonly JsonDictionaryConverter s_jsonImmutableDictionaryConverter = new DefaultImmutableDictionaryConverter(); + public static readonly JsonPropertyInfo s_missingProperty = GetPropertyPlaceholder(); - public static readonly JsonPropertyInfo s_missingProperty = GetMissingProperty(); - - private JsonClassInfo? _elementClassInfo; private JsonClassInfo? _runtimeClassInfo; - private JsonClassInfo? _declaredTypeClassInfo; - - private JsonPropertyInfo? _dictionaryValuePropertyPolicy; - - public bool CanBeNull { get; private set; } public ClassType ClassType; - [DisallowNull] - public abstract JsonConverter? ConverterBase { get; set; } + public abstract JsonConverter ConverterBase { get; set; } - private static JsonPropertyInfo GetMissingProperty() + public static JsonPropertyInfo GetPropertyPlaceholder() { - JsonPropertyInfo info = new JsonPropertyInfoNotNullable(); + JsonPropertyInfo info = new JsonPropertyInfo(); info.IsPropertyPolicy = false; info.ShouldDeserialize = false; info.ShouldSerialize = false; return info; } - // Copy any settings defined at run-time to the new property. - public void CopyRuntimeSettingsTo(JsonPropertyInfo other) - { - other.EscapedName = EscapedName; - other.Name = Name; - other.NameAsString = NameAsString; - other.PropertyNameKey = PropertyNameKey; - } - - public abstract IList CreateConverterList(); - - public abstract IDictionary CreateConverterDictionary(); - - public abstract IEnumerable CreateImmutableCollectionInstance(ref ReadStack state, Type collectionType, string delegateKey, IList sourceList, JsonSerializerOptions options); - - public abstract IDictionary CreateImmutableDictionaryInstance(ref ReadStack state, Type collectionType, string delegateKey, IDictionary sourceDictionary, JsonSerializerOptions options); - // Create a property that is ignored at run-time. It uses the same type (typeof(sbyte)) to help // prevent issues with unsupported types and helps ensure we don't accidently (de)serialize it. - public static JsonPropertyInfo CreateIgnoredPropertyPlaceholder(PropertyInfo? propertyInfo, JsonSerializerOptions options) + public static JsonPropertyInfo CreateIgnoredPropertyPlaceholder(PropertyInfo propertyInfo, JsonSerializerOptions options) { - JsonPropertyInfo jsonPropertyInfo = new JsonPropertyInfoNotNullable(); + JsonPropertyInfo jsonPropertyInfo = new JsonPropertyInfo(); jsonPropertyInfo.Options = options; jsonPropertyInfo.PropertyInfo = propertyInfo; jsonPropertyInfo.DeterminePropertyName(); @@ -136,104 +106,18 @@ private void DetermineSerializationCapabilities() { if (HasGetter) { + Debug.Assert(ConverterBase != null); + ShouldSerialize = true; if (HasSetter) { ShouldDeserialize = true; - - if (RuntimePropertyType.IsArray) - { - // Verify that we don't have a multidimensional array. - if (RuntimePropertyType.GetArrayRank() > 1) - { - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(RuntimePropertyType, ParentClassType, PropertyInfo); - } - - EnumerableConverter = s_jsonArrayConverter; - } - else if (ClassType == ClassType.Dictionary && DefaultImmutableDictionaryConverter.IsImmutableDictionary(RuntimePropertyType)) - { - Debug.Assert(ElementType != null); - DefaultImmutableDictionaryConverter.RegisterImmutableDictionary(RuntimePropertyType, ElementType, Options); - DictionaryConverter = s_jsonImmutableDictionaryConverter; - } - else if (ClassType == ClassType.Enumerable && DefaultImmutableEnumerableConverter.IsImmutableEnumerable(RuntimePropertyType)) - { - Debug.Assert(ElementType != null); - DefaultImmutableEnumerableConverter.RegisterImmutableCollection(RuntimePropertyType, ElementType, Options); - EnumerableConverter = s_jsonImmutableEnumerableConverter; - } - } - } - } - } - - /// - /// Return the JsonPropertyInfo for the TValue in IDictionary{string, TValue} when deserializing. - /// This only needs to contain the raw TValue and does not need converter, etc applied since it - /// is only used for "casting" reasons. - /// - /// - /// This should not be called during warm-up (initial creation of JsonPropertyInfos) to avoid recursive behavior - /// which could result in a StackOverflowException. - /// - public JsonPropertyInfo DictionaryValuePropertyPolicy - { - get - { - Debug.Assert(ClassType == ClassType.Dictionary); - - if (_dictionaryValuePropertyPolicy == null) - { - // Use the existing PolicyProperty if there is one. - if ((_dictionaryValuePropertyPolicy = ElementClassInfo!.PolicyProperty) == null) - { - Type? dictionaryValueType = ElementType; - Debug.Assert(dictionaryValueType != null); - - _dictionaryValuePropertyPolicy = JsonClassInfo.CreatePolicyProperty( - declaredPropertyType : dictionaryValueType, - runtimePropertyType : dictionaryValueType, - elementType : null, - nullableUnderlyingType : Nullable.GetUnderlyingType(dictionaryValueType), - converter: null, - ClassType.Dictionary, - Options); } } - - return _dictionaryValuePropertyPolicy; } } - /// - /// Return the JsonClassInfo for the element type, or null if the property is not an enumerable or dictionary. - /// - /// - /// This should not be called during warm-up (initial creation of JsonClassInfos) to avoid recursive behavior - /// which could result in a StackOverflowException. - /// - public JsonClassInfo? ElementClassInfo - { - get - { - if (_elementClassInfo == null && ElementType != null) - { - Debug.Assert(ClassType == ClassType.Enumerable || ClassType == ClassType.Dictionary); - - _elementClassInfo = Options.GetOrAddClass(ElementType); - } - - return _elementClassInfo; - } - } - - public Type? ElementType { get; set; } - - public JsonEnumerableConverter? EnumerableConverter { get; private set; } - public JsonDictionaryConverter? DictionaryConverter { get; private set; } - // The escaped name passed to the writer. // Use a field here (not a property) to avoid value semantics. public JsonEncodedText? EscapedName; @@ -243,36 +127,8 @@ public JsonClassInfo? ElementClassInfo return (TAttribute?)propertyInfo.GetCustomAttribute(typeof(TAttribute), inherit: false); } - public abstract Type GetDictionaryConcreteType(); - - public void GetDictionaryKeyAndValue(ref WriteStackFrame writeStackFrame, out string key, out object? value) - { - Debug.Assert(ClassType == ClassType.Dictionary); - - if (writeStackFrame.CollectionEnumerator is IDictionaryEnumerator iDictionaryEnumerator) - { - if (iDictionaryEnumerator.Key is string keyAsString) - { - // Since IDictionaryEnumerator is not based on generics we can obtain the value directly. - key = keyAsString; - value = iDictionaryEnumerator.Value; - } - else - { - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection( - writeStackFrame.JsonPropertyInfo!.DeclaredPropertyType, - writeStackFrame.JsonPropertyInfo.ParentClassType, - writeStackFrame.JsonPropertyInfo.PropertyInfo); - } - } - else - { - // Forward to the generic dictionary. - DictionaryValuePropertyPolicy.GetDictionaryKeyAndValueFromGenericDictionary(ref writeStackFrame, out key, out value); - } - } - - public abstract void GetDictionaryKeyAndValueFromGenericDictionary(ref WriteStackFrame writeStackFrame, out string key, out object? value); + public abstract bool GetMemberAndWriteJson(object obj, ref WriteStack state, Utf8JsonWriter writer); + public abstract bool GetMemberAndWriteJsonExtensionData(object obj, ref WriteStack state, Utf8JsonWriter writer); public virtual void GetPolicies() { @@ -281,57 +137,31 @@ public virtual void GetPolicies() IgnoreNullValues = Options.IgnoreNullValues; } - public abstract object? GetValueAsObject(object? obj); + public abstract object? GetValueAsObject(object obj); public bool HasGetter { get; set; } public bool HasSetter { get; set; } - public bool HasInternalConverter { get; private set; } - public virtual void Initialize( Type parentClassType, Type declaredPropertyType, - Type runtimePropertyType, + Type? runtimePropertyType, ClassType runtimeClassType, PropertyInfo? propertyInfo, - Type? elementType, - JsonConverter? converter, - bool treatAsNullable, + JsonConverter converter, JsonSerializerOptions options) { + Debug.Assert(converter != null); + ParentClassType = parentClassType; DeclaredPropertyType = declaredPropertyType; RuntimePropertyType = runtimePropertyType; ClassType = runtimeClassType; PropertyInfo = propertyInfo; - ElementType = elementType; + ConverterBase = converter; Options = options; - CanBeNull = treatAsNullable || !runtimePropertyType.IsValueType; - - if (converter != null) - { - ConverterBase = converter; - - HasInternalConverter = (converter.GetType().Assembly == GetType().Assembly); - } } - public abstract bool TryCreateEnumerableAddMethod(object target, [NotNullWhen(true)] out object? addMethodDelegate); - - public abstract object? CreateEnumerableAddMethod(MethodInfo addMethod, object target); - - public abstract void AddObjectToEnumerableWithReflection(object addMethodDelegate, object? value); - - public abstract void AddObjectToParentEnumerable(object addMethodDelegate, object? value); - - public abstract void AddObjectToDictionary(object target, string key, object? value); - - public abstract void AddObjectToParentDictionary(object target, string key, object? value); - - public abstract bool CanPopulateDictionary(object target); - - public abstract bool ParentDictionaryCanBePopulated(object target); - public bool IgnoreNullValues { get; private set; } public bool IsPropertyPolicy { get; protected set; } @@ -349,89 +179,58 @@ public virtual void Initialize( // Options can be referenced here since all JsonPropertyInfos originate from a JsonClassInfo that is cached on JsonSerializerOptions. protected JsonSerializerOptions Options { get; set; } = null!; // initialized in Init method - protected abstract void OnRead(ref ReadStack state, ref Utf8JsonReader reader); - protected abstract void OnReadEnumerable(ref ReadStack state, ref Utf8JsonReader reader); - protected abstract void OnWrite(ref WriteStackFrame current, Utf8JsonWriter writer); - protected virtual void OnWriteDictionary(ref WriteStackFrame current, Utf8JsonWriter writer) { } - protected abstract void OnWriteEnumerable(ref WriteStackFrame current, Utf8JsonWriter writer); - - public Type ParentClassType { get; private set; } = null!; - - public PropertyInfo? PropertyInfo { get; private set; } - - public void Read(JsonTokenType tokenType, ref ReadStack state, ref Utf8JsonReader reader) + public bool ReadJsonAndAddExtensionProperty(object obj, ref ReadStack state, ref Utf8JsonReader reader) { - Debug.Assert(ShouldDeserialize); + object propValue = GetValueAsObject(obj)!; - JsonPropertyInfo? propertyInfo; - JsonClassInfo? elementClassInfo = ElementClassInfo; - if (elementClassInfo != null && (propertyInfo = elementClassInfo.PolicyProperty) != null) + if (propValue is IDictionary dictionaryObject) { - if (!state.Current.CollectionPropertyInitialized) + // Handle case where extension property is System.Object-based. + + if (reader.TokenType == JsonTokenType.Null) { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(propertyInfo.RuntimePropertyType); + // A null JSON value is treated as a null object reference. + dictionaryObject[state.Current.JsonPropertyNameAsString!] = null; } + else + { + JsonConverter converter = (JsonConverter) + state.Current.JsonPropertyInfo!.RuntimeClassInfo.ElementClassInfo!.PolicyProperty!.ConverterBase; - // Forward the setter to the value-based JsonPropertyInfo. - propertyInfo.ReadEnumerable(tokenType, ref state, ref reader); - } - // For performance on release build, don't verify converter correctness for internal converters. - else if (HasInternalConverter) - { -#if DEBUG - JsonTokenType originalTokenType = reader.TokenType; - int originalDepth = reader.CurrentDepth; - long originalBytesConsumed = reader.BytesConsumed; -#endif - - OnRead(ref state, ref reader); + if (!converter.TryRead(ref reader, typeof(JsonElement), Options, ref state, out object? value)) + { + return false; + } -#if DEBUG - VerifyRead(originalTokenType, originalDepth, originalBytesConsumed, ref reader); -#endif + dictionaryObject[state.Current.JsonPropertyNameAsString!] = value; + } } else { - JsonTokenType originalTokenType = reader.TokenType; - int originalDepth = reader.CurrentDepth; - long originalBytesConsumed = reader.BytesConsumed; + // Handle case where extension property is JsonElement-based. - OnRead(ref state, ref reader); + Debug.Assert(propValue is IDictionary); + IDictionary dictionaryJsonElement = (IDictionary)propValue; - VerifyRead(originalTokenType, originalDepth, originalBytesConsumed, ref reader); - } - } + JsonConverter converter = (JsonConverter) + state.Current.JsonPropertyInfo!.RuntimeClassInfo.ElementClassInfo!.PolicyProperty!.ConverterBase; - public void ReadEnumerable(JsonTokenType tokenType, ref ReadStack state, ref Utf8JsonReader reader) - { - Debug.Assert(ShouldDeserialize); + if (!converter.TryRead(ref reader, typeof(JsonElement), Options, ref state, out JsonElement value)) + { + return false; + } - // For performance on release build, don't verify converter correctness for internal converters. - if (HasInternalConverter) - { -#if DEBUG - JsonTokenType originalTokenType = reader.TokenType; - int originalDepth = reader.CurrentDepth; - long originalBytesConsumed = reader.BytesConsumed; -#endif + dictionaryJsonElement[state.Current.JsonPropertyNameAsString!] = value; + } - OnReadEnumerable(ref state, ref reader); + return true; + } -#if DEBUG - VerifyRead(originalTokenType, originalDepth, originalBytesConsumed, ref reader); -#endif - } - else - { - JsonTokenType originalTokenType = reader.TokenType; - int originalDepth = reader.CurrentDepth; - long originalBytesConsumed = reader.BytesConsumed; + public abstract bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref Utf8JsonReader reader); - OnReadEnumerable(ref state, ref reader); + public Type ParentClassType { get; private set; } = null!; - VerifyRead(originalTokenType, originalDepth, originalBytesConsumed, ref reader); - } - } + public PropertyInfo? PropertyInfo { get; private set; } public JsonClassInfo RuntimeClassInfo { @@ -439,171 +238,18 @@ public JsonClassInfo RuntimeClassInfo { if (_runtimeClassInfo == null) { - _runtimeClassInfo = Options.GetOrAddClass(RuntimePropertyType); + _runtimeClassInfo = Options.GetOrAddClass(RuntimePropertyType!); } return _runtimeClassInfo; } } - public JsonClassInfo DeclaredTypeClassInfo - { - get - { - if (_declaredTypeClassInfo == null) - { - _declaredTypeClassInfo = Options.GetOrAddClass(DeclaredPropertyType); - } + public Type? RuntimePropertyType { get; private set; } = null; - return _declaredTypeClassInfo; - } - } - - public Type RuntimePropertyType { get; private set; } = null!; - - public abstract void SetValueAsObject(object? obj, object? value); + public abstract void SetValueAsObject(object obj, object? value); public bool ShouldSerialize { get; private set; } public bool ShouldDeserialize { get; private set; } - - private void VerifyRead(JsonTokenType tokenType, int depth, long bytesConsumed, ref Utf8JsonReader reader) - { - switch (tokenType) - { - case JsonTokenType.StartArray: - if (reader.TokenType != JsonTokenType.EndArray) - { - ThrowHelper.ThrowJsonException_SerializationConverterRead(ConverterBase); - } - else if (depth != reader.CurrentDepth) - { - ThrowHelper.ThrowJsonException_SerializationConverterRead(ConverterBase); - } - - // Should not be possible to have not read anything. - Debug.Assert(bytesConsumed < reader.BytesConsumed); - break; - - case JsonTokenType.StartObject: - if (reader.TokenType != JsonTokenType.EndObject) - { - ThrowHelper.ThrowJsonException_SerializationConverterRead(ConverterBase); - } - else if (depth != reader.CurrentDepth) - { - ThrowHelper.ThrowJsonException_SerializationConverterRead(ConverterBase); - } - - // Should not be possible to have not read anything. - Debug.Assert(bytesConsumed < reader.BytesConsumed); - break; - - default: - // Reading a single property value. - if (reader.BytesConsumed != bytesConsumed) - { - ThrowHelper.ThrowJsonException_SerializationConverterRead(ConverterBase); - } - - // Should not be possible to change token type. - Debug.Assert(reader.TokenType == tokenType); - - break; - } - } - - public void Write(ref WriteStack state, Utf8JsonWriter writer) - { - Debug.Assert(ShouldSerialize); - - if (state.Current.CollectionEnumerator != null) - { - Debug.Assert(ElementClassInfo != null); - - // Forward the setter to the value-based JsonPropertyInfo. - JsonPropertyInfo? propertyInfo = ElementClassInfo.PolicyProperty; - Debug.Assert(propertyInfo != null); - propertyInfo.WriteEnumerable(ref state, writer); - } - // For performance on release build, don't verify converter correctness for internal converters. - else if (HasInternalConverter) - { -#if DEBUG - int originalDepth = writer.CurrentDepth; -#endif - - OnWrite(ref state.Current, writer); - -#if DEBUG - VerifyWrite(originalDepth, writer); -#endif - } - else - { - int originalDepth = writer.CurrentDepth; - OnWrite(ref state.Current, writer); - VerifyWrite(originalDepth, writer); - } - } - - public void WriteDictionary(ref WriteStack state, Utf8JsonWriter writer) - { - Debug.Assert(ShouldSerialize); - - // For performance on release build, don't verify converter correctness for internal converters. - if (HasInternalConverter) - { -#if DEBUG - int originalDepth = writer.CurrentDepth; -#endif - - OnWriteDictionary(ref state.Current, writer); - -#if DEBUG - VerifyWrite(originalDepth, writer); -#endif - } - else - { - int originalDepth = writer.CurrentDepth; - OnWriteDictionary(ref state.Current, writer); - VerifyWrite(originalDepth, writer); - } - } - - public void WriteEnumerable(ref WriteStack state, Utf8JsonWriter writer) - { - Debug.Assert(ShouldSerialize); - - // For performance on release build, don't verify converter correctness for internal converters. - if (HasInternalConverter) - { -#if DEBUG - int originalDepth = writer.CurrentDepth; -#endif - - OnWriteEnumerable(ref state.Current, writer); - -#if DEBUG - VerifyWrite(originalDepth, writer); -#endif - } - else - { - int originalDepth = writer.CurrentDepth; - OnWriteEnumerable(ref state.Current, writer); - VerifyWrite(originalDepth, writer); - } - } - - private void VerifyWrite(int originalDepth, Utf8JsonWriter writer) - { - if (originalDepth != writer.CurrentDepth) - { - ThrowHelper.ThrowJsonException_SerializationConverterWrite(ConverterBase); - } - } - - public abstract Type GetJsonPreservableArrayReferenceType(); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoCommon.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoCommon.cs deleted file mode 100644 index 69c4246bab9220..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoCommon.cs +++ /dev/null @@ -1,244 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Text.Json.Serialization; - -namespace System.Text.Json -{ - /// - /// Represents a strongly-typed property to prevent boxing and to create a direct delegate to the getter\setter. - /// - internal abstract class JsonPropertyInfoCommon : JsonPropertyInfo - { - public Func? Get { get; private set; } - public Action? Set { get; private set; } - - public JsonConverter? Converter { get; internal set; } - - public override void Initialize( - Type parentClassType, - Type declaredPropertyType, - Type runtimePropertyType, - ClassType runtimeClassType, - PropertyInfo? propertyInfo, - Type? elementType, - JsonConverter? converter, - bool treatAsNullable, - JsonSerializerOptions options) - { - base.Initialize( - parentClassType, - declaredPropertyType, - runtimePropertyType, - runtimeClassType, - propertyInfo, - elementType, - converter, - treatAsNullable, - options); - - if (propertyInfo != null) - { - if (propertyInfo.GetMethod?.IsPublic == true) - { - HasGetter = true; - Get = options.MemberAccessorStrategy.CreatePropertyGetter(propertyInfo); - } - - if (propertyInfo.SetMethod?.IsPublic == true) - { - HasSetter = true; - Set = options.MemberAccessorStrategy.CreatePropertySetter(propertyInfo); - } - } - else - { - IsPropertyPolicy = true; - HasGetter = true; - HasSetter = true; - } - - GetPolicies(); - } - - [DisallowNull] - public override JsonConverter? ConverterBase - { - get - { - return Converter; - } - set - { - Debug.Assert(Converter == null); - Debug.Assert(value is JsonConverter); - - Converter = (JsonConverter)value; - } - } - - public override object? GetValueAsObject(object? obj) - { - if (IsPropertyPolicy) - { - return obj; - } - - Debug.Assert(HasGetter); - return Get!(obj); - } - - public override void SetValueAsObject(object? obj, object? value) - { - Debug.Assert(HasSetter); - TDeclaredProperty typedValue = (TDeclaredProperty)value!; - - if (typedValue != null || !IgnoreNullValues) - { - Set!(obj, typedValue); - } - } - - private JsonPropertyInfo? _elementPropertyInfo; - - private void SetPropertyInfoForObjectElement() - { - Debug.Assert(ElementClassInfo != null); - if (_elementPropertyInfo == null && ElementClassInfo.PolicyProperty == null) - { - _elementPropertyInfo = ElementClassInfo.CreateRootProperty(Options); - } - } - - public override bool TryCreateEnumerableAddMethod(object target, [NotNullWhen(true)] out object? addMethodDelegate) - { - SetPropertyInfoForObjectElement(); - Debug.Assert((_elementPropertyInfo ?? ElementClassInfo!.PolicyProperty) != null); - - addMethodDelegate = (_elementPropertyInfo ?? ElementClassInfo!.PolicyProperty!).CreateEnumerableAddMethod(RuntimeClassInfo.AddItemToObject!, target); - return addMethodDelegate != null; - } - - public override object? CreateEnumerableAddMethod(MethodInfo addMethod, object target) - { - if (target is ICollection collection && collection.IsReadOnly) - { - return null; - } - - return Options.MemberAccessorStrategy.CreateAddDelegate(addMethod, target); - } - - public override void AddObjectToEnumerableWithReflection(object addMethodDelegate, object? value) - { - Debug.Assert((_elementPropertyInfo ?? ElementClassInfo!.PolicyProperty) != null); - (_elementPropertyInfo ?? ElementClassInfo!.PolicyProperty!).AddObjectToParentEnumerable(addMethodDelegate, value); - } - - public override void AddObjectToParentEnumerable(object addMethodDelegate, object? value) - { - ((Action)addMethodDelegate)((TDeclaredProperty)value!); - } - - public override void AddObjectToDictionary(object target, string key, object? value) - { - Debug.Assert((_elementPropertyInfo ?? ElementClassInfo?.PolicyProperty) != null); - (_elementPropertyInfo ?? ElementClassInfo!.PolicyProperty!).AddObjectToParentDictionary(target, key, value); - } - - public override void AddObjectToParentDictionary(object target, string key, object? value) - { - if (target is IDictionary genericDict) - { - Debug.Assert(!genericDict.IsReadOnly); - genericDict[key] = (TDeclaredProperty)value!; - } - else - { - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(target.GetType(), parentType: null, memberInfo: null); - } - } - - public override bool CanPopulateDictionary(object target) - { - SetPropertyInfoForObjectElement(); - Debug.Assert((_elementPropertyInfo ?? ElementClassInfo!.PolicyProperty) != null); - return (_elementPropertyInfo ?? ElementClassInfo!.PolicyProperty!).ParentDictionaryCanBePopulated(target); - } - - public override bool ParentDictionaryCanBePopulated(object target) - { - if (target is IDictionary genericDict && !genericDict.IsReadOnly) - { - return true; - } - else if (target is IDictionary dict && !dict.IsReadOnly) - { - Type? genericDictType = target.GetType().GetInterface("System.Collections.Generic.IDictionary`2") ?? - target.GetType().GetInterface("System.Collections.Generic.IReadOnlyDictionary`2"); - - if (genericDictType != null && genericDictType.GetGenericArguments()[0] != typeof(string)) - { - return false; - } - - return true; - } - - return false; - } - - public override IList CreateConverterList() - { - return new List(); - } - - public override IDictionary CreateConverterDictionary() - { - return new Dictionary(); - } - - // Creates an IEnumerable and populates it with the items in the - // sourceList argument then uses the delegateKey argument to identify the appropriate cached - // CreateRange method to create and return the desired immutable collection type. - public override IEnumerable CreateImmutableCollectionInstance(ref ReadStack state, Type collectionType, string delegateKey, IList sourceList, JsonSerializerOptions options) - { - IEnumerable? collection = null; - - if (!options.TryGetCreateRangeDelegate(delegateKey, out ImmutableCollectionCreator? creator) || - !creator.CreateImmutableEnumerable(sourceList, out collection)) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(collectionType, state.JsonPath()); - } - - return collection; - } - - // Creates an IEnumerable and populates it with the items in the - // sourceList argument then uses the delegateKey argument to identify the appropriate cached - // CreateRange method to create and return the desired immutable collection type. - public override IDictionary CreateImmutableDictionaryInstance(ref ReadStack state, Type collectionType, string delegateKey, IDictionary sourceDictionary, JsonSerializerOptions options) - { - IDictionary? collection = null; - - if (!options.TryGetCreateRangeDelegate(delegateKey, out ImmutableCollectionCreator? creator) || - !creator.CreateImmutableDictionary(sourceDictionary, out collection)) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(collectionType, state.JsonPath()); - } - - return collection; - } - - public override Type GetJsonPreservableArrayReferenceType() - { - return typeof(JsonPreservableArrayReference); - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullable.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullable.cs deleted file mode 100644 index df6f1b4f68ca1d..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullable.cs +++ /dev/null @@ -1,151 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; - -namespace System.Text.Json -{ - /// - /// Represents a strongly-typed property that is not a . - /// - internal sealed class JsonPropertyInfoNotNullable : - JsonPropertyInfoCommon - where TConverter : TDeclaredProperty - { - protected override void OnRead(ref ReadStack state, ref Utf8JsonReader reader) - { - if (Converter == null) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType); - } - - TConverter value = Converter.Read(ref reader, RuntimePropertyType, Options); - - if (state.Current.ReturnValue == null) - { - state.Current.ReturnValue = value; - } - else - { - Debug.Assert(Set != null); - Set(state.Current.ReturnValue, value); - } - } - - protected override void OnReadEnumerable(ref ReadStack state, ref Utf8JsonReader reader) - { - if (Converter == null) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType); - } - - if (state.Current.KeyName == null && state.Current.IsProcessingDictionary()) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType); - return; - } - - // We need an initialized array in order to store the values. - if (state.Current.IsProcessingEnumerable() && state.Current.TempEnumerableValues == null && state.Current.ReturnValue == null) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType); - return; - } - - TConverter value = Converter.Read(ref reader, RuntimePropertyType, Options); - JsonSerializer.ApplyValueToEnumerable(ref value, ref state); - } - - protected override void OnWrite(ref WriteStackFrame current, Utf8JsonWriter writer) - { - TConverter value; - if (IsPropertyPolicy) - { - value = (TConverter)current.CurrentValue!; - } - else - { - Debug.Assert(Get != null); - value = (TConverter)Get(current.CurrentValue)!; - } - - if (value == null) - { - Debug.Assert(EscapedName.HasValue); - - if (!IgnoreNullValues) - { - writer.WriteNull(EscapedName.Value); - } - } - else if (Converter != null) - { - if (EscapedName.HasValue) - { - writer.WritePropertyName(EscapedName.Value); - } - - Converter.Write(writer, value, Options); - } - } - - protected override void OnWriteDictionary(ref WriteStackFrame current, Utf8JsonWriter writer) - { - Debug.Assert(Converter != null); - JsonSerializer.WriteDictionary(Converter, Options, ref current, writer); - } - - protected override void OnWriteEnumerable(ref WriteStackFrame current, Utf8JsonWriter writer) - { - if (Converter != null) - { - Debug.Assert(current.CollectionEnumerator != null); - - TConverter value; - - if (current.CollectionEnumerator is IEnumerator enumerator) - { - // Avoid boxing for strongly-typed enumerators such as returned from IList. - value = enumerator.Current; - } - else - { - value = (TConverter)current.CollectionEnumerator.Current!; - } - - if (value == null) - { - writer.WriteNullValue(); - } - else - { - Converter.Write(writer, value, Options); - } - } - } - - public override Type GetDictionaryConcreteType() - { - return typeof(Dictionary); - } - - public override void GetDictionaryKeyAndValueFromGenericDictionary(ref WriteStackFrame writeStackFrame, out string key, out object? value) - { - if (writeStackFrame.CollectionEnumerator is IEnumerator> genericEnumerator) - { - key = genericEnumerator.Current.Key; - value = genericEnumerator.Current.Value; - } - else - { - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection( - writeStackFrame.JsonPropertyInfo!.DeclaredPropertyType, - writeStackFrame.JsonPropertyInfo.ParentClassType, - writeStackFrame.JsonPropertyInfo.PropertyInfo); - } - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullableContravariant.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullableContravariant.cs deleted file mode 100644 index 80247d3226e005..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullableContravariant.cs +++ /dev/null @@ -1,151 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Generic; -using System.Diagnostics; - -namespace System.Text.Json.Serialization -{ - /// - /// Represents a strongly-typed property that is not a . - /// - internal sealed class JsonPropertyInfoNotNullableContravariant : - JsonPropertyInfoCommon - where TDeclaredProperty : TConverter - { - protected override void OnRead(ref ReadStack state, ref Utf8JsonReader reader) - { - if (Converter == null) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType); - } - - TConverter value = Converter.Read(ref reader, RuntimePropertyType, Options); - - if (state.Current.ReturnValue == null) - { - state.Current.ReturnValue = value; - } - else - { - Debug.Assert(Set != null); - Set(state.Current.ReturnValue, (TDeclaredProperty)value!); - } - - return; - } - - protected override void OnReadEnumerable(ref ReadStack state, ref Utf8JsonReader reader) - { - if (Converter == null) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType); - } - - if (state.Current.KeyName == null && state.Current.IsProcessingDictionary()) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType); - return; - } - - // We need an initialized array in order to store the values. - if (state.Current.IsProcessingEnumerable() && state.Current.TempEnumerableValues == null && state.Current.ReturnValue == null) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType); - return; - } - - TConverter value = Converter.Read(ref reader, RuntimePropertyType, Options); - JsonSerializer.ApplyValueToEnumerable(ref value, ref state); - } - - protected override void OnWrite(ref WriteStackFrame current, Utf8JsonWriter writer) - { - TConverter value; - if (IsPropertyPolicy) - { - value = (TConverter)current.CurrentValue!; - } - else - { - Debug.Assert(Get != null); - value = (TConverter)Get(current.CurrentValue); - } - - if (value == null) - { - Debug.Assert(EscapedName.HasValue); - - if (!IgnoreNullValues) - { - writer.WriteNull(EscapedName.Value); - } - } - else if (Converter != null) - { - if (EscapedName.HasValue) - { - writer.WritePropertyName(EscapedName.Value); - } - - Converter.Write(writer, value, Options); - } - } - - protected override void OnWriteDictionary(ref WriteStackFrame current, Utf8JsonWriter writer) - { - Debug.Assert(Converter != null); - JsonSerializer.WriteDictionary(Converter, Options, ref current, writer); - } - - protected override void OnWriteEnumerable(ref WriteStackFrame current, Utf8JsonWriter writer) - { - if (Converter != null) - { - Debug.Assert(current.CollectionEnumerator != null); - - TConverter value; - if (current.CollectionEnumerator is IEnumerator enumerator) - { - // Avoid boxing for strongly-typed enumerators such as returned from IList. - value = enumerator.Current; - } - else - { - value = (TConverter)current.CollectionEnumerator.Current!; - } - - if (value == null) - { - writer.WriteNullValue(); - } - else - { - Converter.Write(writer, value, Options); - } - } - } - - public override Type GetDictionaryConcreteType() - { - return typeof(Dictionary); - } - - public override void GetDictionaryKeyAndValueFromGenericDictionary(ref WriteStackFrame writeStackFrame, out string key, out object? value) - { - if (writeStackFrame.CollectionEnumerator is IEnumerator> genericEnumerator) - { - key = genericEnumerator.Current.Key; - value = genericEnumerator.Current.Value; - } - else - { - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection( - writeStackFrame.JsonPropertyInfo!.DeclaredPropertyType, - writeStackFrame.JsonPropertyInfo.ParentClassType, - writeStackFrame.JsonPropertyInfo.PropertyInfo); - } - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs deleted file mode 100644 index c5dbac440df0f7..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs +++ /dev/null @@ -1,189 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text.Json.Serialization; - -namespace System.Text.Json -{ - /// - /// Represents a strongly-typed property that is a . - /// - internal sealed class JsonPropertyInfoNullable - : JsonPropertyInfoCommon - where TProperty : struct - { - private static readonly Type s_underlyingType = typeof(TProperty); - - protected override void OnRead(ref ReadStack state, ref Utf8JsonReader reader) - { - if (Converter == null) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType); - } - - TProperty value = Converter.Read(ref reader, s_underlyingType, Options); - - if (state.Current.ReturnValue == null) - { - state.Current.ReturnValue = value; - } - else - { - Debug.Assert(Set != null); - Set(state.Current.ReturnValue, value); - } - } - - protected override void OnReadEnumerable(ref ReadStack state, ref Utf8JsonReader reader) - { - if (Converter == null) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType); - } - - TProperty value = Converter.Read(ref reader, s_underlyingType, Options); - TProperty? nullableValue = new TProperty?(value); - JsonSerializer.ApplyValueToEnumerable(ref nullableValue, ref state); - } - - protected override void OnWrite(ref WriteStackFrame current, Utf8JsonWriter writer) - { - TProperty? value; - if (IsPropertyPolicy) - { - value = (TProperty?)current.CurrentValue; - } - else - { - Debug.Assert(Get != null); - value = Get(current.CurrentValue); - } - - if (value == null) - { - Debug.Assert(EscapedName.HasValue); - - if (!IgnoreNullValues) - { - writer.WriteNull(EscapedName.Value); - } - } - else if (Converter != null) - { - if (EscapedName.HasValue) - { - writer.WritePropertyName(EscapedName.Value); - } - - Converter.Write(writer, value.GetValueOrDefault(), Options); - } - } - - protected override void OnWriteDictionary(ref WriteStackFrame current, Utf8JsonWriter writer) - { - Debug.Assert(Converter != null && current.CollectionEnumerator != null); - - string? key = null; - TProperty? value = null; - if (current.CollectionEnumerator is IEnumerator> enumerator) - { - key = enumerator.Current.Key; - value = enumerator.Current.Value; - } - else - { - if (((DictionaryEntry)current.CollectionEnumerator.Current!).Key is string keyAsString) - { - key = keyAsString; - value = (TProperty?)((DictionaryEntry)current.CollectionEnumerator.Current).Value; - } - else - { - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection( - current.JsonPropertyInfo!.DeclaredPropertyType, - current.JsonPropertyInfo.ParentClassType, - current.JsonPropertyInfo.PropertyInfo); - } - } - - Debug.Assert(key != null); - - if (Options.DictionaryKeyPolicy != null) - { - // We should not be in the Nullable-value implementation branch for extension data. - // (TValue should be typeof(object) or typeof(JsonElement)). - Debug.Assert(current.ExtensionDataStatus != ExtensionDataWriteStatus.Writing); - - key = Options.DictionaryKeyPolicy.ConvertName(key); - - if (key == null) - { - ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(Options.DictionaryKeyPolicy.GetType()); - } - } - - if (value == null) - { - writer.WriteNull(key); - } - else - { - writer.WritePropertyName(key); - Converter.Write(writer, value.GetValueOrDefault(), Options); - } - } - - protected override void OnWriteEnumerable(ref WriteStackFrame current, Utf8JsonWriter writer) - { - if (Converter != null) - { - Debug.Assert(current.CollectionEnumerator != null); - - TProperty? value; - if (current.CollectionEnumerator is IEnumerator enumerator) - { - // Avoid boxing for strongly-typed enumerators such as returned from IList. - value = enumerator.Current; - } - else - { - value = (TProperty?)current.CollectionEnumerator.Current; - } - - if (value == null) - { - writer.WriteNullValue(); - } - else - { - Converter.Write(writer, value.GetValueOrDefault(), Options); - } - } - } - - public override Type GetDictionaryConcreteType() - { - return typeof(Dictionary); - } - - public override void GetDictionaryKeyAndValueFromGenericDictionary(ref WriteStackFrame writeStackFrame, out string key, out object? value) - { - if (writeStackFrame.CollectionEnumerator is IEnumerator> genericEnumerator) - { - key = genericEnumerator.Current.Key; - value = genericEnumerator.Current.Value; - } - else - { - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection( - writeStackFrame.JsonPropertyInfo!.DeclaredPropertyType, - writeStackFrame.JsonPropertyInfo.ParentClassType, - writeStackFrame.JsonPropertyInfo.PropertyInfo); - } - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfTConverter.cs new file mode 100644 index 00000000000000..252f8446c3882b --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfTConverter.cs @@ -0,0 +1,189 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Reflection; +using System.Text.Json.Serialization; + +namespace System.Text.Json +{ + /// + /// Represents a strongly-typed property to prevent boxing and to create a direct delegate to the getter\setter. + /// + internal sealed class JsonPropertyInfo : JsonPropertyInfo + { + public Func? Get { get; private set; } + public Action? Set { get; private set; } + + public JsonConverter Converter { get; internal set; } = null!; + + public override void Initialize( + Type parentClassType, + Type declaredPropertyType, + Type? runtimePropertyType, + ClassType runtimeClassType, + PropertyInfo? propertyInfo, + JsonConverter converter, + JsonSerializerOptions options) + { + base.Initialize( + parentClassType, + declaredPropertyType, + runtimePropertyType, + runtimeClassType, + propertyInfo, + converter, + options); + + if (propertyInfo != null) + { + if (propertyInfo.GetMethod?.IsPublic == true) + { + HasGetter = true; + Get = options.MemberAccessorStrategy.CreatePropertyGetter(propertyInfo); + } + + if (propertyInfo.SetMethod?.IsPublic == true) + { + HasSetter = true; + Set = options.MemberAccessorStrategy.CreatePropertySetter(propertyInfo); + } + } + else + { + IsPropertyPolicy = true; + HasGetter = true; + HasSetter = true; + } + + GetPolicies(); + } + + public override JsonConverter ConverterBase + { + get + { + return Converter; + } + set + { + Debug.Assert(value is JsonConverter); + Converter = (JsonConverter)value; + } + } + + public override object? GetValueAsObject(object obj) + { + if (IsPropertyPolicy) + { + return obj; + } + + Debug.Assert(HasGetter); + return Get!(obj); + } + + public override bool GetMemberAndWriteJson(object obj, ref WriteStack state, Utf8JsonWriter writer) + { + Debug.Assert(EscapedName.HasValue); + + bool success; + TConverter value = Get!(obj); + if (value == null) + { + if (!IgnoreNullValues) + { + writer.WriteNull(EscapedName.Value); + } + + success = true; + } + else + { + if (state.Current.PropertyState < StackFramePropertyState.Name) + { + state.Current.PropertyState = StackFramePropertyState.Name; + writer.WritePropertyName(EscapedName.Value); + } + + success = Converter.TryWrite(writer, value, Options, ref state); + } + + return success; + } + + public override bool GetMemberAndWriteJsonExtensionData(object obj, ref WriteStack state, Utf8JsonWriter writer) + { + bool success; + TConverter value = Get!(obj); + + if (value == null) + { + success = true; + } + else + { + state.Current.PolymorphicJsonPropertyInfo = state.Current.DeclaredJsonPropertyInfo!.RuntimeClassInfo.ElementClassInfo!.PolicyProperty; + success = Converter.TryWriteDataExtensionProperty(writer, value, Options, ref state); + } + + return success; + } + + public override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref Utf8JsonReader reader) + { + bool success; + bool isNullToken = reader.TokenType == JsonTokenType.Null; + if (isNullToken && !Converter.HandleNullValue && !state.IsContinuation) + { + if (!IgnoreNullValues) + { + TConverter value = default!; + Set!(obj, value); + } + + success = true; + } + else + { + // Get the value from the converter and set the property. + if (Converter.CanUseDirectReadOrWrite) + { + // Optimize for internal converters by avoiding the extra call to TryRead. + TConverter fastvalue = Converter.Read(ref reader, RuntimePropertyType!, Options); + if (!IgnoreNullValues || (!isNullToken && fastvalue != null)) + { + Set!(obj, fastvalue); + } + + return true; + } + else + { + success = Converter.TryRead(ref reader, RuntimePropertyType!, Options, ref state, out TConverter value); + if (success) + { + if (!IgnoreNullValues || (!isNullToken && value != null)) + { + Set!(obj, value); + } + } + } + } + + return success; + } + + public override void SetValueAsObject(object obj, object? value) + { + Debug.Assert(HasSetter); + TConverter typedValue = (TConverter)value!; + + if (typedValue != null || !IgnoreNullValues) + { + Set!(obj, typedValue); + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs new file mode 100644 index 00000000000000..205b45f3c2aa88 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Text.Json.Serialization +{ + /// + /// Base class for converters that are able to resume after reading or writing to a buffer. + /// This is used when the Stream-based serialization APIs are used. + /// + /// + internal abstract class JsonResumableConverter : JsonConverter + { + public override sealed T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Bridge from resumable to value converters. + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + ReadStack state = default; + state.InitializeRoot(typeToConvert, options); + TryRead(ref reader, typeToConvert, options, ref state, out T value); + return value; + } + + public override sealed void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + // Bridge from resumable to value converters. + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + WriteStack state = default; + state.InitializeRoot(typeof(T), options, supportContinuation: false); + TryWrite(writer, value, options, ref state); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs deleted file mode 100644 index 136c555e281302..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs +++ /dev/null @@ -1,347 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Text.Json.Serialization.Converters; - -namespace System.Text.Json -{ - public static partial class JsonSerializer - { - private static void HandleStartArray(JsonSerializerOptions options, ref ReadStack state) - { - if (state.Current.SkipProperty) - { - // The array is not being applied to the object. - state.Push(); - state.Current.Drain = true; - return; - } - Debug.Assert(state.Current.JsonClassInfo != null); - JsonPropertyInfo? jsonPropertyInfo = state.Current.JsonPropertyInfo; - if (jsonPropertyInfo == null) - { - jsonPropertyInfo = state.Current.JsonClassInfo.CreateRootProperty(options); - } - - // Verify that we are processing a valid enumerable or dictionary. - if (((ClassType.Enumerable | ClassType.Dictionary) & jsonPropertyInfo.ClassType) == 0) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(jsonPropertyInfo.RuntimePropertyType); - } - - if (state.Current.CollectionPropertyInitialized) - { - // An array nested in a dictionary or array, so push a new stack frame. - Type elementType = jsonPropertyInfo.ElementClassInfo!.Type; - - state.Push(); - state.Current.Initialize(elementType, options); - } - - state.Current.CollectionPropertyInitialized = true; - - // The current JsonPropertyInfo will be null if the current type is not one of - // ClassType.Value | ClassType.Enumerable | ClassType.Dictionary. - // We should not see ClassType.Value here because we handle it on a different code - // path invoked in the main read loop. - // Only ClassType.Enumerable is valid here since we just saw a StartArray token. - if (state.Current.JsonPropertyInfo == null || - state.Current.JsonPropertyInfo.ClassType != ClassType.Enumerable) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.JsonClassInfo.Type); - } - - // Set or replace the existing enumerable value. - object? value = ReadStackFrame.CreateEnumerableValue(ref state); - - // If value is not null, then we don't have a converter so apply the value. - if (value != null) - { - state.Current.DetermineEnumerablePopulationStrategy(value); - - if (state.Current.ReturnValue != null) - { - state.Current.JsonPropertyInfo!.SetValueAsObject(state.Current.ReturnValue, value); - } - else - { - state.Current.ReturnValue = value; - } - } - } - - private static bool HandleEndArray( - JsonSerializerOptions options, - ref ReadStack state) - { - bool lastFrame = state.IsLastFrame; - - if (state.Current.Drain) - { - // The array is not being applied to the object. - state.Pop(); - return lastFrame; - } - - IEnumerable? value = ReadStackFrame.GetEnumerableValue(ref state.Current); - bool setPropertyDirectly = false; - - if (state.Current.TempEnumerableValues != null) - { - Debug.Assert(state.Current.JsonPropertyInfo != null); - // We have a converter; possibilities: - // - Add value to current frame's current property or TempEnumerableValues. - // - Add value to previous frame's current property or TempEnumerableValues. - // - Set current property on current frame to value. - // - Set current property on previous frame to value. - // - Set ReturnValue if root frame and value is the actual return value. - JsonEnumerableConverter? converter = state.Current.JsonPropertyInfo.EnumerableConverter; - Debug.Assert(converter != null); - - value = converter.CreateFromList(ref state, (IList)value!, options); - state.Current.TempEnumerableValues = null; - - // Since we used a converter, we just processed an array or an immutable collection. This means we created a new enumerable object. - // If we are processing an enumerable property, replace the current value of the property with the new instance. - if (state.Current.IsProcessingProperty(ClassType.Enumerable)) - { - setPropertyDirectly = true; - } - } - else if (state.Current.IsProcessingProperty(ClassType.Enumerable)) - { - // We added the items to the list already. - state.Current.EndProperty(); - return false; - } - - if (lastFrame) - { - if (state.Current.ReturnValue == null) - { - // Returning a converted list or object. - state.Current.Reset(); - state.Current.ReturnValue = value; - return true; - } - else if (state.Current.IsProcessingCollectionObject()) - { - // Returning a non-converted list. - return true; - } - // else there must be an outer object, so we'll return false here. - } - else if (state.Current.IsProcessingObject(ClassType.Enumerable)) - { - state.Pop(); - } - - ApplyObjectToEnumerable(value, ref state, setPropertyDirectly); - return false; - } - - // If this method is changed, also change ApplyValueToEnumerable. - internal static void ApplyObjectToEnumerable( - object? value, - ref ReadStack state, - bool setPropertyDirectly = false) - { - Debug.Assert(!state.Current.SkipProperty && state.Current.JsonPropertyInfo != null); - - if (state.Current.IsProcessingObject(ClassType.Enumerable)) - { - if (state.Current.TempEnumerableValues != null) - { - state.Current.TempEnumerableValues.Add(value); - } - else - { - if (state.Current.AddObjectToEnumerable == null) - { - if (state.Current.ReturnValue is IList list) - { - list.Add(value); - } - else - { - Debug.Assert(value != null); - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(value.GetType()); - return; - } - } - else - { - state.Current.JsonPropertyInfo.AddObjectToEnumerableWithReflection(state.Current.AddObjectToEnumerable, value); - } - } - } - else if (!setPropertyDirectly && state.Current.IsProcessingProperty(ClassType.Enumerable)) - { - Debug.Assert(state.Current.JsonPropertyInfo != null); - Debug.Assert(state.Current.ReturnValue != null); - - if (state.Current.TempEnumerableValues != null) - { - state.Current.TempEnumerableValues.Add(value); - } - else - { - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo; - - object? currentEnumerable = jsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue); - if (currentEnumerable == null) - { - jsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, value); - } - else if (state.Current.AddObjectToEnumerable == null) - { - ((IList)currentEnumerable).Add(value); - } - else - { - jsonPropertyInfo.AddObjectToEnumerableWithReflection(state.Current.AddObjectToEnumerable, value); - } - } - - } - else if (state.Current.IsProcessingObject(ClassType.Dictionary) || (state.Current.IsProcessingProperty(ClassType.Dictionary) && !setPropertyDirectly)) - { - string? key = state.Current.KeyName; - Debug.Assert(key != null); - - if (state.Current.TempDictionaryValues != null) - { - (state.Current.TempDictionaryValues)[key] = value; - } - else - { - Debug.Assert(state.Current.ReturnValue != null); - - object? currentDictionary = state.Current.JsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue); - - if (currentDictionary is IDictionary dict) - { - Debug.Assert(!dict.IsReadOnly); - dict[key] = value; - } - else - { - Debug.Assert(currentDictionary != null); - state.Current.JsonPropertyInfo.AddObjectToDictionary(currentDictionary, key, value); - } - } - } - else - { - Debug.Assert(state.Current.JsonPropertyInfo != null); - state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, value); - } - } - - // If this method is changed, also change ApplyObjectToEnumerable. - internal static void ApplyValueToEnumerable( - ref TProperty value, - ref ReadStack state) - { - Debug.Assert(!state.Current.SkipProperty); - - if (state.Current.IsProcessingObject(ClassType.Enumerable)) - { - if (state.Current.TempEnumerableValues != null) - { - ((IList)state.Current.TempEnumerableValues).Add(value); - } - else - { - AddValueToEnumerable(ref state, state.Current.ReturnValue, value); - } - } - else if (state.Current.IsProcessingProperty(ClassType.Enumerable)) - { - if (state.Current.TempEnumerableValues != null) - { - ((IList)state.Current.TempEnumerableValues).Add(value); - } - else - { - Debug.Assert(state.Current.JsonPropertyInfo != null); - Debug.Assert(state.Current.ReturnValue != null); - - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo; - - object? currentEnumerable = jsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue); - if (currentEnumerable == null) - { - jsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, value); - } - else - { - AddValueToEnumerable(ref state, currentEnumerable, value); - } - } - } - else if (state.Current.IsProcessingDictionary()) - { - string? key = state.Current.KeyName; - Debug.Assert(key != null); - - if (state.Current.TempDictionaryValues != null) - { - ((IDictionary)state.Current.TempDictionaryValues)[key] = value; - } - else - { - Debug.Assert(state.Current.ReturnValue != null && state.Current.JsonPropertyInfo != null); - - object? currentDictionary = state.Current.JsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue); - - if (currentDictionary is IDictionary genericDict) - { - Debug.Assert(!genericDict.IsReadOnly); - genericDict[key] = value; - } - else if (currentDictionary is IDictionary dict) - { - Debug.Assert(!dict.IsReadOnly); - dict[key] = value; - } - else - { - Debug.Assert(currentDictionary != null); - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(currentDictionary.GetType(), parentType: null, memberInfo: null); - } - } - } - else - { - Debug.Assert(state.Current.JsonPropertyInfo != null); - state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, value); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddValueToEnumerable(ref ReadStack state, object? target, TProperty value) - { - if (target is IList genericList) - { - Debug.Assert(!genericList.IsReadOnly); - genericList.Add(value); - } - else if (target is IList list) - { - Debug.Assert(!list.IsReadOnly); - list.Add(value); - } - else - { - Debug.Assert(state.Current.AddObjectToEnumerable != null); - ((Action)state.Current.AddObjectToEnumerable)(value); - } - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs deleted file mode 100644 index 03a3ac673faaad..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs +++ /dev/null @@ -1,151 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections; -using System.Diagnostics; -using System.Text.Json.Serialization.Converters; - -namespace System.Text.Json -{ - public static partial class JsonSerializer - { - private static void HandleStartDictionary(JsonSerializerOptions options, ref ReadStack state) - { - Debug.Assert(!state.Current.IsProcessingEnumerable()); - - JsonPropertyInfo? jsonPropertyInfo = state.Current.JsonPropertyInfo; - if (jsonPropertyInfo == null) - { - jsonPropertyInfo = state.Current.JsonClassInfo!.CreateRootProperty(options); - } - - Debug.Assert(jsonPropertyInfo != null); - - // A nested object or dictionary, so push new frame. - if (state.Current.CollectionPropertyInitialized) - { - state.Push(); - state.Current.JsonClassInfo = jsonPropertyInfo.ElementClassInfo!; - state.Current.InitializeJsonPropertyInfo(); - - JsonClassInfo classInfo = state.Current.JsonClassInfo; - - Debug.Assert(state.Current.IsProcessingDictionary() || state.Current.IsProcessingObject(ClassType.Object) || state.Current.IsProcessingObject(ClassType.Enumerable)); - - if (state.Current.IsProcessingDictionary()) - { - object? dictValue = ReadStackFrame.CreateDictionaryValue(ref state); - - // If value is not null, then we don't have a converter so apply the value. - if (dictValue != null) - { - state.Current.ReturnValue = dictValue; - state.Current.DetermineIfDictionaryCanBePopulated(state.Current.ReturnValue); - } - - state.Current.CollectionPropertyInitialized = true; - } - else if (state.Current.IsProcessingObject(ClassType.Object)) - { - if (classInfo.CreateObject == null) - { - ThrowHelper.ThrowNotSupportedException_DeserializeCreateObjectDelegateIsNull(classInfo.Type); - } - - state.Current.ReturnValue = classInfo.CreateObject(); - } - else if (state.Current.IsProcessingObject(ClassType.Enumerable)) - { - // Array with metadata within the dictionary. - HandleStartObjectInEnumerable(ref state, options, classInfo.Type); - - Debug.Assert(options.ReferenceHandling.ShouldReadPreservedReferences()); - Debug.Assert(state.Current.JsonClassInfo!.Type.GetGenericTypeDefinition() == typeof(JsonPreservableArrayReference<>)); - - state.Current.ReturnValue = state.Current.JsonClassInfo.CreateObject!(); - } - - return; - } - - state.Current.CollectionPropertyInitialized = true; - - object? value = ReadStackFrame.CreateDictionaryValue(ref state); - if (value != null) - { - state.Current.DetermineIfDictionaryCanBePopulated(value); - - if (state.Current.ReturnValue != null) - { - Debug.Assert(state.Current.JsonPropertyInfo != null); - state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, value); - } - else - { - // A dictionary is being returned directly, or a nested dictionary. - state.Current.ReturnValue = value; - } - } - } - - private static void HandleEndDictionary(JsonSerializerOptions options, ref ReadStack state) - { - Debug.Assert(!state.Current.SkipProperty && state.Current.JsonPropertyInfo != null); - - if (state.Current.IsProcessingProperty(ClassType.Dictionary)) - { - if (state.Current.TempDictionaryValues != null) - { - JsonDictionaryConverter? converter = state.Current.JsonPropertyInfo.DictionaryConverter; - Debug.Assert(converter != null); - state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, converter.CreateFromDictionary(ref state, state.Current.TempDictionaryValues, options)); - state.Current.EndProperty(); - } - else - { - Debug.Assert(state.Current.JsonClassInfo != null); - - // Handle special case of DataExtensionProperty where we just added a dictionary element to the extension property. - // Since the JSON value is not a dictionary element (it's a normal property in JSON) a JsonTokenType.EndObject - // encountered here is from the outer object so forward to HandleEndObject(). - if (state.Current.JsonClassInfo.DataExtensionProperty == state.Current.JsonPropertyInfo) - { - HandleEndObject(ref state); - } - else - { - // We added the items to the dictionary already. - state.Current.EndProperty(); - } - } - } - else - { - object? value; - if (state.Current.TempDictionaryValues != null) - { - JsonDictionaryConverter? converter = state.Current.JsonPropertyInfo.DictionaryConverter; - Debug.Assert(converter != null); - value = converter.CreateFromDictionary(ref state, state.Current.TempDictionaryValues, options); - } - else - { - value = state.Current.ReturnValue; - } - - if (state.IsLastFrame) - { - // Set the return value directly since this will be returned to the user. - state.Current.Reset(); - state.Current.ReturnValue = value; - } - else - { - state.Pop(); - ApplyObjectToEnumerable(value, ref state); - } - } - } - } -} 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 898b1b26e9426a..206de8d71ac19b 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 @@ -3,49 +3,208 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; +using System.Text.Json.Serialization; namespace System.Text.Json { public static partial class JsonSerializer { - private static void HandleMetadataPropertyValue(ref Utf8JsonReader reader, ref ReadStack state) + /// + /// Returns true if successful, false is the reader ran out of buffer. + /// Sets state.Current.ReturnValue to the $ref target for MetadataRefProperty cases. + /// + internal static bool ResolveMetadata( + JsonConverter converter, + ref Utf8JsonReader reader, + ref ReadStack state) { - Debug.Assert(state.Current.JsonClassInfo!.Options.ReferenceHandling.ShouldReadPreservedReferences()); - - if (reader.TokenType != JsonTokenType.String) + if (state.Current.ObjectState < StackFrameObjectState.MetadataPropertyName) { - ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType); + // Read the first metadata property name. + if (!reader.Read()) + { + return false; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + // An enumerable needs metadata since it starts with StartObject. + if (converter.ClassType == ClassType.Enumerable) + { + ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(converter.TypeToConvert); + } + + // The reader should have detected other invalid cases. + Debug.Assert(reader.TokenType == JsonTokenType.EndObject); + + // Skip the read of the first property name, since we already read it above. + state.Current.PropertyState = StackFramePropertyState.ReadName; + + return true; + } + + ReadOnlySpan propertyName = reader.GetSpan(); + MetadataPropertyName metadata = GetMetadataPropertyName(propertyName); + if (metadata == MetadataPropertyName.Id) + { + state.Current.JsonPropertyName = propertyName.ToArray(); + if (!converter.CanHaveIdMetadata) + { + ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(converter.TypeToConvert); + } + + state.Current.ObjectState = StackFrameObjectState.MetadataIdProperty; + } + else if (metadata == MetadataPropertyName.Ref) + { + state.Current.JsonPropertyName = propertyName.ToArray(); + if (converter.TypeToConvert.IsValueType) + { + ThrowHelper.ThrowJsonException_MetadataInvalidReferenceToValueType(converter.TypeToConvert); + } + + state.Current.ObjectState = StackFrameObjectState.MetadataRefProperty; + } + else if (metadata == MetadataPropertyName.Values) + { + state.Current.JsonPropertyName = propertyName.ToArray(); + if (converter.ClassType == ClassType.Enumerable) + { + ThrowHelper.ThrowJsonException_MetadataMissingIdBeforeValues(); + } + else + { + ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(propertyName, ref state, reader); + } + } + else + { + Debug.Assert(metadata == MetadataPropertyName.NoMetadata); + + // Having a StartObject without metadata properties is not allowed. + if (converter.ClassType == ClassType.Enumerable) + { + state.Current.JsonPropertyName = propertyName.ToArray(); + ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(converter.TypeToConvert, reader); + } + + // Skip the read of the first property name, since we already read it above. + state.Current.PropertyState = StackFramePropertyState.ReadName; + return true; + } } - MetadataPropertyName metadata = state.Current.LastSeenMetadataProperty; - string key = reader.GetString()!; - Debug.Assert(metadata == MetadataPropertyName.Id || metadata == MetadataPropertyName.Ref); + if (state.Current.ObjectState == StackFrameObjectState.MetadataRefProperty) + { + if (!reader.Read()) + { + return false; + } + + if (reader.TokenType != JsonTokenType.String) + { + ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType); + } + + string key = reader.GetString()!; - if (metadata == MetadataPropertyName.Id) + // todo: verify value is converter.TypeToConvert and throw JsonException? (currently no test) + state.Current.ReturnValue = state.ReferenceResolver.ResolveReferenceOnDeserialize(key); + state.Current.ObjectState = StackFrameObjectState.MetadataRefPropertyEndObject; + } + else if (state.Current.ObjectState == StackFrameObjectState.MetadataIdProperty) { - // Special case for dictionary properties since those do not push into the ReadStack. - // There is no need to check for enumerables since those will always be wrapped into JsonPreservableArrayReference which turns enumerables into objects. - object value = state.Current.IsProcessingProperty(ClassType.Dictionary) ? - state.Current.JsonPropertyInfo!.GetValueAsObject(state.Current.ReturnValue)! : - state.Current.ReturnValue!; + if (!reader.Read()) + { + return false; + } + + if (reader.TokenType != JsonTokenType.String) + { + ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType); + } - state.ReferenceResolver.AddReferenceOnDeserialize(key, value); + state.Current.MetadataId = reader.GetString(); + + // Clear the MetadataPropertyName since we are done processing Id. + state.Current.JsonPropertyName = default; + + if (converter.ClassType == ClassType.Enumerable) + { + // Need to Read $values property name. + state.Current.ObjectState = StackFrameObjectState.MetadataValuesPropertyName; + } + else + { + // We are done reading metadata. + state.Current.ObjectState = StackFrameObjectState.MetadataPropertyValue; + return true; + } } - else if (metadata == MetadataPropertyName.Ref) + + if (state.Current.ObjectState == StackFrameObjectState.MetadataRefPropertyEndObject) { - state.Current.ReferenceId = key; + if (!reader.Read()) + { + return false; + } + + if (reader.TokenType != JsonTokenType.EndObject) + { + // We just read a property. The only valid next tokens are EndObject and PropertyName. + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + + ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(reader.GetSpan(), ref state); + } + + return true; } - } - private static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan propertyName, ref ReadStack state, ref Utf8JsonReader reader) - { - Debug.Assert(state.Current.JsonClassInfo!.Options.ReferenceHandling.ShouldReadPreservedReferences()); + if (state.Current.ObjectState == StackFrameObjectState.MetadataValuesPropertyName) + { + if (!reader.Read()) + { + return false; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(converter.TypeToConvert); + } - if (state.Current.ReferenceId != null) + ReadOnlySpan propertyName = reader.GetSpan(); + + // Remember the property in case we get an exception. + state.Current.JsonPropertyName = propertyName.ToArray(); + + if (GetMetadataPropertyName(propertyName) != MetadataPropertyName.Values) + { + ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(converter.TypeToConvert, reader); + } + + state.Current.ObjectState = StackFrameObjectState.MetadataValuesPropertyStartArray; + } + + if (state.Current.ObjectState == StackFrameObjectState.MetadataValuesPropertyStartArray) { - ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(); + if (!reader.Read()) + { + return false; + } + + if (reader.TokenType != JsonTokenType.StartArray) + { + ThrowHelper.ThrowJsonException_MetadataValuesInvalidToken(reader.TokenType); + } + + state.Current.ObjectState = StackFrameObjectState.MetadataPropertyValue; } + return true; + } + + internal static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan propertyName) + { if (propertyName.Length > 0 && propertyName[0] == '$') { switch (propertyName.Length) @@ -68,9 +227,7 @@ private static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan p break; case 7: - // Only enumerables wrapped in JsonPreservableArrayReference are allowed to understand $values as metadata. - if (state.Current.IsPreservedArray && - propertyName[1] == 'v' && + if (propertyName[1] == 'v' && propertyName[2] == 'a' && propertyName[3] == 'l' && propertyName[4] == 'u' && @@ -81,44 +238,9 @@ private static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan p } break; } - - ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(propertyName, ref state, in reader); } return MetadataPropertyName.NoMetadata; } - - private static void HandleReference(ref ReadStack state) - { - Debug.Assert(state.Current.JsonClassInfo!.Options.ReferenceHandling.ShouldReadPreservedReferences()); - - object referenceValue = state.ReferenceResolver.ResolveReferenceOnDeserialize(state.Current.ReferenceId!); - if (state.Current.IsProcessingProperty(ClassType.Dictionary)) - { - ApplyObjectToEnumerable(referenceValue, ref state, setPropertyDirectly: true); - state.Current.EndProperty(); - } - else - { - state.Current.ReturnValue = referenceValue; - HandleEndObject(ref state); - } - - // Set back to null to no longer treat subsequent objects as references. - state.Current.ReferenceId = null; - } - - internal static JsonPropertyInfo GetValuesPropertyInfoFromJsonPreservableArrayRef(ref ReadStackFrame current) - { - Debug.Assert(current.JsonClassInfo!.Options.ReferenceHandling.ShouldReadPreservedReferences()); - Debug.Assert(current.JsonClassInfo.Type.GetGenericTypeDefinition() == typeof(JsonPreservableArrayReference<>)); - - JsonPropertyInfo info = current.JsonClassInfo.PropertyCacheArray![0]; - - Debug.Assert(info == current.JsonClassInfo.PropertyCache!["Values"]); - Debug.Assert(info.ClassType == ClassType.Enumerable); - - return info; - } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleNull.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleNull.cs deleted file mode 100644 index 07c81295facd33..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleNull.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Diagnostics; - -namespace System.Text.Json -{ - public static partial class JsonSerializer - { - private static bool HandleNull(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) - { - if (state.Current.SkipProperty) - { - // Clear the current property in case it is a dictionary, since dictionaries must have EndProperty() called when completed. - // A non-dictionary property can also have EndProperty() called when completed, although it is redundant. - state.Current.EndProperty(); - - return false; - } - - JsonPropertyInfo? jsonPropertyInfo = state.Current.JsonPropertyInfo; - - if (jsonPropertyInfo == null || (reader.CurrentDepth == 0 && jsonPropertyInfo.CanBeNull)) - { - Debug.Assert(state.IsLastFrame); - Debug.Assert(state.Current.ReturnValue == null); - return true; - } - - if (state.Current.IsProcessingCollectionObject()) - { - AddNullToCollection(jsonPropertyInfo, ref reader, ref state); - return false; - } - - if (state.Current.IsProcessingCollectionProperty()) - { - if (state.Current.CollectionPropertyInitialized) - { - // Add the element. - AddNullToCollection(jsonPropertyInfo, ref reader, ref state); - } - else - { - // Set the property to null. - ApplyObjectToEnumerable(null, ref state, setPropertyDirectly: true); - - // Reset so that `Is*Property` no longer returns true - state.Current.EndProperty(); - } - - return false; - } - - if (!jsonPropertyInfo.CanBeNull) - { - // Allow a value type converter to return a null value representation, such as JsonElement. - // Most likely this will throw JsonException. - jsonPropertyInfo.Read(JsonTokenType.Null, ref state, ref reader); - return false; - } - - if (state.Current.ReturnValue == null) - { - Debug.Assert(state.IsLastFrame); - return true; - } - - if (!jsonPropertyInfo.IgnoreNullValues) - { - jsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, value: null); - } - - return false; - } - - private static void AddNullToCollection(JsonPropertyInfo jsonPropertyInfo, ref Utf8JsonReader reader, ref ReadStack state) - { - JsonPropertyInfo? elementPropertyInfo = jsonPropertyInfo.ElementClassInfo!.PolicyProperty; - - // if elementPropertyInfo == null then this element doesn't need a converter (an object). - if (elementPropertyInfo?.CanBeNull == false) - { - // Allow a value type converter to return a null value representation. - // Most likely this will throw JsonException unless the converter has special logic (like converter for JsonElement). - elementPropertyInfo.ReadEnumerable(JsonTokenType.Null, ref state, ref reader); - } - else - { - // Assume collection types are reference types and can have null assigned. - ApplyObjectToEnumerable(null, ref state); - } - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleObject.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleObject.cs deleted file mode 100644 index 8791858f102f6d..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleObject.cs +++ /dev/null @@ -1,176 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace System.Text.Json -{ - public static partial class JsonSerializer - { - private static void HandleStartObject(JsonSerializerOptions options, ref ReadStack state) - { - Debug.Assert(!state.Current.IsProcessingDictionary()); - - // Note: unless we are a root object, we are going to push a property onto the ReadStack - // in the if/else if check below. - - if (state.Current.IsProcessingEnumerable()) - { - // A nested object within an enumerable (non-dictionary). - HandleStartObjectInEnumerable(ref state, options, state.Current.JsonPropertyInfo!.DeclaredPropertyType); - } - else if (state.Current.JsonPropertyInfo != null) - { - // Nested object within an object. - Debug.Assert(state.Current.IsProcessingObject(ClassType.Object)); - - Type objType = state.Current.JsonPropertyInfo.RuntimePropertyType; - state.Push(); - state.Current.Initialize(objType, options); - } - - JsonClassInfo classInfo = state.Current.JsonClassInfo!; - - Debug.Assert(state.Current.IsProcessingObject(ClassType.Dictionary) || state.Current.IsProcessingObject(ClassType.Object) || state.Current.IsProcessingObject(ClassType.Enumerable)); - - if (state.Current.IsProcessingObject(ClassType.Dictionary)) - { - object? value = ReadStackFrame.CreateDictionaryValue(ref state); - - // If value is not null, then we don't have a converter so apply the value. - if (value != null) - { - state.Current.ReturnValue = value; - state.Current.DetermineIfDictionaryCanBePopulated(state.Current.ReturnValue); - } - - state.Current.CollectionPropertyInitialized = true; - } - else if (state.Current.IsProcessingObject(ClassType.Object)) - { - if (classInfo.CreateObject == null) - { - ThrowHelper.ThrowNotSupportedException_DeserializeCreateObjectDelegateIsNull(classInfo.Type); - } - - state.Current.ReturnValue = classInfo.CreateObject(); - } - else if (state.Current.IsProcessingObject(ClassType.Enumerable)) - { - // Nested array with metadata within another array with metadata. - HandleStartObjectInEnumerable(ref state, options, classInfo.Type); - - Debug.Assert(options.ReferenceHandling.ShouldReadPreservedReferences()); - Debug.Assert(state.Current.JsonClassInfo!.Type.GetGenericTypeDefinition() == typeof(JsonPreservableArrayReference<>)); - - state.Current.ReturnValue = state.Current.JsonClassInfo.CreateObject!(); - state.Current.IsNestedPreservedArray = true; - } - } - - private static void HandleEndObject(ref ReadStack state) - { - Debug.Assert(state.Current.JsonClassInfo != null); - - // Only allow dictionaries to be processed here if this is the DataExtensionProperty or if the dictionary is a preserved reference. - Debug.Assert(!state.Current.IsProcessingDictionary() || - state.Current.JsonClassInfo.DataExtensionProperty == state.Current.JsonPropertyInfo || - (state.Current.IsProcessingObject(ClassType.Dictionary) && state.Current.ReferenceId != null)); - - // Check if we are trying to build the sorted cache. - if (state.Current.PropertyRefCache != null) - { - state.Current.JsonClassInfo.UpdateSortedPropertyCache(ref state.Current); - } - - object? value; - // Used for ReferenceHandling.Preserve - if (state.Current.IsPreservedArray) - { - value = GetPreservedArrayValue(ref state); - } - else - { - value = state.Current.ReturnValue; - } - - if (state.IsLastFrame) - { - state.Current.Reset(); - state.Current.ReturnValue = value; - } - else - { - // Set directly when handling non-nested preserved array - bool setPropertyDirectly = state.Current.IsPreservedArray && !state.Current.IsNestedPreservedArray; - state.Pop(); - - ApplyObjectToEnumerable(value, ref state, setPropertyDirectly); - } - } - - private static object GetPreservedArrayValue(ref ReadStack state) - { - JsonPropertyInfo info = GetValuesPropertyInfoFromJsonPreservableArrayRef(ref state.Current); - object? value = info.GetValueAsObject(state.Current.ReturnValue); - - if (value == null) - { - ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(info.DeclaredPropertyType); - } - - return value; - } - - private static void HandleStartPreservedArray(ref ReadStack state, JsonSerializerOptions options) - { - // Check we are not parsing into an immutable list or array. - if (state.Current.JsonPropertyInfo!.EnumerableConverter != null) - { - ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(state.Current.JsonPropertyInfo.DeclaredPropertyType); - } - Type preservedObjType = state.Current.JsonPropertyInfo.GetJsonPreservableArrayReferenceType(); - if (state.Current.IsProcessingProperty(ClassType.Enumerable)) - { - state.Push(); - state.Current.Initialize(preservedObjType, options); - } - else - { - // For array objects, we don't need to Push a new frame to the stack, - // so we just call Initialize again passing the wrapper class - // since we are going to handle the array at the moment we step into JsonPreservableArrayReference.Values. - state.Current.Initialize(preservedObjType, options); - } - - state.Current.IsPreservedArray = true; - } - - [PreserveDependency("get_Values", "System.Text.Json.JsonPreservableArrayReference`1")] - [PreserveDependency("set_Values", "System.Text.Json.JsonPreservableArrayReference`1")] - [PreserveDependency(".ctor()", "System.Text.Json.JsonPreservableArrayReference`1")] - private static void HandleStartObjectInEnumerable(ref ReadStack state, JsonSerializerOptions options, Type type) - { - if (!state.Current.CollectionPropertyInitialized) - { - if (options.ReferenceHandling.ShouldReadPreservedReferences()) - { - HandleStartPreservedArray(ref state, options); - } - else - { - // We have bad JSON: enumerable element appeared without preceding StartArray token. - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(type); - } - } - else - { - Type objType = state.Current.GetElementType(); - state.Push(); - state.Current.Initialize(objType, options); - } - } - } -} 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 f7d4c5014d9854..7ecdf4c8396d35 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 @@ -2,151 +2,111 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Buffers; using System.Collections; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; namespace System.Text.Json { public static partial class JsonSerializer { - // AggressiveInlining used although a large method it is only called from one locations and is on a hot path. + /// + /// Lookup the property given its name (obtained from the reader) and return it. + /// Also sets state.Current.JsonPropertyInfo to a non-null value. + /// + // AggressiveInlining used although a large method it is only called from two locations and is on a hot path. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void HandlePropertyName( - JsonSerializerOptions options, + internal static JsonPropertyInfo LookupProperty( + object obj, ref Utf8JsonReader reader, - ref ReadStack state) + JsonSerializerOptions options, + ref ReadStack state, + out bool useExtensionProperty) { - if (state.Current.Drain) - { - return; - } + Debug.Assert(state.Current.JsonClassInfo.ClassType == ClassType.Object); - Debug.Assert(state.Current.ReturnValue != null || state.Current.TempDictionaryValues != null); - Debug.Assert(state.Current.JsonClassInfo != null); + JsonPropertyInfo jsonPropertyInfo; - bool isProcessingDictObject = state.Current.IsProcessingObject(ClassType.Dictionary); - if ((isProcessingDictObject || state.Current.IsProcessingProperty(ClassType.Dictionary)) && - state.Current.JsonClassInfo.DataExtensionProperty != state.Current.JsonPropertyInfo) - { - if (isProcessingDictObject) - { - state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.PolicyProperty; - } - - if (options.ReferenceHandling.ShouldReadPreservedReferences()) - { - ReadOnlySpan propertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; - MetadataPropertyName metadata = GetMetadataPropertyName(propertyName, ref state, ref reader); - ResolveMetadataOnDictionary(metadata, ref state); - - state.Current.LastSeenMetadataProperty = metadata; - } + ReadOnlySpan unescapedPropertyName; + ReadOnlySpan propertyName = reader.GetSpan(); - state.Current.KeyName = reader.GetString(); + if (reader._stringHasEscaping) + { + int idx = propertyName.IndexOf(JsonConstants.BackSlash); + Debug.Assert(idx != -1); + unescapedPropertyName = GetUnescapedString(propertyName, idx); } else { - Debug.Assert(state.Current.JsonClassInfo.ClassType == ClassType.Object); - - state.Current.EndProperty(); + unescapedPropertyName = propertyName; + } - ReadOnlySpan propertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; - if (options.ReferenceHandling.ShouldReadPreservedReferences()) - { - MetadataPropertyName metadata = GetMetadataPropertyName(propertyName, ref state, ref reader); - - if (metadata == MetadataPropertyName.NoMetadata) - { - if (state.Current.IsPreservedArray) - { - ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(in reader, ref state); - } - - HandlePropertyNameDefault(propertyName, ref state, ref reader, options); - } - else - { - ResolveMetadataOnObject(metadata, ref state); - } - - state.Current.LastSeenMetadataProperty = metadata; - } - else + if (options.ReferenceHandling.ShouldReadPreservedReferences()) + { + if (propertyName.Length > 0 && propertyName[0] == '$') { - HandlePropertyNameDefault(propertyName, ref state, ref reader, options); + ThrowHelper.ThrowUnexpectedMetadataException(propertyName, ref reader, ref state); } } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void HandlePropertyNameDefault(ReadOnlySpan propertyName, ref ReadStack state, ref Utf8JsonReader reader, JsonSerializerOptions options) - { - if (reader._stringHasEscaping) - { - int idx = propertyName.IndexOf(JsonConstants.BackSlash); - Debug.Assert(idx != -1); - propertyName = GetUnescapedString(propertyName, idx); - } + jsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(unescapedPropertyName, ref state.Current); + + // Increment PropertyIndex so GetProperty() starts with the next property the next time this function is called. + state.Current.PropertyIndex++; - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo!.GetProperty(propertyName, ref state.Current); + // Determine if we should use the extension property. if (jsonPropertyInfo == JsonPropertyInfo.s_missingProperty) { - JsonPropertyInfo? dataExtProperty = state.Current.JsonClassInfo!.DataExtensionProperty; - if (dataExtProperty == null) + JsonPropertyInfo? dataExtProperty = state.Current.JsonClassInfo.DataExtensionProperty; + if (dataExtProperty != null) { - state.Current.JsonPropertyInfo = JsonPropertyInfo.s_missingProperty; + state.Current.JsonPropertyNameAsString = JsonHelpers.Utf8GetString(unescapedPropertyName); + CreateDataExtensionProperty(obj, dataExtProperty); + jsonPropertyInfo = dataExtProperty; } - else - { - state.Current.JsonPropertyInfo = dataExtProperty; - state.Current.JsonPropertyName = propertyName.ToArray(); - state.Current.KeyName = JsonHelpers.Utf8GetString(propertyName); - state.Current.CollectionPropertyInitialized = true; - CreateDataExtensionProperty(dataExtProperty, ref state); - } + state.Current.JsonPropertyInfo = jsonPropertyInfo; + useExtensionProperty = true; + return jsonPropertyInfo; } - else - { - // Support JsonException.Path. - Debug.Assert( - jsonPropertyInfo.JsonPropertyName == null || - options.PropertyNameCaseInsensitive || - propertyName.SequenceEqual(jsonPropertyInfo.JsonPropertyName)); - state.Current.JsonPropertyInfo = jsonPropertyInfo; + // Support JsonException.Path. + Debug.Assert( + jsonPropertyInfo.JsonPropertyName == null || + options.PropertyNameCaseInsensitive || + unescapedPropertyName.SequenceEqual(jsonPropertyInfo.JsonPropertyName)); - if (jsonPropertyInfo.JsonPropertyName == null) + state.Current.JsonPropertyInfo = jsonPropertyInfo; + + if (jsonPropertyInfo.JsonPropertyName == null) + { + byte[] propertyNameArray = unescapedPropertyName.ToArray(); + if (options.PropertyNameCaseInsensitive) { - byte[] propertyNameArray = propertyName.ToArray(); - if (options.PropertyNameCaseInsensitive) - { - // Each payload can have a different name here; remember the value on the temporary stack. - state.Current.JsonPropertyName = propertyNameArray; - } - else - { - // Prevent future allocs by caching globally on the JsonPropertyInfo which is specific to a Type+PropertyName - // so it will match the incoming payload except when case insensitivity is enabled (which is handled above). - state.Current.JsonPropertyInfo.JsonPropertyName = propertyNameArray; - } + // Each payload can have a different name here; remember the value on the temporary stack. + state.Current.JsonPropertyName = propertyNameArray; + } + else + { + // Prevent future allocs by caching globally on the JsonPropertyInfo which is specific to a Type+PropertyName + // so it will match the incoming payload except when case insensitivity is enabled (which is handled above). + state.Current.JsonPropertyInfo.JsonPropertyName = propertyNameArray; } } - // Increment the PropertyIndex so JsonClassInfo.GetProperty() starts with the next property. - state.Current.PropertyIndex++; + state.Current.JsonPropertyInfo = jsonPropertyInfo; + useExtensionProperty = false; + return jsonPropertyInfo; } private static void CreateDataExtensionProperty( - JsonPropertyInfo jsonPropertyInfo, - ref ReadStack state) + object obj, + JsonPropertyInfo jsonPropertyInfo) { Debug.Assert(jsonPropertyInfo != null); - Debug.Assert(state.Current.ReturnValue != null); - IDictionary? extensionData = (IDictionary?)jsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue); + IDictionary? extensionData = (IDictionary?)jsonPropertyInfo.GetValueAsObject(obj); if (extensionData == null) { // Create the appropriate dictionary type. We already verified the types. @@ -159,75 +119,10 @@ private static void CreateDataExtensionProperty( Debug.Assert(jsonPropertyInfo.RuntimeClassInfo.CreateObject != null); extensionData = (IDictionary?)jsonPropertyInfo.RuntimeClassInfo.CreateObject(); - jsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, extensionData); + jsonPropertyInfo.SetValueAsObject(obj, extensionData); } // We don't add the value to the dictionary here because we need to support the read-ahead functionality for Streams. } - - private static void ResolveMetadataOnDictionary(MetadataPropertyName metadata, ref ReadStack state) - { - if (metadata == MetadataPropertyName.Id) - { - // Check we are not parsing into an immutable dictionary. - if (state.Current.JsonPropertyInfo!.DictionaryConverter != null) - { - ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(state.Current.JsonPropertyInfo.DeclaredPropertyType); - } - - if (state.Current.KeyName != null) - { - ThrowHelper.ThrowJsonException_MetadataIdIsNotFirstProperty_Dictionary(ref state.Current); - } - } - else if (metadata == MetadataPropertyName.Ref) - { - if (state.Current.KeyName != null) - { - ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties_Dictionary(ref state.Current); - } - } - } - - private static void ResolveMetadataOnObject(MetadataPropertyName metadata, ref ReadStack state) - { - if (metadata == MetadataPropertyName.Id) - { - if (state.Current.PropertyIndex > 0 || state.Current.LastSeenMetadataProperty != MetadataPropertyName.NoMetadata) - { - ThrowHelper.ThrowJsonException_MetadataIdIsNotFirstProperty(); - } - - state.Current.JsonPropertyName = ReadStack.s_idMetadataPropertyName; - } - else if (metadata == MetadataPropertyName.Values) - { - JsonPropertyInfo info = GetValuesPropertyInfoFromJsonPreservableArrayRef(ref state.Current); - state.Current.JsonPropertyName = ReadStack.s_valuesMetadataPropertyName; - state.Current.JsonPropertyInfo = info; - - // Throw after setting JsonPropertyName to show the correct JSON Path. - if (state.Current.LastSeenMetadataProperty != MetadataPropertyName.Id) - { - ThrowHelper.ThrowJsonException_MetadataMissingIdBeforeValues(); - } - } - else - { - Debug.Assert(metadata == MetadataPropertyName.Ref); - - if (state.Current.JsonClassInfo!.Type.IsValueType) - { - ThrowHelper.ThrowJsonException_MetadataInvalidReferenceToValueType(state.Current.JsonClassInfo.Type); - } - - if (state.Current.PropertyIndex > 0 || state.Current.LastSeenMetadataProperty != MetadataPropertyName.NoMetadata) - { - ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(); - } - - state.Current.JsonPropertyName = ReadStack.s_refMetadataPropertyName; - } - } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs deleted file mode 100644 index 2650b838aabfe4..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace System.Text.Json -{ - public static partial class JsonSerializer - { - // AggressiveInlining used although a large method it is only called from two locations and is on a hot path. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void HandleValue(JsonTokenType tokenType, JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) - { - if (state.Current.SkipProperty) - { - return; - } - - if (state.Current.LastSeenMetadataProperty == MetadataPropertyName.Id || state.Current.LastSeenMetadataProperty == MetadataPropertyName.Ref) - { - Debug.Assert(options.ReferenceHandling.ShouldReadPreservedReferences()); - - HandleMetadataPropertyValue(ref reader, ref state); - return; - } - - JsonPropertyInfo? jsonPropertyInfo = state.Current.JsonPropertyInfo; - Debug.Assert(state.Current.JsonClassInfo != null); - if (jsonPropertyInfo == null) - { - jsonPropertyInfo = state.Current.JsonClassInfo.CreateRootProperty(options); - } - else if (state.Current.JsonClassInfo.ClassType == ClassType.Unknown) - { - jsonPropertyInfo = state.Current.JsonClassInfo.GetOrAddPolymorphicProperty(jsonPropertyInfo, typeof(object), options); - } - - jsonPropertyInfo.Read(tokenType, ref state, ref reader); - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs index 0f2339ac9ed61d..1c5113b3f8bef3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Text.Json.Serialization; - namespace System.Text.Json { public static partial class JsonSerializer @@ -14,11 +12,7 @@ public static partial class JsonSerializer ref Utf8JsonReader reader) { ReadStack state = default; - if (options.ReferenceHandling.ShouldReadPreservedReferences()) - { - state.ReferenceResolver = new DefaultReferenceResolver(writing: false); - } - state.Current.Initialize(returnType, options); + state.InitializeRoot(returnType, options); ReadCore(options, ref reader, ref state); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs index 008bef29aacff9..3f2234a56be387 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs @@ -84,13 +84,11 @@ private static async ValueTask ReadAsync( options = JsonSerializerOptions.s_defaultOptions; } - ReadStack readStack = default; - if (options.ReferenceHandling.ShouldReadPreservedReferences()) - { - readStack.ReferenceResolver = new DefaultReferenceResolver(writing: false); - } + ReadStack state = default; + state.InitializeRoot(returnType, options); - readStack.Current.Initialize(returnType, options); + // Ensures converters support contination due to having to re-populate the buffer from a Stream. + state.SupportContinuation = true; var readerState = new JsonReaderState(options.GetReaderOptions()); @@ -100,7 +98,7 @@ private static async ValueTask ReadAsync( int bytesInBuffer = 0; long totalBytesRead = 0; int clearMax = 0; - bool firstIteration = true; + bool isFirstIteration = true; try { @@ -141,9 +139,10 @@ private static async ValueTask ReadAsync( } int start = 0; - if (firstIteration) + if (isFirstIteration) { - firstIteration = false; + isFirstIteration = false; + // Handle the UTF-8 BOM if present Debug.Assert(buffer.Length >= JsonConstants.Utf8Bom.Length); if (buffer.AsSpan().StartsWith(JsonConstants.Utf8Bom)) @@ -159,10 +158,10 @@ private static async ValueTask ReadAsync( isFinalBlock, new ReadOnlySpan(buffer, start, bytesInBuffer), options, - ref readStack); + ref state); - Debug.Assert(readStack.BytesConsumed <= bytesInBuffer); - int bytesConsumed = checked((int)readStack.BytesConsumed); + Debug.Assert(state.BytesConsumed <= bytesInBuffer); + int bytesConsumed = checked((int)state.BytesConsumed); bytesInBuffer -= bytesConsumed; @@ -203,7 +202,7 @@ private static async ValueTask ReadAsync( // The reader should have thrown if we have remaining bytes. Debug.Assert(bytesInBuffer == 0); - return (TValue)readStack.Current.ReturnValue!; + return (TValue)state.Current.ReturnValue!; } private static void ReadCore( @@ -211,7 +210,7 @@ private static void ReadCore( bool isFinalBlock, ReadOnlySpan buffer, JsonSerializerOptions options, - ref ReadStack readStack) + ref ReadStack state) { var reader = new Utf8JsonReader(buffer, isFinalBlock, readerState); @@ -219,13 +218,13 @@ private static void ReadCore( // to enable read ahead behaviors to ensure we have complete json objects and arrays // ({}, []) when needed. (Notably to successfully parse JsonElement via JsonDocument // to assign to object and JsonElement properties in the constructed .NET object.) - readStack.ReadAhead = !isFinalBlock; - readStack.BytesConsumed = 0; + state.ReadAhead = !isFinalBlock; + state.BytesConsumed = 0; ReadCore( options, ref reader, - ref readStack); + ref state); readerState = reader.CurrentState; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs index cf1ac4d9c1017a..df534fa8f4691f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs @@ -5,12 +5,31 @@ using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; namespace System.Text.Json { public static partial class JsonSerializer { + /// + /// Internal version that allows re-entry with preserving ReadStack so that JsonPath works correctly. + /// + internal static T Deserialize(ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state, string? propertyName = null) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + state.Current.InitializeReEntry(typeof(T), options, propertyName); + + T value = (T)ReadCoreReEntry(options, ref reader, ref state)!; + + // Clear the current property state since we are done processing it. + state.Current.EndProperty(); + + return value; + } + /// /// Reads one JSON value (including objects or arrays) from the provided reader into a . /// @@ -111,16 +130,12 @@ public static TValue Deserialize(ref Utf8JsonReader reader, JsonSerializ options = JsonSerializerOptions.s_defaultOptions; } - ReadStack readStack = default; - if (options.ReferenceHandling.ShouldReadPreservedReferences()) - { - readStack.ReferenceResolver = new DefaultReferenceResolver(writing: false); - } - readStack.Current.Initialize(returnType, options); + ReadStack state = default; + state.InitializeRoot(returnType, options); - ReadValueCore(options, ref reader, ref readStack); + ReadValueCore(options, ref reader, ref state); - return readStack.Current.ReturnValue; + return state.Current.ReturnValue; } private static void CheckSupportedOptions(JsonReaderOptions readerOptions, string paramName) @@ -131,10 +146,10 @@ private static void CheckSupportedOptions(JsonReaderOptions readerOptions, strin } } - private static void ReadValueCore(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack readStack) + private static void ReadValueCore(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) { - JsonReaderState state = reader.CurrentState; - CheckSupportedOptions(state.Options, nameof(reader)); + JsonReaderState readerState = reader.CurrentState; + CheckSupportedOptions(readerState.Options, nameof(reader)); // Value copy to overwrite the ref on an exception and undo the destructive reads. Utf8JsonReader restore = reader; @@ -288,7 +303,7 @@ private static void ReadValueCore(JsonSerializerOptions options, ref Utf8JsonRea { reader = restore; // Re-throw with Path information. - ThrowHelper.ReThrowWithPath(readStack, ex); + ThrowHelper.ReThrowWithPath(state, ex); } int length = valueSpan.IsEmpty ? checked((int)valueSequence.Length) : valueSpan.Length; @@ -306,11 +321,11 @@ private static void ReadValueCore(JsonSerializerOptions options, ref Utf8JsonRea valueSpan.CopyTo(rentedSpan); } - JsonReaderOptions originalReaderOptions = state.Options; + JsonReaderOptions originalReaderOptions = readerState.Options; var newReader = new Utf8JsonReader(rentedSpan, originalReaderOptions); - ReadCore(options, ref newReader, ref readStack); + ReadCore(options, ref newReader, ref state); // The reader should have thrown if we have remaining bytes. Debug.Assert(newReader.BytesConsumed == length); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs index 54683977ed6489..50deaa6ababff5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs @@ -4,7 +4,7 @@ using System.Buffers; using System.Diagnostics; -using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; namespace System.Text.Json { @@ -17,178 +17,75 @@ public static partial class JsonSerializer private static void ReadCore( JsonSerializerOptions options, ref Utf8JsonReader reader, - ref ReadStack readStack) + ref ReadStack state) { try { - JsonReaderState initialState = default; - long initialBytesConsumed = default; + JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo!.PolicyProperty!; + JsonConverter converter = jsonPropertyInfo.ConverterBase; - while (true) + if (!state.IsContinuation) { - if (readStack.ReadAhead) + if (!JsonConverter.SingleValueReadWithReadAhead(converter.ClassType, ref reader, ref state)) { - // When we're reading ahead we always have to save the state - // as we don't know if the next token is an opening object or - // array brace. - initialState = reader.CurrentState; - initialBytesConsumed = reader.BytesConsumed; + // Read more data until we have the full element. + state.BytesConsumed += reader.BytesConsumed; + return; } - - if (!reader.Read()) - { - // Need more data - break; - } - - JsonTokenType tokenType = reader.TokenType; - - if (options.ReferenceHandling.ShouldReadPreservedReferences()) - { - CheckValidTokenAfterMetadataValues(ref readStack, tokenType); - } - - if (JsonHelpers.IsInRangeInclusive(tokenType, JsonTokenType.String, JsonTokenType.False)) - { - Debug.Assert(tokenType == JsonTokenType.String || tokenType == JsonTokenType.Number || tokenType == JsonTokenType.True || tokenType == JsonTokenType.False); - - HandleValue(tokenType, options, ref reader, ref readStack); - } - else if (tokenType == JsonTokenType.PropertyName) - { - HandlePropertyName(options, ref reader, ref readStack); - } - else if (tokenType == JsonTokenType.StartObject) + } + else + { + // For a continuation, read ahead here to avoid having to build and then tear + // down the call stack if there is more than one buffer fetch necessary. + if (!JsonConverter.SingleValueReadWithReadAhead(ClassType.Value, ref reader, ref state)) { - if (readStack.Current.SkipProperty) - { - readStack.Push(); - readStack.Current.Drain = true; - } - else if (readStack.Current.IsProcessingValue()) - { - if (!HandleObjectAsValue(tokenType, options, ref reader, ref readStack, ref initialState, initialBytesConsumed)) - { - // Need more data - break; - } - } - else if (readStack.Current.IsProcessingDictionary()) - { - HandleStartDictionary(options, ref readStack); - } - else - { - HandleStartObject(options, ref readStack); - } + state.BytesConsumed += reader.BytesConsumed; + return; } - else if (tokenType == JsonTokenType.EndObject) - { - if (readStack.Current.Drain) - { - readStack.Pop(); + } - // Clear the current property in case it is a dictionary, since dictionaries must have EndProperty() called when completed. - // A non-dictionary property can also have EndProperty() called when completed, although it is redundant. - readStack.Current.EndProperty(); - } - else if (readStack.Current.ReferenceId != null) - { - Debug.Assert(options.ReferenceHandling.ShouldReadPreservedReferences()); + bool success = converter.TryReadAsObject(ref reader, jsonPropertyInfo.RuntimePropertyType!, options, ref state, out object? value); + if (success) + { + state.Current.ReturnValue = value; - HandleReference(ref readStack); - } - else if (readStack.Current.IsProcessingDictionary()) - { - HandleEndDictionary(options, ref readStack); - } - else - { - HandleEndObject(ref readStack); - } - } - else if (tokenType == JsonTokenType.StartArray) - { - if (!readStack.Current.IsProcessingValue()) - { - HandleStartArray(options, ref readStack); - } - else if (!HandleObjectAsValue(tokenType, options, ref reader, ref readStack, ref initialState, initialBytesConsumed)) - { - // Need more data - break; - } - } - else if (tokenType == JsonTokenType.EndArray) - { - HandleEndArray(options, ref readStack); - } - else if (tokenType == JsonTokenType.Null) - { - HandleNull(options, ref reader, ref readStack); - } + // Read any trailing whitespace. + // If additional whitespace exists after this read, the subsequent call to reader.Read() will throw. + reader.Read(); } + + state.BytesConsumed += reader.BytesConsumed; } catch (JsonReaderException ex) { // Re-throw with Path information. - ThrowHelper.ReThrowWithPath(readStack, ex); + ThrowHelper.ReThrowWithPath(state, ex); } catch (FormatException ex) when (ex.Source == ThrowHelper.ExceptionSourceValueToRethrowAsJsonException) { - ThrowHelper.ReThrowWithPath(readStack, reader, ex); + ThrowHelper.ReThrowWithPath(state, reader, ex); } catch (InvalidOperationException ex) when (ex.Source == ThrowHelper.ExceptionSourceValueToRethrowAsJsonException) { - ThrowHelper.ReThrowWithPath(readStack, reader, ex); + ThrowHelper.ReThrowWithPath(state, reader, ex); } catch (JsonException ex) { - ThrowHelper.AddExceptionInformation(readStack, reader, ex); + ThrowHelper.AddExceptionInformation(state, reader, ex); throw; } - - readStack.BytesConsumed += reader.BytesConsumed; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool HandleObjectAsValue( - JsonTokenType tokenType, + internal static object? ReadCoreReEntry( JsonSerializerOptions options, ref Utf8JsonReader reader, - ref ReadStack readStack, - ref JsonReaderState initialState, - long initialBytesConsumed) + ref ReadStack state) { - if (readStack.ReadAhead) - { - // Attempt to skip to make sure we have all the data we need. - bool complete = reader.TrySkip(); - - // We need to restore the state in all cases as we need to be positioned back before - // the current token to either attempt to skip again or to actually read the value in - // HandleValue below. - - reader = new Utf8JsonReader( - reader.OriginalSpan.Slice(checked((int)initialBytesConsumed)), - isFinalBlock: reader.IsFinalBlock, - state: initialState); - Debug.Assert(reader.BytesConsumed == 0); - readStack.BytesConsumed += initialBytesConsumed; - - if (!complete) - { - // Couldn't read to the end of the object, exit out to get more data in the buffer. - return false; - } - - // Success, requeue the reader to the token for HandleValue. - reader.Read(); - Debug.Assert(tokenType == reader.TokenType); - } - - HandleValue(tokenType, options, ref reader, ref readStack); - return true; + JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo!; + JsonConverter converter = jsonPropertyInfo.ConverterBase; + bool success = converter.TryReadAsObject(ref reader, jsonPropertyInfo.RuntimePropertyType!, options, ref state, out object? value); + Debug.Assert(success); + return value; } private static ReadOnlySpan GetUnescapedString(ReadOnlySpan utf8Source, int idx) @@ -213,18 +110,5 @@ private static ReadOnlySpan GetUnescapedString(ReadOnlySpan utf8Sour return propertyName; } - - private static void CheckValidTokenAfterMetadataValues(ref ReadStack state, JsonTokenType tokenType) - { - if (state.Current.LastSeenMetadataProperty == MetadataPropertyName.Values) - { - if (tokenType != JsonTokenType.StartArray) - { - ThrowHelper.ThrowJsonException_MetadataValuesInvalidToken(tokenType); - } - - state.Current.LastSeenMetadataProperty = MetadataPropertyName.NoMetadata; - } - } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs deleted file mode 100644 index 85622ba8b62728..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs +++ /dev/null @@ -1,197 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text.Json.Serialization; - -namespace System.Text.Json -{ - public static partial class JsonSerializer - { - private static bool HandleDictionary( - JsonClassInfo elementClassInfo, - JsonSerializerOptions options, - Utf8JsonWriter writer, - ref WriteStack state) - { - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo!; - if (state.Current.CollectionEnumerator == null) - { - IEnumerable? enumerable = (IEnumerable?)jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue); - if (enumerable == null) - { - if ((state.Current.JsonClassInfo!.ClassType != ClassType.Object || // Write null dictionary values - !jsonPropertyInfo.IgnoreNullValues) && // Ignore ClassType.Object properties if IgnoreNullValues is true - state.Current.ExtensionDataStatus != ExtensionDataWriteStatus.Writing) // Ignore null extension property (which is a dictionary) - { - // Write a null object or enumerable. - state.Current.WriteObjectOrArrayStart(ClassType.Dictionary, writer, options, writeNull: true); - } - - if (state.Current.PopStackOnEndCollection) - { - state.Pop(); - } - - return true; - } - - if (state.Current.ExtensionDataStatus != ExtensionDataWriteStatus.Writing) - { - if (options.ReferenceHandling.ShouldWritePreservedReferences()) - { - if (WriteReference(ref state, writer, options, ClassType.Dictionary, enumerable)) - { - return WriteEndDictionary(ref state); - } - } - else - { - state.Current.WriteObjectOrArrayStart(ClassType.Dictionary, writer, options); - } - } - - // Let the dictionary return the default IEnumerator from its IEnumerable.GetEnumerator(). - // For IDictionary-derived classes this is normally be IDictionaryEnumerator. - // For IDictionary-derived classes this is normally IDictionaryEnumerator as well - // but may be IEnumerable> if the dictionary only supports generics. - state.Current.CollectionEnumerator = enumerable.GetEnumerator(); - } - - if (state.Current.CollectionEnumerator.MoveNext()) - { - // A dictionary should not have a null KeyValuePair. - Debug.Assert(state.Current.CollectionEnumerator.Current != null); - - bool obtainedValues = false; - string? key = default; - object? value = default; - - // Check for polymorphism. - if (elementClassInfo.ClassType == ClassType.Unknown) - { - jsonPropertyInfo.GetDictionaryKeyAndValue(ref state.Current, out key, out value); - GetRuntimeClassInfo(value, ref elementClassInfo, options); - obtainedValues = true; - } - - if (elementClassInfo.ClassType == ClassType.Value) - { - elementClassInfo.PolicyProperty!.WriteDictionary(ref state, writer); - } - else - { - if (!obtainedValues) - { - jsonPropertyInfo.GetDictionaryKeyAndValue(ref state.Current, out key, out value); - } - - if (options.DictionaryKeyPolicy != null && state.Current.ExtensionDataStatus != ExtensionDataWriteStatus.Writing) - { - Debug.Assert(key != null); - key = options.DictionaryKeyPolicy.ConvertName(key); - if (key == null) - { - ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(options.DictionaryKeyPolicy.GetType()); - } - } - - // An object or another enumerator requires a new stack frame. - state.Push(elementClassInfo, value); - - state.Current.KeyName = key; - } - - return false; - } - - // We are done enumerating. - if (state.Current.ExtensionDataStatus == ExtensionDataWriteStatus.Writing) - { - state.Current.ExtensionDataStatus = ExtensionDataWriteStatus.Finished; - } - else - { - writer.WriteEndObject(); - } - - return WriteEndDictionary(ref state); - } - - private static bool WriteEndDictionary(ref WriteStack state) - { - if (state.Current.PopStackOnEndCollection) - { - state.Pop(); - } - else - { - state.Current.EndDictionary(); - } - - return true; - } - - internal static void WriteDictionary( - JsonConverter converter, - JsonSerializerOptions options, - ref WriteStackFrame current, - Utf8JsonWriter writer) - { - Debug.Assert(converter != null && current.CollectionEnumerator != null); - - string key; - TProperty value; - if (current.CollectionEnumerator is IEnumerator> enumerator) - { - key = enumerator.Current.Key; - value = enumerator.Current.Value; - } - else if (current.CollectionEnumerator is IEnumerator> polymorphicEnumerator) - { - key = polymorphicEnumerator.Current.Key; - value = (TProperty)polymorphicEnumerator.Current.Value; - } - else if (current.CollectionEnumerator is IDictionaryEnumerator iDictionaryEnumerator && - iDictionaryEnumerator.Key is string keyAsString) - { - key = keyAsString; - value = (TProperty)iDictionaryEnumerator.Value!; - } - else - { - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection( - current.JsonPropertyInfo!.DeclaredPropertyType, - current.JsonPropertyInfo.ParentClassType, - current.JsonPropertyInfo.PropertyInfo); - } - - Debug.Assert(key != null); - - if (options.DictionaryKeyPolicy != null && - // We do not convert extension data. - current.ExtensionDataStatus != ExtensionDataWriteStatus.Writing) - { - key = options.DictionaryKeyPolicy.ConvertName(key); - - if (key == null) - { - ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(options.DictionaryKeyPolicy.GetType()); - } - } - - if (value == null) - { - writer.WriteNull(key); - } - else - { - writer.WritePropertyName(key); - converter.Write(writer, value, options); - } - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleEnumerable.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleEnumerable.cs deleted file mode 100644 index d01d44727b42b1..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleEnumerable.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections; -using System.Diagnostics; - -namespace System.Text.Json -{ - public static partial class JsonSerializer - { - private static bool HandleEnumerable( - JsonClassInfo elementClassInfo, - JsonSerializerOptions options, - Utf8JsonWriter writer, - ref WriteStack state) - { - Debug.Assert(state.Current.JsonPropertyInfo!.ClassType == ClassType.Enumerable); - - if (state.Current.CollectionEnumerator == null) - { - IEnumerable? enumerable = (IEnumerable?)state.Current.JsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue); - - if (enumerable == null) - { - // If applicable, we only want to ignore object properties. - if (state.Current.JsonClassInfo!.ClassType != ClassType.Object || - !state.Current.JsonPropertyInfo.IgnoreNullValues) - { - // Write a null object or enumerable. - state.Current.WriteObjectOrArrayStart(ClassType.Enumerable, writer, options, writeNull: true); - } - - if (state.Current.PopStackOnEndCollection) - { - state.Pop(); - } - - return true; - } - - if (options.ReferenceHandling.ShouldWritePreservedReferences()) - { - if (WriteReference(ref state, writer, options, ClassType.Enumerable, enumerable)) - { - return WriteEndArray(ref state); - } - } - else - { - state.Current.WriteObjectOrArrayStart(ClassType.Enumerable, writer, options); - } - - state.Current.CollectionEnumerator = enumerable.GetEnumerator(); - } - - if (state.Current.CollectionEnumerator.MoveNext()) - { - // Check for polymorphism. - if (elementClassInfo.ClassType == ClassType.Unknown) - { - object? currentValue = state.Current.CollectionEnumerator.Current; - GetRuntimeClassInfo(currentValue, ref elementClassInfo, options); - } - - if (elementClassInfo.ClassType == ClassType.Value) - { - elementClassInfo.PolicyProperty!.WriteEnumerable(ref state, writer); - } - else if (state.Current.CollectionEnumerator.Current == null) - { - // Write a null object or enumerable. - writer.WriteNullValue(); - } - else - { - // An object or another enumerator requires a new stack frame. - object nextValue = state.Current.CollectionEnumerator.Current; - state.Push(elementClassInfo, nextValue); - } - - return false; - } - - // We are done enumerating. - writer.WriteEndArray(); - - // Used for ReferenceHandling.Preserve - if (state.Current.WriteWrappingBraceOnEndPreservedArray) - { - writer.WriteEndObject(); - } - - return WriteEndArray(ref state); - } - - private static bool WriteEndArray(ref WriteStack state) - { - if (state.Current.PopStackOnEndCollection) - { - state.Pop(); - } - else - { - state.Current.EndArray(); - } - - return true; - } - } -} 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 new file mode 100644 index 00000000000000..151e5325da874d --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Text.Json.Serialization; + +namespace System.Text.Json +{ + public static partial class JsonSerializer + { + // Pre-encoded metadata properties. + 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 MetadataPropertyName GetResolvedReferenceHandling( + JsonConverter converter, + object value, + ref WriteStack state, + out string? referenceId) + { + if (!converter.CanHaveIdMetadata || converter.TypeToConvert.IsValueType) + { + referenceId = default; + return MetadataPropertyName.NoMetadata; + } + + if (state.ReferenceResolver.TryGetOrAddReferenceOnSerialize(value, out referenceId)) + { + return MetadataPropertyName.Ref; + } + + return MetadataPropertyName.Id; + } + + internal static MetadataPropertyName WriteReferenceForObject( + JsonConverter jsonConverter, + object currentValue, + ref WriteStack state, + Utf8JsonWriter writer) + { + MetadataPropertyName metadataToWrite = GetResolvedReferenceHandling(jsonConverter, currentValue, ref state, out string? referenceId); + + if (metadataToWrite == MetadataPropertyName.Ref) + { + writer.WriteString(s_metadataRef, referenceId!); + writer.WriteEndObject(); + } + else if (metadataToWrite == MetadataPropertyName.Id) + { + writer.WriteString(s_metadataId, referenceId!); + } + + return metadataToWrite; + } + + internal static MetadataPropertyName WriteReferenceForCollection( + JsonConverter jsonConverter, + object currentValue, + ref WriteStack state, + Utf8JsonWriter writer) + { + MetadataPropertyName metadataToWrite = GetResolvedReferenceHandling(jsonConverter, currentValue, ref state, out string? referenceId); + + if (metadataToWrite == MetadataPropertyName.NoMetadata) + { + writer.WriteStartArray(); + } + else if (metadataToWrite == MetadataPropertyName.Id) + { + writer.WriteStartObject(); + writer.WriteString(s_metadataId, referenceId!); + writer.WriteStartArray(s_metadataValues); + } + else + { + Debug.Assert(metadataToWrite == MetadataPropertyName.Ref); + writer.WriteStartObject(); + writer.WriteString(s_metadataRef, referenceId!); + writer.WriteEndObject(); + } + + return metadataToWrite; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs deleted file mode 100644 index dd42b6ddc7b2a2..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs +++ /dev/null @@ -1,169 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Text.Json.Serialization; - -namespace System.Text.Json -{ - public static partial class JsonSerializer - { - // AggressiveInlining used although a large method it is only called from one location and is on a hot path. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool WriteObject( - JsonSerializerOptions options, - Utf8JsonWriter writer, - ref WriteStack state) - { - // Write the start. - if (!state.Current.StartObjectWritten) - { - // If true, we are writing a root object or a value that doesn't belong - // to an object e.g. a dictionary value. - if (state.Current.CurrentValue == null) - { - state.Current.WriteObjectOrArrayStart(ClassType.Object, writer, options, writeNull: true); - return WriteEndObject(ref state); - } - - if (options.ReferenceHandling.ShouldWritePreservedReferences()) - { - if (WriteReference(ref state, writer, options, ClassType.Object, state.Current.CurrentValue)) - { - return WriteEndObject(ref state); - } - } - else - { - state.Current.WriteObjectOrArrayStart(ClassType.Object, writer, options); - } - - state.Current.MoveToNextProperty = true; - } - - if (state.Current.MoveToNextProperty) - { - state.Current.NextProperty(); - } - - // Determine if we are done enumerating properties. - if (state.Current.ExtensionDataStatus != ExtensionDataWriteStatus.Finished) - { - // If ClassType.Unknown at this point, we are typeof(object) which should not have any properties. - Debug.Assert(state.Current.JsonClassInfo!.ClassType != ClassType.Unknown); - - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.PropertyCacheArray![state.Current.PropertyEnumeratorIndex - 1]; - HandleObject(jsonPropertyInfo, options, writer, ref state); - - return false; - } - - writer.WriteEndObject(); - return WriteEndObject(ref state); - } - - private static bool WriteEndObject(ref WriteStack state) - { - if (state.Current.PopStackOnEndObject) - { - state.Pop(); - } - - return true; - } - - // AggressiveInlining used although a large method it is only called from one location and is on a hot path. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void HandleObject( - JsonPropertyInfo jsonPropertyInfo, - JsonSerializerOptions options, - Utf8JsonWriter writer, - ref WriteStack state) - { - Debug.Assert( - state.Current.JsonClassInfo!.ClassType == ClassType.Object || - state.Current.JsonClassInfo.ClassType == ClassType.Unknown); - - if (!jsonPropertyInfo.ShouldSerialize) - { - state.Current.MoveToNextProperty = true; - return; - } - - bool obtainedValue = false; - object? currentValue = null; - - // Check for polymorphism. - if (jsonPropertyInfo.ClassType == ClassType.Unknown) - { - currentValue = jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue); - obtainedValue = true; - GetRuntimePropertyInfo(currentValue, state.Current.JsonClassInfo, ref jsonPropertyInfo, options); - } - - state.Current.JsonPropertyInfo = jsonPropertyInfo; - - if (jsonPropertyInfo.ClassType == ClassType.Value) - { - jsonPropertyInfo.Write(ref state, writer); - state.Current.MoveToNextProperty = true; - return; - } - - // A property that returns an enumerator keeps the same stack frame. - if (jsonPropertyInfo.ClassType == ClassType.Enumerable) - { - bool endOfEnumerable = HandleEnumerable(jsonPropertyInfo.ElementClassInfo!, options, writer, ref state); - if (endOfEnumerable) - { - state.Current.MoveToNextProperty = true; - } - - return; - } - - // A property that returns a dictionary keeps the same stack frame. - if (jsonPropertyInfo.ClassType == ClassType.Dictionary) - { - bool endOfEnumerable = HandleDictionary(jsonPropertyInfo.ElementClassInfo!, options, writer, ref state); - if (endOfEnumerable) - { - state.Current.MoveToNextProperty = true; - } - - return; - } - - // A property that returns an object. - if (!obtainedValue) - { - currentValue = jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue); - } - - if (currentValue != null) - { - // A new stack frame is required. - JsonPropertyInfo previousPropertyInfo = state.Current.JsonPropertyInfo; - state.Current.MoveToNextProperty = true; - - JsonClassInfo nextClassInfo = jsonPropertyInfo.RuntimeClassInfo; - state.Push(nextClassInfo, currentValue); - - // Set the PropertyInfo so we can obtain the property name in order to write it. - state.Current.JsonPropertyInfo = previousPropertyInfo; - } - else - { - if (!jsonPropertyInfo.IgnoreNullValues) - { - Debug.Assert(jsonPropertyInfo.EscapedName != null); - writer.WriteNull(jsonPropertyInfo.EscapedName.Value); - } - - state.Current.MoveToNextProperty = true; - } - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs index 696b773009c9aa..31d2b2180d86cd 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs @@ -9,34 +9,6 @@ namespace System.Text.Json { public static partial class JsonSerializer { - private static void GetRuntimeClassInfo(object? value, ref JsonClassInfo jsonClassInfo, JsonSerializerOptions options) - { - if (value != null) - { - Type runtimeType = value.GetType(); - - // Nothing to do for typeof(object) - if (runtimeType != typeof(object)) - { - jsonClassInfo = options.GetOrAddClass(runtimeType); - } - } - } - - private static void GetRuntimePropertyInfo(object? value, JsonClassInfo jsonClassInfo, ref JsonPropertyInfo jsonPropertyInfo, JsonSerializerOptions options) - { - if (value != null) - { - Type runtimeType = value.GetType(); - - // Nothing to do for typeof(object) - if (runtimeType != typeof(object)) - { - jsonPropertyInfo = jsonClassInfo.GetOrAddPolymorphicProperty(jsonPropertyInfo, runtimeType, options); - } - } - } - private static void VerifyValueAndType(object? value, Type type) { if (type == null) @@ -129,42 +101,11 @@ private static void WriteCore(Utf8JsonWriter writer, object? value, Type type, J } WriteStack state = default; - if (options.ReferenceHandling.ShouldWritePreservedReferences()) - { - state.ReferenceResolver = new DefaultReferenceResolver(writing: true); - } - Debug.Assert(type != null); - state.Current.Initialize(type, options); - state.Current.CurrentValue = value; - - Write(writer, writer.CurrentDepth, flushThreshold: -1, options, ref state); + state.InitializeRoot(type!, options, supportContinuation: false); + WriteCore(writer, value, options, ref state, state.Current.JsonClassInfo!.PolicyProperty!.ConverterBase); } writer.Flush(); } - - private static bool WriteReference(ref WriteStack state, Utf8JsonWriter writer, JsonSerializerOptions options, ClassType classType, object currentValue) - { - // Avoid emitting metadata for value types. - Type currentType = state.Current.JsonPropertyInfo?.DeclaredPropertyType ?? state.Current.JsonClassInfo!.Type; - if (currentType.IsValueType) - { - // Value type, fallback on regular Write method. - state.Current.WriteObjectOrArrayStart(classType, writer, options); - return false; - } - - if (state.ReferenceResolver.TryGetOrAddReferenceOnSerialize(currentValue, out string referenceId)) - { - // Object written before, write { "$ref": "#" } and jump to the next property/element. - state.Current.WriteReferenceObject(writer, options, referenceId); - return true; - } - - // New object reference, write start and append $id. - // OR New array reference, write as object and append $id and $values; at the end writes EndObject token using WriteWrappingBraceOnEndCollection. - state.Current.WritePreservedObjectOrArrayStart(classType, writer, options, referenceId); - return false; - } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs index 4677e032a48437..fb42a2b2591c2e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs @@ -71,21 +71,20 @@ private static async Task WriteAsyncCore(Stream utf8Json, object? value, Type in } WriteStack state = default; - if (options.ReferenceHandling.ShouldWritePreservedReferences()) - { - state.ReferenceResolver = new DefaultReferenceResolver(writing: true); - } - state.Current.Initialize(inputType, options); - state.Current.CurrentValue = value; + state.InitializeRoot(inputType, options, supportContinuation: true); bool isFinalBlock; - int flushThreshold; do { - flushThreshold = (int)(bufferWriter.Capacity * .9); //todo: determine best value here + state.FlushThreshold = (int)(bufferWriter.Capacity * .9); //todo: determine best value here + isFinalBlock = WriteCore( + writer, + value, + options, + ref state, + state.Current.JsonClassInfo!.PolicyProperty!.ConverterBase); - isFinalBlock = Write(writer, originalWriterDepth: 0, flushThreshold, options, ref state); writer.Flush(); await bufferWriter.WriteToStreamAsync(utf8Json, cancellationToken).ConfigureAwait(false); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Utf8JsonWriter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Utf8JsonWriter.cs index aa420f900d468a..bf31e422263448 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Utf8JsonWriter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Utf8JsonWriter.cs @@ -2,10 +2,29 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Text.Json.Serialization; +using System.Diagnostics; + namespace System.Text.Json { public static partial class JsonSerializer { + /// + /// Internal version that allows re-entry with preserving WriteStack so that JsonPath works correctly. + /// + // If this is made public, we will also want to have a non-generic version. + internal static void Serialize(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state, string? propertyName = null) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + JsonConverter jsonConverter = state.Current.InitializeReEntry(typeof(T), options, propertyName); + bool success = jsonConverter.TryWriteAsObject(writer, value, options, ref state); + Debug.Assert(success); + } + /// /// Write one JSON value (including objects or arrays) to the provided writer. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs index ce6e11fa7eae08..a1999acc1829ef 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Diagnostics; +using System.Text.Json.Serialization; namespace System.Text.Json { @@ -12,70 +12,27 @@ public static partial class JsonSerializer // 1) The object type was specified as the root-level return type to a Deserialize method. // 2) The object is a property on a parent object. // 3) The object is an element in an enumerable. - private static bool Write( + private static bool WriteCore( Utf8JsonWriter writer, - int originalWriterDepth, - int flushThreshold, + object? value, JsonSerializerOptions options, - ref WriteStack state) + ref WriteStack state, + JsonConverter jsonConverter) { - bool finishedSerializing; - try { - do - { - switch (state.Current.JsonClassInfo!.ClassType) - { - case ClassType.Enumerable: - finishedSerializing = HandleEnumerable(state.Current.JsonClassInfo.ElementClassInfo!, options, writer, ref state); - break; - case ClassType.Value: - Debug.Assert(state.Current.JsonPropertyInfo!.ClassType == ClassType.Value); - state.Current.JsonPropertyInfo.Write(ref state, writer); - finishedSerializing = true; - break; - case ClassType.Dictionary: - finishedSerializing = HandleDictionary(state.Current.JsonClassInfo.ElementClassInfo!, options, writer, ref state); - break; - default: - Debug.Assert(state.Current.JsonClassInfo.ClassType == ClassType.Object || - state.Current.JsonClassInfo.ClassType == ClassType.Unknown); - - finishedSerializing = WriteObject(options, writer, ref state); - break; - } - - if (finishedSerializing) - { - if (writer.CurrentDepth == originalWriterDepth) - { - break; - } - } - else if (writer.CurrentDepth >= options.EffectiveMaxDepth) - { - ThrowHelper.ThrowInvalidOperationException_SerializerCycleDetected(options.MaxDepth); - } - - // If serialization is not finished and we surpass flush threshold then return false which will flush stream. - if (flushThreshold >= 0 && writer.BytesPending > flushThreshold) - { - return false; - } - } while (true); + return jsonConverter.TryWriteAsObject(writer, value, options, ref state); } catch (InvalidOperationException ex) when (ex.Source == ThrowHelper.ExceptionSourceValueToRethrowAsJsonException) { ThrowHelper.ReThrowWithPath(state, ex); + throw; } catch (JsonException ex) { ThrowHelper.AddExceptionInformation(state, ex); throw; } - - return true; } } } 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 161c1345c8af7b..227a5b8c835c9d 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 @@ -40,17 +40,24 @@ private static Dictionary GetDefaultSimpleConverters() return converters; } + // Get the list for converters that implement CanConvert(). private static List GetDefaultConverters() { - const int NumberOfConverters = 2; + const int NumberOfConverters = 5; var converters = new List(NumberOfConverters); - // Use a list for converters that implement CanConvert(). + // Nullable converter should always be first since it forwards to any nullable type. + converters.Add(new JsonValueConverterNullableFactory()); + converters.Add(new JsonConverterEnum()); converters.Add(new JsonKeyValuePairConverter()); - // We will likely add collection converters here in the future. + // IEnumerable should always be last since they can convert any IEnumerable. + converters.Add(new JsonIEnumerableConverterFactory()); + + // Object should always be last since it converts any type. + converters.Add(new JsonObjectConverterFactory()); Debug.Assert(NumberOfConverters == converters.Count); @@ -65,7 +72,7 @@ private static List GetDefaultConverters() /// public IList Converters { get; } - internal JsonConverter? DetermineConverterForProperty(Type parentClassType, Type runtimePropertyType, PropertyInfo? propertyInfo) + internal JsonConverter? DetermineConverter(Type parentClassType, Type runtimePropertyType, PropertyInfo? propertyInfo) { JsonConverter? converter = null; @@ -155,10 +162,8 @@ private static List GetDefaultConverters() if (converter is JsonConverterFactory factory) { converter = factory.GetConverterInternal(typeToConvert, this); - if (converter == null || converter.TypeToConvert == null) - { - throw new ArgumentNullException(nameof(typeToConvert)); - } + // Allow null converters from the factory. This will result in a NotSupportedException later + // and with a nice exception that indicates the parent type. } if (converter != null) @@ -184,11 +189,6 @@ private static List GetDefaultConverters() return converter; } - internal bool HasConverter(Type typeToConvert) - { - return GetConverter(typeToConvert) != null; - } - private JsonConverter GetConverterFromAttribute(JsonConverterAttribute converterAttribute, Type typeToConvert, Type classTypeAttributeIsOn, PropertyInfo? propertyInfo) { JsonConverter? converter; 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 8e3646dac5ac60..9497f30bb98031 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 @@ -6,7 +6,6 @@ using System.Diagnostics; using System.Text.Json.Serialization; using System.Text.Encodings.Web; -using System.Diagnostics.CodeAnalysis; namespace System.Text.Json { @@ -20,13 +19,14 @@ public sealed partial class JsonSerializerOptions internal static readonly JsonSerializerOptions s_defaultOptions = new JsonSerializerOptions(); private readonly ConcurrentDictionary _classes = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary s_createRangeDelegates = new ConcurrentDictionary(); + private MemberAccessor? _memberAccessorStrategy; private JsonNamingPolicy? _dictionaryKeyPolicy; private JsonNamingPolicy? _jsonPropertyNamingPolicy; private JsonCommentHandling _readCommentHandling; private ReferenceHandling _referenceHandling = ReferenceHandling.Default; - private JavaScriptEncoder? _encoder; + private JavaScriptEncoder? _encoder = null; + private int _defaultBufferSize = BufferSizeDefault; private int _maxDepth; private bool _allowTrailingCommas; @@ -307,6 +307,7 @@ public ReferenceHandling ReferenceHandling set { VerifyMutable(); + _referenceHandling = value ?? throw new ArgumentNullException(nameof(value)); } } @@ -317,10 +318,9 @@ internal MemberAccessor MemberAccessorStrategy { if (_memberAccessorStrategy == null) { -#if BUILDING_INBOX_LIBRARY +#if NETFRAMEWORK || NETCOREAPP _memberAccessorStrategy = new ReflectionEmitMemberAccessor(); #else - // todo: should we attempt to detect here, or at least have a #define like #SUPPORTS_IL_EMIT _memberAccessorStrategy = new ReflectionMemberAccessor(); #endif } @@ -364,21 +364,6 @@ internal JsonWriterOptions GetWriterOptions() }; } - internal bool CreateRangeDelegatesContainsKey(string key) - { - return s_createRangeDelegates.ContainsKey(key); - } - - internal bool TryGetCreateRangeDelegate(string delegateKey, [NotNullWhen(true)] out ImmutableCollectionCreator? createRangeDelegate) - { - return s_createRangeDelegates.TryGetValue(delegateKey, out createRangeDelegate) && createRangeDelegate != null; - } - - internal bool TryAddCreateRangeDelegate(string key, ImmutableCollectionCreator createRangeDelegate) - { - return s_createRangeDelegates.TryAdd(key, createRangeDelegate); - } - internal void VerifyMutable() { // The default options are hidden and thus should be immutable. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonValueConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonValueConverterOfT.cs new file mode 100644 index 00000000000000..15a1e24f332deb --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonValueConverterOfT.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Text.Json.Serialization +{ + // Used for value converters that need to re-enter the serializer since it will support JsonPath + // and reference handling. + internal abstract class JsonValueConverter : JsonConverter + { + internal sealed override ClassType ClassType => ClassType.NewValue; + + public override sealed T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Bridge from resumable to value converters. + if (options == null) + { + options = JsonSerializerOptions.s_defaultOptions; + } + + ReadStack state = default; + state.InitializeRoot(typeToConvert, options); + TryRead(ref reader, typeToConvert, options, ref state, out T value); + return value; + } + + public override sealed void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + // Bridge from resumable to value converters. + if (options == null) + { + options = JsonSerializerOptions.s_defaultOptions; + } + + WriteStack state = default; + state.InitializeRoot(typeof(T), options, supportContinuation: false); + TryWrite(writer, value, options, ref state); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/MemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/MemberAccessor.cs index 1f34061f1f0021..5a308567628952 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/MemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/MemberAccessor.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Diagnostics; +using System.Collections.Generic; using System.Reflection; namespace System.Text.Json @@ -11,47 +11,14 @@ internal abstract class MemberAccessor { public abstract JsonClassInfo.ConstructorDelegate? CreateConstructor(Type classType); - public abstract Action CreateAddDelegate(MethodInfo addMethod, object target); + public abstract Action CreateAddMethodDelegate(); - public abstract ImmutableCollectionCreator ImmutableCollectionCreateRange(Type constructingType, Type collectionType, Type elementType); + public abstract Func, TCollection> CreateImmutableEnumerableCreateRangeDelegate(); - public abstract ImmutableCollectionCreator ImmutableDictionaryCreateRange(Type constructingType, Type collectionType, Type elementType); + public abstract Func>, TCollection> CreateImmutableDictionaryCreateRangeDelegate(); - protected MethodInfo ImmutableCollectionCreateRangeMethod(Type constructingType, Type elementType) - { - MethodInfo createRangeMethod = FindImmutableCreateRangeMethod(constructingType); + public abstract Func CreatePropertyGetter(PropertyInfo propertyInfo); - return createRangeMethod.MakeGenericMethod(elementType); - } - - protected MethodInfo ImmutableDictionaryCreateRangeMethod(Type constructingType, Type elementType) - { - MethodInfo createRangeMethod = FindImmutableCreateRangeMethod(constructingType); - - return createRangeMethod.MakeGenericMethod(typeof(string), elementType); - } - - private MethodInfo FindImmutableCreateRangeMethod(Type constructingType) - { - MethodInfo[] constructingTypeMethods = constructingType.GetMethods(); - - foreach (MethodInfo method in constructingTypeMethods) - { - if (method.Name == "CreateRange" && method.GetParameters().Length == 1) - { - return method; - } - } - - // This shouldn't happen because constructingType should be an immutable type with - // a CreateRange method. `null` being returned here will cause a JsonException to be - // thrown when the desired CreateRange delegate is about to be invoked. - Debug.Fail("Could not create the appropriate CreateRange method."); - return null!; - } - - public abstract Func CreatePropertyGetter(PropertyInfo propertyInfo); - - public abstract Action CreatePropertySetter(PropertyInfo propertyInfo); + public abstract Action CreatePropertySetter(PropertyInfo propertyInfo); } } 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 8303e65ec81cf6..0c8aa502a85beb 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 @@ -14,51 +14,179 @@ internal struct ReadStack { internal static readonly char[] SpecialCharacters = { '.', ' ', '\'', '/', '"', '[', ']', '(', ')', '\t', '\n', '\r', '\f', '\b', '\\', '\u0085', '\u2028', '\u2029' }; - internal static byte[] s_idMetadataPropertyName = { (byte)'$', (byte)'i', (byte)'d' }; - internal static byte[] s_refMetadataPropertyName = { (byte)'$', (byte)'r', (byte)'e', (byte)'f' }; - internal static byte[] s_valuesMetadataPropertyName = { (byte)'$', (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', (byte)'s' }; + /// + /// The number of stack frames when the continuation started. + /// + private int _continuationCount; + + /// + /// The number of stack frames including Current. _previous will contain _count-1 higher frames. + /// + private int _count; + + private List _previous; + + /// + /// Bytes consumed in the current loop. + /// + public long BytesConsumed; // A field is used instead of a property to avoid value semantics. public ReadStackFrame Current; - private List _previous; - public int _index; + 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; - // The bag of preservable references. It needs to be kept in the state and never in JsonSerializerOptions because - // the options should not have any per-serialization state since every serialization shares the same immutable state on the options. + // The bag of preservable references. public DefaultReferenceResolver ReferenceResolver; - public void Push() + /// + /// Whether we need to read ahead in the inner read loop. + /// + public bool SupportContinuation; + + private void AddCurrent() { if (_previous == null) { _previous = new List(); } - if (_index == _previous.Count) + if (_count > _previous.Count) { // Need to allocate a new array element. _previous.Add(Current); } else { - Debug.Assert(_index < _previous.Count); - // Use a previously allocated slot. - _previous[_index] = Current; + _previous[_count - 1] = Current; } - Current.Reset(); - _index++; + _count++; } - public void Pop() + public void InitializeRoot(Type type, JsonSerializerOptions options) { - Debug.Assert(_index > 0); - Current = _previous[--_index]; + JsonClassInfo jsonClassInfo = options.GetOrAddClass(type); + Debug.Assert(jsonClassInfo.ClassType != ClassType.Invalid); + + Current.JsonClassInfo = jsonClassInfo; + + // The initial JsonPropertyInfo will be used to obtain the converter. + Current.JsonPropertyInfo = jsonClassInfo.PolicyProperty!; + + if (options.ReferenceHandling.ShouldReadPreservedReferences()) + { + ReferenceResolver = new DefaultReferenceResolver(writing: false); + } } - public bool IsLastFrame => _index == 0; + public void Push() + { + if (_continuationCount == 0) + { + if (_count == 0) + { + // The first stack frame is held in Current. + _count = 1; + } + else + { + JsonClassInfo jsonClassInfo; + if ((Current.JsonClassInfo.ClassType & (ClassType.Object | ClassType.Value | ClassType.NewValue)) != 0) + { + // Although ClassType.Value doesn't push, a custom custom converter may re-enter serialization. + jsonClassInfo = Current.JsonPropertyInfo!.RuntimeClassInfo; + } + else + { + jsonClassInfo = Current.JsonClassInfo.ElementClassInfo!; + } + + AddCurrent(); + Current.Reset(); + + Current.JsonClassInfo = jsonClassInfo; + Current.JsonPropertyInfo = jsonClassInfo.PolicyProperty!; + } + } + 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]; + + // Check if we are done. + if (_count == _continuationCount) + { + _continuationCount = 0; + } + else + { + _count++; + } + } + } + + public void Pop(bool success) + { + Debug.Assert(_count > 0); + + if (!success) + { + // Check if we need to initialize the continuation. + if (_continuationCount == 0) + { + if (_count == 1) + { + // No need for a continuation since there is only one stack frame. + _continuationCount = 1; + } + else + { + AddCurrent(); + _count--; + _continuationCount = _count; + _count--; + Current = _previous[_count - 1]; + } + + return; + } + + if (_continuationCount == 1) + { + // No need for a pop since there is only one stack frame. + Debug.Assert(_count == 1); + return; + } + + // Update the list entry to the current value. + _previous[_count - 1] = Current; + + Debug.Assert(_count > 0); + } + else + { + Debug.Assert(_continuationCount == 0); + } + + if (_count > 1) + { + Current = _previous[--_count -1]; + } + } // Return a JSONPath using simple dot-notation when possible. When special characters are present, bracket-notation is used: // $.x.y[0].z @@ -67,95 +195,105 @@ public string JsonPath() { StringBuilder sb = new StringBuilder("$"); - for (int i = 0; i < _index; i++) + // If a continuation, always report back full stack. + int count = Math.Max(_count, _continuationCount); + + for (int i = 0; i < count - 1; i++) { AppendStackFrame(sb, _previous[i]); } - AppendStackFrame(sb, Current); - return sb.ToString(); - } + if (_continuationCount == 0) + { + AppendStackFrame(sb, Current); + } - private void AppendStackFrame(StringBuilder sb, in ReadStackFrame frame) - { - // Append the property name. - string? propertyName = GetPropertyName(frame); - AppendPropertyName(sb, propertyName); + return sb.ToString(); - if (frame.JsonClassInfo != null) + static void AppendStackFrame(StringBuilder sb, in ReadStackFrame frame) { - if (frame.IsProcessingDictionary()) - { - // For dictionaries add the key. - AppendPropertyName(sb, frame.KeyName); - } - else if (frame.IsProcessingEnumerable()) + // Append the property name. + string? propertyName = GetPropertyName(frame); + AppendPropertyName(sb, propertyName); + + if (frame.JsonClassInfo != null && frame.IsProcessingEnumerable()) { - IList? list = frame.TempEnumerableValues; - if (list == null && frame.ReturnValue != null) + IEnumerable? enumerable = (IEnumerable?)frame.ReturnValue; + if (enumerable == null) { - - list = (IList?)frame.JsonPropertyInfo?.GetValueAsObject(frame.ReturnValue); + return; } - if (list != null) + + // Once all elements are read, the exception is not within the array. + if (frame.ObjectState < StackFrameObjectState.ReadElements) { sb.Append(@"["); - sb.Append(list.Count); + sb.Append(GetCount(enumerable)); sb.Append(@"]"); } } } - } - private void AppendPropertyName(StringBuilder sb, string? propertyName) - { - if (propertyName != null) + static int GetCount(IEnumerable enumerable) { - if (propertyName.IndexOfAny(SpecialCharacters) != -1) + if (enumerable is ICollection collection) { - sb.Append(@"['"); - sb.Append(propertyName); - sb.Append(@"']"); + return collection.Count; } - else + + int count = 0; + IEnumerator enumerator = enumerable.GetEnumerator(); + while (enumerator.MoveNext()) { - sb.Append('.'); - sb.Append(propertyName); + count++; } - } - } - private string? GetPropertyName(in ReadStackFrame frame) - { - // Attempt to get the JSON property name from the frame. - byte[]? utf8PropertyName = frame.JsonPropertyName; - if (utf8PropertyName == null) - { - // Attempt to get the JSON property name from the JsonPropertyInfo. - utf8PropertyName = frame.JsonPropertyInfo?.JsonPropertyName; + return count; } - string? propertyName; - if (utf8PropertyName != null) + static void AppendPropertyName(StringBuilder sb, string? propertyName) { - propertyName = JsonHelpers.Utf8GetString(utf8PropertyName); + if (propertyName != null) + { + if (propertyName.IndexOfAny(SpecialCharacters) != -1) + { + sb.Append(@"['"); + sb.Append(propertyName); + sb.Append(@"']"); + } + else + { + sb.Append('.'); + sb.Append(propertyName); + } + } } - else + + static string? GetPropertyName(in ReadStackFrame frame) { - propertyName = null; - } + string? propertyName = null; - return propertyName; - } + // Attempt to get the JSON property name from the frame. + byte[]? utf8PropertyName = frame.JsonPropertyName; + if (utf8PropertyName == null) + { + // Attempt to get the JSON property name from the JsonPropertyInfo. + utf8PropertyName = frame.JsonPropertyInfo?.JsonPropertyName; + if (utf8PropertyName == null) + { + // Attempt to get the JSON property name set manually for dictionary + // keys and serializer re-entry cases where a property is specified. + propertyName = frame.JsonPropertyNameAsString; + } + } - /// - /// Bytes consumed in the current loop - /// - public long BytesConsumed; + if (utf8PropertyName != null) + { + propertyName = JsonHelpers.Utf8GetString(utf8PropertyName); + } - /// - /// Internal flag to let us know that we need to read ahead in the inner read loop. - /// - internal bool ReadAhead; + return propertyName; + } + } } } 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 84f6638c11ebd2..dfdfd19376c587 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 @@ -2,360 +2,100 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections; using System.Collections.Generic; using System.Diagnostics; -using System.Runtime.CompilerServices; namespace System.Text.Json { [DebuggerDisplay("ClassType.{JsonClassInfo.ClassType}, {JsonClassInfo.Type.Name}")] internal struct ReadStackFrame { - // The object (POCO or IEnumerable) that is being populated - public object? ReturnValue; - public JsonClassInfo? JsonClassInfo; - - // Support Dictionary keys. - public string? KeyName; - - // Support JSON Path on exceptions. - public byte[]? JsonPropertyName; - // Current property values. public JsonPropertyInfo? JsonPropertyInfo; + public StackFramePropertyState PropertyState; + public bool UseExtensionProperty; - // Delegate used to add elements to the current property. - public object? AddObjectToEnumerable; - - // Support System.Array and other types that don't implement IList. - public IList? TempEnumerableValues; - - // Has an array or dictionary property been initialized. - public bool CollectionPropertyInitialized; + // Support JSON Path on exceptions. + public byte[]? JsonPropertyName; // This is Utf8 since we don't want to convert to string until an exception is thown. + public string? JsonPropertyNameAsString; // This is used for dictionary keys and re-entry cases that specify a property name. - // The current JSON data for a property does not match a given POCO, so ignore the property (recursively). - public bool Drain; + // Validation state. + public int OriginalDepth; + public JsonTokenType OriginalTokenType; - // Preserve Reference - public bool IsPreservedArray; - public bool IsNestedPreservedArray; - public MetadataPropertyName LastSeenMetadataProperty; - public string? ReferenceId; + // Current object (POCO or IEnumerable). + public object? ReturnValue; // The current return value used for re-entry. + public JsonClassInfo JsonClassInfo; + public StackFrameObjectState ObjectState; // State tracking the current object. - // Support IDictionary constructible types, i.e. types that we - // support by passing and IDictionary to their constructors: - // immutable dictionaries, Hashtable, SortedList - public IDictionary? TempDictionaryValues; + // Preserve reference. + public string? MetadataId; // For performance, we order the properties by the first deserialize and PropertyIndex helps find the right slot quicker. public int PropertyIndex; public List? PropertyRefCache; - /// - /// Is the current object an Enumerable or Dictionary. - /// - public bool IsProcessingCollectionObject() - { - return IsProcessingObject(ClassType.Enumerable | ClassType.Dictionary); - } + // Add method delegate for Non-generic Stack and Queue; and types that derive from them. + public object? AddMethodDelegate; - /// - /// Is the current property an Enumerable or Dictionary. - /// - public bool IsProcessingCollectionProperty() + public void EndProperty() { - return IsProcessingProperty(ClassType.Enumerable | ClassType.Dictionary); - } + JsonPropertyInfo = null!; + JsonPropertyName = null; + JsonPropertyNameAsString = null; + PropertyState = StackFramePropertyState.None; + MetadataId = null; - /// - /// Is the current object or property an Enumerable or Dictionary. - /// - public bool IsProcessingCollection() - { - return IsProcessingObject(ClassType.Enumerable | ClassType.Dictionary) || - IsProcessingProperty(ClassType.Enumerable | ClassType.Dictionary); + // No need to clear these since they are overwritten each time: + // UseExtensionProperty } - /// - /// Is the current object or property a Dictionary. - /// - public bool IsProcessingDictionary() + public void EndElement() { - return IsProcessingObject(ClassType.Dictionary) || - IsProcessingProperty(ClassType.Dictionary); + JsonPropertyNameAsString = null; + PropertyState = StackFramePropertyState.None; } - /// - /// Is the current object or property an Enumerable. - /// - public bool IsProcessingEnumerable() + public void InitializeReEntry(Type type, JsonSerializerOptions options, string? propertyName) { - return IsProcessingObject(ClassType.Enumerable) || - IsProcessingProperty(ClassType.Enumerable); - } + JsonClassInfo jsonClassInfo = options.GetOrAddClass(type); + Debug.Assert(jsonClassInfo.ClassType != ClassType.Invalid); - /// - /// Is the current object of the provided . - /// - public bool IsProcessingObject(ClassType classTypes) - { - Debug.Assert(JsonClassInfo != null); - return (JsonClassInfo.ClassType & classTypes) != 0; + // The initial JsonPropertyInfo will be used to obtain the converter. + JsonPropertyInfo = jsonClassInfo.PolicyProperty!; + + // Set for exception handling calculation of JsonPath. + JsonPropertyNameAsString = propertyName; } /// - /// Is the current property of the provided . + /// Is the current object a Dictionary. /// - public bool IsProcessingProperty(ClassType classTypes) + public bool IsProcessingDictionary() { - return JsonPropertyInfo != null && - !JsonPropertyInfo.IsPropertyPolicy && - (JsonPropertyInfo.ClassType & classTypes) != 0; + return (JsonClassInfo.ClassType & ClassType.Dictionary) != 0; } /// - /// Determine whether a StartObject or StartArray token should be treated as a value. - /// This allows read-ahead functionality required for Streams so that a custom converter - /// does not run out of data and fail. + /// Is the current object an Enumerable. /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsProcessingValue() - { - if (SkipProperty) - { - return false; - } - - ClassType classType; - - if (CollectionPropertyInitialized) - { - Debug.Assert(JsonPropertyInfo != null && JsonPropertyInfo.ElementClassInfo != null); - classType = JsonPropertyInfo.ElementClassInfo.ClassType; - } - else if (JsonPropertyInfo == null) - { - Debug.Assert(JsonClassInfo != null); - classType = JsonClassInfo.ClassType; - } - else - { - classType = JsonPropertyInfo.ClassType; - } - - // A ClassType.Value indicates the object has a converter and ClassType.Unknown indicates the - // property or element type is System.Object. - // System.Object is treated as a JsonElement which requires returning true from this - // method in order to properly read-ahead (since JsonElement has a custom converter). - return (classType & (ClassType.Value | ClassType.Unknown)) != 0; - } - - public void Initialize(Type type, JsonSerializerOptions options) - { - JsonClassInfo = options.GetOrAddClass(type); - InitializeJsonPropertyInfo(); - } - - public void InitializeJsonPropertyInfo() + public bool IsProcessingEnumerable() { - if (IsProcessingObject(ClassType.Value | ClassType.Enumerable | ClassType.Dictionary)) - { - JsonPropertyInfo = JsonClassInfo!.PolicyProperty; - } + return (JsonClassInfo.ClassType & ClassType.Enumerable) != 0; } public void Reset() { - Drain = false; - JsonClassInfo = null; + AddMethodDelegate = null; + JsonClassInfo = null!; + ObjectState = StackFrameObjectState.None; + OriginalDepth = 0; + OriginalTokenType = JsonTokenType.None; + PropertyIndex = 0; PropertyRefCache = null; ReturnValue = null; - IsPreservedArray = false; - IsNestedPreservedArray = false; - EndObject(); - } - public void EndObject() - { - PropertyIndex = 0; EndProperty(); } - - public void EndProperty() - { - AddObjectToEnumerable = null; - CollectionPropertyInitialized = false; - JsonPropertyInfo = null; - TempEnumerableValues = null; - TempDictionaryValues = null; - JsonPropertyName = null; - KeyName = null; - } - - public static object? CreateEnumerableValue(ref ReadStack state) - { - Debug.Assert(state.Current.JsonPropertyInfo != null); - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo; - - // If the property has an EnumerableConverter, then we use tempEnumerableValues. - if (jsonPropertyInfo.EnumerableConverter != null) - { - IList converterList; - JsonClassInfo elementClassInfo = jsonPropertyInfo.ElementClassInfo!; - if (elementClassInfo.ClassType == ClassType.Value) - { - converterList = elementClassInfo.PolicyProperty!.CreateConverterList(); - } - else - { - converterList = new List(); - } - - state.Current.TempEnumerableValues = converterList; - - // Clear the value if present to ensure we don't confuse TempEnumerableValues with the collection. - if (!jsonPropertyInfo.IsPropertyPolicy && jsonPropertyInfo.CanBeNull) - { - jsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, null); - } - - return null; - } - - JsonClassInfo runtimeClassInfo = jsonPropertyInfo.RuntimeClassInfo; - - if (runtimeClassInfo.CreateObject == null) - { - ThrowHelper.ThrowNotSupportedException_DeserializeCreateObjectDelegateIsNull(jsonPropertyInfo.DeclaredPropertyType); - } - - return runtimeClassInfo.CreateObject(); - } - - public static object? CreateDictionaryValue(ref ReadStack state) - { - Debug.Assert(state.Current.JsonPropertyInfo != null); - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo; - - // If the property has a DictionaryConverter, then we use tempDictionaryValues. - if (jsonPropertyInfo.DictionaryConverter != null) - { - IDictionary converterDictionary; - JsonClassInfo elementClassInfo = jsonPropertyInfo.ElementClassInfo!; - if (elementClassInfo.ClassType == ClassType.Value) - { - converterDictionary = elementClassInfo.PolicyProperty!.CreateConverterDictionary(); - } - else - { - converterDictionary = new Dictionary(); - } - - state.Current.TempDictionaryValues = converterDictionary; - - // Clear the value if present to ensure we don't confuse TempDictionaryValues with the collection. - if (!jsonPropertyInfo.IsPropertyPolicy && jsonPropertyInfo.CanBeNull) - { - jsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, null); - } - - return null; - } - - JsonClassInfo runtimeClassInfo = jsonPropertyInfo.RuntimeClassInfo; - - if (runtimeClassInfo.CreateObject == null) - { - ThrowHelper.ThrowNotSupportedException_DeserializeCreateObjectDelegateIsNull(jsonPropertyInfo.DeclaredPropertyType); - } - - return runtimeClassInfo.CreateObject(); - } - - public Type GetElementType() - { - Debug.Assert(JsonPropertyInfo != null); - if (IsProcessingCollectionProperty()) - { - return JsonPropertyInfo.ElementClassInfo!.Type; - } - - if (IsProcessingCollectionObject()) - { - return JsonClassInfo!.ElementClassInfo!.Type; - } - - return JsonPropertyInfo.RuntimePropertyType; - } - - public static IEnumerable? GetEnumerableValue(ref ReadStackFrame current) - { - if (current.IsProcessingObject(ClassType.Enumerable)) - { - if (current.ReturnValue != null) - { - return (IEnumerable)current.ReturnValue; - } - } - - // IEnumerable properties are finished (values added inline) unless they are using tempEnumerableValues. - return current.TempEnumerableValues; - } - - public void DetermineEnumerablePopulationStrategy(object targetEnumerable) - { - Debug.Assert(JsonPropertyInfo!.ClassType == ClassType.Enumerable); - - if (JsonPropertyInfo.RuntimeClassInfo.AddItemToObject != null) - { - if (!JsonPropertyInfo.TryCreateEnumerableAddMethod(targetEnumerable, out object? addMethodDelegate)) - { - // No "add" method for this collection, hence, not supported for deserialization. - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection( - JsonPropertyInfo.DeclaredPropertyType, - JsonPropertyInfo.ParentClassType, - JsonPropertyInfo.PropertyInfo); - } - - AddObjectToEnumerable = addMethodDelegate; - } - else if (targetEnumerable is IList targetList) - { - if (targetList.IsReadOnly) - { - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection( - JsonPropertyInfo.DeclaredPropertyType, - JsonPropertyInfo.ParentClassType, - JsonPropertyInfo.PropertyInfo); - } - } - // If there's no add method, and we can't cast to IList, this collection is not supported for deserialization. - else - { - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection( - JsonPropertyInfo.DeclaredPropertyType, - JsonPropertyInfo.ParentClassType, - JsonPropertyInfo.PropertyInfo); - } - } - - public void DetermineIfDictionaryCanBePopulated(object targetDictionary) - { - Debug.Assert(JsonPropertyInfo!.ClassType == ClassType.Dictionary); - - if (!JsonPropertyInfo.CanPopulateDictionary(targetDictionary)) - { - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection( - JsonPropertyInfo.DeclaredPropertyType, - JsonPropertyInfo.ParentClassType, - JsonPropertyInfo.PropertyInfo); - } - } - - public bool SkipProperty => Drain || - JsonPropertyInfo != null && - JsonPropertyInfo.IsPropertyPolicy == false && - JsonPropertyInfo.ShouldDeserialize == false; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReflectionEmitMemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReflectionEmitMemberAccessor.cs index cd5135ea7959c6..c410ef024790fa 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReflectionEmitMemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReflectionEmitMemberAccessor.cs @@ -2,13 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#if BUILDING_INBOX_LIBRARY +#if NETFRAMEWORK || NETCOREAPP +using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; -using System.Runtime.CompilerServices; -namespace System.Text.Json +namespace System.Text.Json.Serialization { internal sealed class ReflectionEmitMemberAccessor : MemberAccessor { @@ -55,96 +55,77 @@ internal sealed class ReflectionEmitMemberAccessor : MemberAccessor return (JsonClassInfo.ConstructorDelegate)dynamicMethod.CreateDelegate(typeof(JsonClassInfo.ConstructorDelegate)); } - public override Action CreateAddDelegate(MethodInfo addMethod, object target) + public override Action CreateAddMethodDelegate() { - Debug.Assert(addMethod != null && target != null); - return (Action)addMethod.CreateDelegate(typeof(Action), target); - } - - [PreserveDependency(".ctor()", "System.Text.Json.ImmutableEnumerableCreator`2")] - public override ImmutableCollectionCreator ImmutableCollectionCreateRange(Type constructingType, Type collectionType, Type elementType) - { - MethodInfo createRange = ImmutableCollectionCreateRangeMethod(constructingType, elementType); - - Type creatorType = typeof(ImmutableEnumerableCreator<,>).MakeGenericType(elementType, collectionType); - - ConstructorInfo? realMethod = creatorType.GetConstructor( - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, - binder: null, - Type.EmptyTypes, - modifiers: null); + Type collectionType = typeof(TCollection); + Type elementType = typeof(object); - Debug.Assert(realMethod != null); + // We verified this won't be null when we created the converter that calls this method. + MethodInfo realMethod = (collectionType.GetMethod("Push") ?? collectionType.GetMethod("Enqueue"))!; var dynamicMethod = new DynamicMethod( - ConstructorInfo.ConstructorName, - typeof(object), - Type.EmptyTypes, + realMethod.Name, + typeof(void), + new[] { collectionType, elementType }, typeof(ReflectionEmitMemberAccessor).Module, skipVisibility: true); ILGenerator generator = dynamicMethod.GetILGenerator(); - generator.Emit(OpCodes.Newobj, realMethod); - generator.Emit(OpCodes.Ret); - JsonClassInfo.ConstructorDelegate constructor = (JsonClassInfo.ConstructorDelegate)dynamicMethod.CreateDelegate( - typeof(JsonClassInfo.ConstructorDelegate)); - - ImmutableCollectionCreator? creator = (ImmutableCollectionCreator?)constructor(); + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Ldarg_1); + generator.Emit(OpCodes.Callvirt, realMethod); + generator.Emit(OpCodes.Ret); - Debug.Assert(creator != null); - creator.RegisterCreatorDelegateFromMethod(createRange); - return creator; + return (Action)dynamicMethod.CreateDelegate(typeof(Action)); } - [PreserveDependency(".ctor()", "System.Text.Json.ImmutableDictionaryCreator`2")] - public override ImmutableCollectionCreator ImmutableDictionaryCreateRange(Type constructingType, Type collectionType, Type elementType) + public override Func, TCollection> CreateImmutableEnumerableCreateRangeDelegate() { - Debug.Assert(collectionType.IsGenericType); + Type collectionType = typeof(TCollection); + MethodInfo realMethod = collectionType.GetImmutableEnumerableCreateRangeMethod(typeof(TElement)); - // Only string keys are allowed. - if (collectionType.GetGenericArguments()[0] != typeof(string)) - { - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(collectionType, parentType: null, memberInfo: null); - } + var dynamicMethod = new DynamicMethod( + realMethod.Name, + collectionType, + new[] { typeof(IEnumerable) }, + typeof(ReflectionEmitMemberAccessor).Module, + skipVisibility: true); - MethodInfo createRange = ImmutableDictionaryCreateRangeMethod(constructingType, elementType); + ILGenerator generator = dynamicMethod.GetILGenerator(); - Type creatorType = typeof(ImmutableDictionaryCreator<,>).MakeGenericType(elementType, collectionType); + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Call, realMethod); + generator.Emit(OpCodes.Ret); - ConstructorInfo? realMethod = creatorType.GetConstructor( - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, - binder: null, - Type.EmptyTypes, - modifiers: null); + return (Func, TCollection>)dynamicMethod.CreateDelegate(typeof(Func, TCollection>)); + } - Debug.Assert(realMethod != null); + public override Func>, TCollection> CreateImmutableDictionaryCreateRangeDelegate() + { + Type collectionType = typeof(TCollection); + MethodInfo realMethod = collectionType.GetImmutableDictionaryCreateRangeMethod(typeof(TElement)); var dynamicMethod = new DynamicMethod( - ConstructorInfo.ConstructorName, - typeof(object), - Type.EmptyTypes, + realMethod.Name, + collectionType, + new[] { typeof(IEnumerable>) }, typeof(ReflectionEmitMemberAccessor).Module, skipVisibility: true); ILGenerator generator = dynamicMethod.GetILGenerator(); - generator.Emit(OpCodes.Newobj, realMethod); - generator.Emit(OpCodes.Ret); - - JsonClassInfo.ConstructorDelegate constructor = (JsonClassInfo.ConstructorDelegate)dynamicMethod.CreateDelegate( - typeof(JsonClassInfo.ConstructorDelegate)); - ImmutableCollectionCreator? creator = (ImmutableCollectionCreator?)constructor(); + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Call, realMethod); + generator.Emit(OpCodes.Ret); - Debug.Assert(creator != null); - creator.RegisterCreatorDelegateFromMethod(createRange); - return creator; + return (Func>, TCollection>)dynamicMethod.CreateDelegate(typeof(Func>, TCollection>)); } - public override Func CreatePropertyGetter(PropertyInfo propertyInfo) => - (Func)CreatePropertyGetter(propertyInfo, typeof(TClass)); + public override Func CreatePropertyGetter(PropertyInfo propertyInfo) => + (Func)CreatePropertyGetter(propertyInfo, propertyInfo.DeclaringType!, typeof(TProperty)); - private static Delegate CreatePropertyGetter(PropertyInfo propertyInfo, Type classType) + private static Delegate CreatePropertyGetter(PropertyInfo propertyInfo, Type classType, Type propertyType) { MethodInfo? realMethod = propertyInfo.GetGetMethod(); Type objectType = typeof(object); @@ -152,7 +133,7 @@ private static Delegate CreatePropertyGetter(PropertyInfo propertyInfo, Type cla Debug.Assert(realMethod != null); var dynamicMethod = new DynamicMethod( realMethod.Name, - propertyInfo.PropertyType, + propertyType, new[] { objectType }, typeof(ReflectionEmitMemberAccessor).Module, skipVisibility: true); @@ -174,13 +155,13 @@ private static Delegate CreatePropertyGetter(PropertyInfo propertyInfo, Type cla generator.Emit(OpCodes.Ret); - return dynamicMethod.CreateDelegate(typeof(Func<,>).MakeGenericType(objectType, propertyInfo.PropertyType)); + return dynamicMethod.CreateDelegate(typeof(Func<,>).MakeGenericType(objectType, propertyType)); } - public override Action CreatePropertySetter(PropertyInfo propertyInfo) => - (Action)CreatePropertySetter(propertyInfo, typeof(TClass)); + public override Action CreatePropertySetter(PropertyInfo propertyInfo) => + (Action)CreatePropertySetter(propertyInfo, propertyInfo.DeclaringType!, typeof(TProperty)); - private static Delegate CreatePropertySetter(PropertyInfo propertyInfo, Type classType) + private static Delegate CreatePropertySetter(PropertyInfo propertyInfo, Type classType, Type propertyType) { MethodInfo? realMethod = propertyInfo.GetSetMethod(); Type objectType = typeof(object); @@ -189,7 +170,7 @@ private static Delegate CreatePropertySetter(PropertyInfo propertyInfo, Type cla var dynamicMethod = new DynamicMethod( realMethod.Name, typeof(void), - new[] { objectType, propertyInfo.PropertyType }, + new[] { objectType, propertyType }, typeof(ReflectionEmitMemberAccessor).Module, skipVisibility: true); @@ -212,7 +193,7 @@ private static Delegate CreatePropertySetter(PropertyInfo propertyInfo, Type cla generator.Emit(OpCodes.Ret); - return dynamicMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(objectType, propertyInfo.PropertyType)); + return dynamicMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(objectType, propertyType)); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReflectionMemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReflectionMemberAccessor.cs index 0560db9adfe5ce..f04927fa3c0cff 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReflectionMemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReflectionMemberAccessor.cs @@ -2,28 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using System.Diagnostics; using System.Reflection; -using System.Runtime.CompilerServices; -namespace System.Text.Json +namespace System.Text.Json.Serialization { internal sealed class ReflectionMemberAccessor : MemberAccessor { - private delegate TProperty GetProperty(TClass obj); - private delegate TProperty GetPropertyByRef(ref TClass obj); - - private delegate void SetProperty(TClass obj, TProperty value); - private delegate void SetPropertyByRef(ref TClass obj, TProperty value); - - private delegate Func GetPropertyByRefFactory(GetPropertyByRef set); - private delegate Action SetPropertyByRefFactory(SetPropertyByRef set); - - private static readonly MethodInfo s_createStructPropertyGetterMethod = new GetPropertyByRefFactory(CreateStructPropertyGetter!) - .Method.GetGenericMethodDefinition(); - - private static readonly MethodInfo s_createStructPropertySetterMethod = new SetPropertyByRefFactory(CreateStructPropertySetter!) - .Method.GetGenericMethodDefinition(); public override JsonClassInfo.ConstructorDelegate? CreateConstructor(Type type) { Debug.Assert(type != null); @@ -42,119 +28,51 @@ internal sealed class ReflectionMemberAccessor : MemberAccessor return () => Activator.CreateInstance(type); } - public override Action CreateAddDelegate(MethodInfo addMethod, object target) + public override Action CreateAddMethodDelegate() { - Debug.Assert(addMethod != null && target != null); - return (Action)addMethod.CreateDelegate(typeof(Action), target); - } - - [PreserveDependency(".ctor()", "System.Text.Json.ImmutableEnumerableCreator`2")] - public override ImmutableCollectionCreator ImmutableCollectionCreateRange(Type constructingType, Type collectionType, Type elementType) - { - MethodInfo createRange = ImmutableCollectionCreateRangeMethod(constructingType, elementType); - - Type creatorType = typeof(ImmutableEnumerableCreator<,>).MakeGenericType(elementType, collectionType); - ConstructorInfo constructor = creatorType.GetConstructor( - BindingFlags.Public | - BindingFlags.NonPublic | - BindingFlags.Instance, binder: null, - Type.EmptyTypes, - modifiers: null)!; - - ImmutableCollectionCreator creator = (ImmutableCollectionCreator)constructor.Invoke(Array.Empty()); - creator.RegisterCreatorDelegateFromMethod(createRange); - return creator; - } + Type collectionType = typeof(TCollection); + Type elementType = typeof(object); - [PreserveDependency(".ctor()", "System.Text.Json.ImmutableDictionaryCreator`2")] - public override ImmutableCollectionCreator ImmutableDictionaryCreateRange(Type constructingType, Type collectionType, Type elementType) - { - Debug.Assert(collectionType.IsGenericType); + // We verified this won't be null when we created the converter for the collection type. + MethodInfo addMethod = (collectionType.GetMethod("Push") ?? collectionType.GetMethod("Enqueue"))!; - // Only string keys are allowed. - if (collectionType.GetGenericArguments()[0] != typeof(string)) + return delegate (TCollection collection, object? element) { - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(collectionType, parentType: null, memberInfo: null); - } - - MethodInfo createRange = ImmutableDictionaryCreateRangeMethod(constructingType, elementType); - - Type creatorType = typeof(ImmutableDictionaryCreator<,>).MakeGenericType(elementType, collectionType); - ConstructorInfo constructor = creatorType.GetConstructor( - BindingFlags.Public | - BindingFlags.NonPublic | - BindingFlags.Instance, binder: null, - Type.EmptyTypes, - modifiers: null)!; - - ImmutableCollectionCreator creator = (ImmutableCollectionCreator)constructor.Invoke(Array.Empty()); - creator.RegisterCreatorDelegateFromMethod(createRange); - return creator; + addMethod.Invoke(collection, new object[] { element! }); + }; } - public override Func CreatePropertyGetter(PropertyInfo propertyInfo) + public override Func, TCollection> CreateImmutableEnumerableCreateRangeDelegate() { - MethodInfo getMethodInfo = propertyInfo.GetGetMethod()!; - - if (typeof(TClass).IsValueType) - { - var factory = CreateDelegate>(s_createStructPropertyGetterMethod.MakeGenericMethod(typeof(TClass), typeof(TProperty))); - var propertyGetter = CreateDelegate>(getMethodInfo); - - return factory(propertyGetter); - } - else - { - var propertyGetter = CreateDelegate>(getMethodInfo); - return delegate (object? obj) - { - return propertyGetter((TClass)obj!); - }; - } + MethodInfo createRange = typeof(TCollection).GetImmutableEnumerableCreateRangeMethod(typeof(TElement)); + return (Func, TCollection>)createRange.CreateDelegate( + typeof(Func, TCollection>)); } - public override Action CreatePropertySetter(PropertyInfo propertyInfo) + public override Func>, TCollection> CreateImmutableDictionaryCreateRangeDelegate() { - MethodInfo setMethodInfo = propertyInfo.GetSetMethod()!; - - if (typeof(TClass).IsValueType) - { - SetPropertyByRefFactory factory = CreateDelegate>(s_createStructPropertySetterMethod.MakeGenericMethod(typeof(TClass), typeof(TProperty))); - SetPropertyByRef propertySetter = CreateDelegate>(setMethodInfo); - - return factory(propertySetter); - } - else - { - var propertySetter = CreateDelegate>(setMethodInfo); - return delegate (object? obj, TProperty value) - { - propertySetter((TClass)obj!, value); - }; - } + MethodInfo createRange = typeof(TCollection).GetImmutableDictionaryCreateRangeMethod(typeof(TElement)); + return (Func>, TCollection>)createRange.CreateDelegate( + typeof(Func>, TCollection>)); } - private static TDelegate CreateDelegate(MethodInfo methodInfo) - where TDelegate : Delegate + public override Func CreatePropertyGetter(PropertyInfo propertyInfo) { - return (TDelegate)Delegate.CreateDelegate(typeof(TDelegate), methodInfo); - } + MethodInfo getMethodInfo = propertyInfo.GetGetMethod()!; - private static Func CreateStructPropertyGetter(GetPropertyByRef get) - where TClass : struct - { return delegate (object obj) { - return get(ref Unsafe.Unbox(obj)); + return (TProperty)getMethodInfo.Invoke(obj, null)!; }; } - private static Action CreateStructPropertySetter(SetPropertyByRef set) - where TClass : struct + public override Action CreatePropertySetter(PropertyInfo propertyInfo) { + MethodInfo setMethodInfo = propertyInfo.GetSetMethod()!; + return delegate (object obj, TProperty value) { - set(ref Unsafe.Unbox(obj), value); + setMethodInfo.Invoke(obj, new object[] { value! }); }; } } 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 new file mode 100644 index 00000000000000..a1530ab5621d98 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFrameObjectState.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Text.Json +{ + /// + /// The current state of an object or collection that supports continuation. + /// The values are typically compared with the less-than operator so the ordering is important. + /// + internal enum StackFrameObjectState : byte + { + None = 0, + + StartToken, + + MetadataPropertyName, // Read the first $id or $ref. + MetadataIdProperty, // Read value for $id. + MetadataRefProperty, // Read value for $ref. + MetadataRefPropertyEndObject, // Read EndObject for $ref. + MetadataValuesPropertyName, // Read $values property name. + MetadataValuesPropertyStartArray, // Read StartArray for $values. + MetadataPropertyValue, // Whether all metadata properties has been read. + + CreatedObject, + ReadElements, + EndToken, + EndTokenValidation, + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFramePropertyState.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFramePropertyState.cs new file mode 100644 index 00000000000000..0e78bccef5fc1d --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFramePropertyState.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Text.Json +{ + /// + /// The current state of a property that supports continuation. + /// The values are typically compared with the less-than operator so the ordering is important. + /// + internal enum StackFramePropertyState : byte + { + None = 0, + + ReadName, // Read the name of the property. + Name, // Verify or process the name. + ReadValue, // Read the value of the property. + ReadValueIsEnd, // Determine if we are done reading. + TryRead, // Perform the actual call to the converter's TryRead(). + } +} 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 1995ac01ca5e82..4debd5ed506a56 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 @@ -11,63 +11,172 @@ namespace System.Text.Json [DebuggerDisplay("Path:{PropertyPath()} Current: ClassType.{Current.JsonClassInfo.ClassType}, {Current.JsonClassInfo.Type.Name}")] internal struct WriteStack { - // Fields are used instead of properties to avoid value semantics. - public WriteStackFrame Current; + /// + /// The number of stack frames when the continuation started. + /// + private int _continuationCount; + + /// + /// The number of stack frames including Current. _previous will contain _count-1 higher frames. + /// + private int _count; + private List _previous; - private int _index; - // The bag of preservable references. It needs to be kept in the state and never in JsonSerializerOptions because - // the options should not have any per-serialization state since every serialization shares the same immutable state on the options. + // 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. + /// + public int FlushThreshold; + + public bool IsContinuation => _continuationCount != 0; + + // The bag of preservable references. public DefaultReferenceResolver ReferenceResolver; - public void Push() + /// + /// Internal flag to let us know that we need to read ahead in the inner read loop. + /// + public bool SupportContinuation; + + private void AddCurrent() { if (_previous == null) { _previous = new List(); } - if (_index == _previous.Count) + if (_count > _previous.Count) { // Need to allocate a new array element. _previous.Add(Current); } else { - Debug.Assert(_index < _previous.Count); - // Use a previously allocated slot. - _previous[_index] = Current; + _previous[_count - 1] = Current; } - Current.Reset(); - _index++; + _count++; } - public void Push(JsonClassInfo nextClassInfo, object? nextValue) + /// + /// Initializes the state for the first type being serialized. + /// + public void InitializeRoot(Type type, JsonSerializerOptions options, bool supportContinuation) { - Push(); - Current.JsonClassInfo = nextClassInfo; - Current.CurrentValue = nextValue; + JsonClassInfo jsonClassInfo = options.GetOrAddClass(type); + Debug.Assert(jsonClassInfo.ClassType != ClassType.Invalid); + + Current.JsonClassInfo = jsonClassInfo; - ClassType classType = nextClassInfo.ClassType; + if ((jsonClassInfo.ClassType & (ClassType.Enumerable | ClassType.Dictionary)) == 0) + { + Current.DeclaredJsonPropertyInfo = jsonClassInfo.PolicyProperty!; + } - if (classType == ClassType.Enumerable || nextClassInfo.ClassType == ClassType.Dictionary) + if (options.ReferenceHandling.ShouldWritePreservedReferences()) { - Current.PopStackOnEndCollection = true; - Current.JsonPropertyInfo = Current.JsonClassInfo.PolicyProperty; + ReferenceResolver = new DefaultReferenceResolver(writing: true); + } + + SupportContinuation = supportContinuation; + } + + public void Push() + { + if (_continuationCount == 0) + { + if (_count == 0) + { + // The first stack frame is held in Current. + _count = 1; + } + else + { + JsonClassInfo jsonClassInfo = Current.GetPolymorphicJsonPropertyInfo().RuntimeClassInfo; + + AddCurrent(); + Current.Reset(); + + Current.JsonClassInfo = jsonClassInfo; + Current.DeclaredJsonPropertyInfo = jsonClassInfo.PolicyProperty!; + } + } + else if (_continuationCount == 1) + { + // No need for a push since there is only one stack frame. + Debug.Assert(_count == 1); + _continuationCount = 0; } else { - Debug.Assert(nextClassInfo.ClassType == ClassType.Object || nextClassInfo.ClassType == ClassType.Unknown); - Current.PopStackOnEndObject = true; + // A continuation, adjust the index. + Current = _previous[_count - 1]; + + // Check if we are done. + if (_count == _continuationCount) + { + _continuationCount = 0; + } + else + { + _count++; + } } } - public void Pop() + public void Pop(bool success) { - Debug.Assert(_index > 0); - Current = _previous[--_index]; + Debug.Assert(_count > 0); + + if (!success) + { + // Check if we need to initialize the continuation. + if (_continuationCount == 0) + { + if (_count == 1) + { + // No need for a continuation since there is only one stack frame. + _continuationCount = 1; + _count = 1; + } + else + { + AddCurrent(); + _count--; + _continuationCount = _count; + _count--; + Current = _previous[_count - 1]; + } + + return; + } + + if (_continuationCount == 1) + { + // No need for a pop since there is only one stack frame. + Debug.Assert(_count == 1); + return; + } + + // Update the list entry to the current value. + _previous[_count - 1] = Current; + + Debug.Assert(_count > 0); + } + else + { + Debug.Assert(_continuationCount == 0); + } + + if (_count > 1) + { + Current = _previous[--_count - 1]; + } } // Return a property path as a simple JSONPath using dot-notation when possible. When special characters are present, bracket-notation is used: @@ -77,36 +186,49 @@ public string PropertyPath() { StringBuilder sb = new StringBuilder("$"); - for (int i = 0; i < _index; i++) + // If a continuation, always report back full stack. + int count = Math.Max(_count, _continuationCount); + + for (int i = 0; i < count - 1; i++) { AppendStackFrame(sb, _previous[i]); } - AppendStackFrame(sb, Current); - return sb.ToString(); - } + if (_continuationCount == 0) + { + AppendStackFrame(sb, Current); + } - private void AppendStackFrame(StringBuilder sb, in WriteStackFrame frame) - { - // Append the property name. - string? propertyName = frame.JsonPropertyInfo?.PropertyInfo?.Name; - AppendPropertyName(sb, propertyName); - } + return sb.ToString(); - private void AppendPropertyName(StringBuilder sb, string? propertyName) - { - if (propertyName != null) + void AppendStackFrame(StringBuilder sb, in WriteStackFrame frame) { - if (propertyName.IndexOfAny(ReadStack.SpecialCharacters) != -1) + // Append the property name. + string? propertyName = frame.DeclaredJsonPropertyInfo?.PropertyInfo?.Name; + if (propertyName == null) { - sb.Append(@"['"); - sb.Append(propertyName); - sb.Append(@"']"); + // Attempt to get the JSON property name from the property name specified in re-entry. + propertyName = frame.JsonPropertyNameAsString; } - else + + AppendPropertyName(sb, propertyName); + } + + void AppendPropertyName(StringBuilder sb, string? propertyName) + { + if (propertyName != null) { - sb.Append('.'); - sb.Append(propertyName); + if (propertyName.IndexOfAny(ReadStack.SpecialCharacters) != -1) + { + sb.Append(@"['"); + sb.Append(propertyName); + sb.Append(@"']"); + } + else + { + sb.Append('.'); + sb.Append(propertyName); + } } } } 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 75354126a7d2a7..01292e9815cfd5 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 @@ -4,204 +4,120 @@ using System.Collections; using System.Diagnostics; -using System.Runtime.CompilerServices; using System.Text.Json.Serialization; namespace System.Text.Json { + [DebuggerDisplay("ClassType.{JsonClassInfo.ClassType}, {JsonClassInfo.Type.Name}")] internal struct WriteStackFrame { - // The object (POCO or IEnumerable) that is being populated. - public object? CurrentValue; - public JsonClassInfo? JsonClassInfo; - - // Support Dictionary keys. - public string? KeyName; - - // The current IEnumerable or IDictionary. + /// + /// The enumerator for resumable collections. + /// public IEnumerator? CollectionEnumerator; - // Note all bools are kept together for packing: - public bool PopStackOnEndCollection; - - // The current object. - public bool PopStackOnEndObject; - public bool StartObjectWritten; - public bool MoveToNextProperty; - - public bool WriteWrappingBraceOnEndPreservedArray; - // The current property. - public int PropertyEnumeratorIndex; - public ExtensionDataWriteStatus ExtensionDataStatus; - public JsonPropertyInfo? JsonPropertyInfo; - - // Pre-encoded metadata properties. - private static readonly JsonEncodedText s_metadataId = JsonEncodedText.Encode("$id", encoder: null); - private static readonly JsonEncodedText s_metadataRef = JsonEncodedText.Encode("$ref", encoder: null); - private static readonly JsonEncodedText s_metadataValues = JsonEncodedText.Encode("$values", encoder: null); - - public void Initialize(Type type, JsonSerializerOptions options) + /// + /// The original JsonPropertyInfo that is not changed. It contains all properties. + /// + /// + /// For objects, it is either the policy property for the class or the current property. + /// For collections, it is either the policy property for the class or the policy property for the current element. + /// + public JsonPropertyInfo? DeclaredJsonPropertyInfo; + + /// + /// Used when processing extension data dictionaries. + /// + public bool IgnoreDictionaryKeyPolicy; + + /// + /// The class (POCO or IEnumerable) that is being populated. + /// + public JsonClassInfo JsonClassInfo; + + /// + /// Validation state for a class. + /// + public int OriginalDepth; + + // Class-level state for collections. + public bool ProcessedStartToken; + public bool ProcessedEndToken; + + /// + /// Property or Element state. + /// + public StackFramePropertyState PropertyState; + + /// + /// The enumerator index for resumable collections. + /// + public int EnumeratorIndex; + + // This is used for re-entry cases for exception handling. + public string? JsonPropertyNameAsString; + + // Preserve Reference + public MetadataPropertyName MetadataPropertyName; + + /// + /// The run-time JsonPropertyInfo that contains the ClassInfo and ConverterBase for polymorphic scenarios. + /// + /// + /// For objects, it is either the policy property for the class or the policy property for the current property. + /// For collections, it is either the policy property for the class or the policy property for the current element. + /// + public JsonPropertyInfo? PolymorphicJsonPropertyInfo; + + public void EndDictionaryElement() { - JsonClassInfo = options.GetOrAddClass(type); - if ((JsonClassInfo.ClassType & (ClassType.Value | ClassType.Enumerable | ClassType.Dictionary)) != 0) - { - JsonPropertyInfo = JsonClassInfo.PolicyProperty; - } + PropertyState = StackFramePropertyState.None; } - public void WriteObjectOrArrayStart(ClassType classType, Utf8JsonWriter writer, JsonSerializerOptions options, bool writeNull = false) - { - if (JsonPropertyInfo?.EscapedName.HasValue == true) - { - WriteObjectOrArrayStart(classType, JsonPropertyInfo.EscapedName!.Value, writer, writeNull); - } - else if (KeyName != null) - { - JsonEncodedText propertyName = JsonEncodedText.Encode(KeyName, options.Encoder); - WriteObjectOrArrayStart(classType, propertyName, writer, writeNull); - } - else - { - Debug.Assert(writeNull == false); - - // Write start without a property name. - if (classType == ClassType.Object || classType == ClassType.Dictionary) - { - writer.WriteStartObject(); - StartObjectWritten = true; - } - else - { - Debug.Assert(classType == ClassType.Enumerable); - writer.WriteStartArray(); - } - } - } - - private void WriteObjectOrArrayStart(ClassType classType, JsonEncodedText propertyName, Utf8JsonWriter writer, bool writeNull) - { - if (writeNull) - { - writer.WriteNull(propertyName); - } - else if ((classType & (ClassType.Object | ClassType.Dictionary)) != 0) - { - writer.WriteStartObject(propertyName); - StartObjectWritten = true; - } - else - { - Debug.Assert(classType == ClassType.Enumerable); - writer.WriteStartArray(propertyName); - } - } - - public void WritePreservedObjectOrArrayStart(ClassType classType, Utf8JsonWriter writer, JsonSerializerOptions options, string referenceId) + public void EndProperty() { - if (JsonPropertyInfo?.EscapedName.HasValue == true) - { - writer.WriteStartObject(JsonPropertyInfo.EscapedName!.Value); - } - else if (KeyName != null) - { - writer.WriteStartObject(KeyName); - } - else - { - writer.WriteStartObject(); - } - - - writer.WriteString(s_metadataId, referenceId); - - if ((classType & (ClassType.Object | ClassType.Dictionary)) != 0) - { - StartObjectWritten = true; - } - else - { - // Wrap array into an object with $id and $values metadata properties. - Debug.Assert(classType == ClassType.Enumerable); - writer.WriteStartArray(s_metadataValues); - WriteWrappingBraceOnEndPreservedArray = true; - } + DeclaredJsonPropertyInfo = null!; + JsonPropertyNameAsString = null; + PolymorphicJsonPropertyInfo = null; + PropertyState = StackFramePropertyState.None; } - public void WriteReferenceObject(Utf8JsonWriter writer, JsonSerializerOptions options, string referenceId) + /// + /// Return the property that contains the correct polymorphic properties including + /// the ClassType and ConverterBase. + /// + public JsonPropertyInfo GetPolymorphicJsonPropertyInfo() { - if (JsonPropertyInfo?.EscapedName.HasValue == true) - { - writer.WriteStartObject(JsonPropertyInfo.EscapedName!.Value); - } - else if (KeyName != null) - { - writer.WriteStartObject(KeyName); - } - else - { - writer.WriteStartObject(); - } - - writer.WriteString(s_metadataRef, referenceId); - writer.WriteEndObject(); + return PolymorphicJsonPropertyInfo ?? DeclaredJsonPropertyInfo!; } - public void Reset() + /// + /// Initializes the state for polymorphic or re-entry cases. + /// + public JsonConverter InitializeReEntry(Type type, JsonSerializerOptions options, string? propertyName = null) { - CurrentValue = null; - CollectionEnumerator = null; - ExtensionDataStatus = ExtensionDataWriteStatus.NotStarted; - JsonClassInfo = null; - PropertyEnumeratorIndex = 0; - PopStackOnEndCollection = false; - PopStackOnEndObject = false; - StartObjectWritten = false; + JsonClassInfo newClassInfo = options.GetOrAddClass(type); - EndProperty(); - } + // todo: check if type==newtype and skip below? - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void EndProperty() - { - JsonPropertyInfo = null; - KeyName = null; - MoveToNextProperty = false; - WriteWrappingBraceOnEndPreservedArray = false; - } + // Set for exception handling calculation of JsonPath. + JsonPropertyNameAsString = propertyName; - public void EndDictionary() - { - CollectionEnumerator = null; - PopStackOnEndCollection = false; + PolymorphicJsonPropertyInfo = newClassInfo.PolicyProperty!; + return PolymorphicJsonPropertyInfo.ConverterBase; } - public void EndArray() + public void Reset() { CollectionEnumerator = null; - PopStackOnEndCollection = false; - } + EnumeratorIndex = 0; + IgnoreDictionaryKeyPolicy = false; + JsonClassInfo = null!; + OriginalDepth = 0; + ProcessedStartToken = false; + ProcessedEndToken = false; - // AggressiveInlining used although a large method it is only called from one location and is on a hot path. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void NextProperty() - { EndProperty(); - - Debug.Assert(JsonClassInfo != null && JsonClassInfo.PropertyCacheArray != null); - int maxPropertyIndex = JsonClassInfo.PropertyCacheArray.Length; - - ++PropertyEnumeratorIndex; - if (PropertyEnumeratorIndex >= maxPropertyIndex) - { - if (PropertyEnumeratorIndex > maxPropertyIndex) - { - ExtensionDataStatus = ExtensionDataWriteStatus.Finished; - } - else if (JsonClassInfo.DataExtensionProperty != null) - { - ExtensionDataStatus = ExtensionDataWriteStatus.Writing; - } - } } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index a03e0c3e233d31..475f5138a80df6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -21,44 +21,37 @@ public static void ThrowArgumentException_DeserializeWrongType(Type type, object [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static NotSupportedException GetNotSupportedException_SerializationNotSupportedCollection(Type propertyType, Type? parentType, MemberInfo? memberInfo) + public static NotSupportedException GetNotSupportedException_SerializationNotSupported(Type propertyType, Type? parentType, MemberInfo? memberInfo) { if (parentType != null && parentType != typeof(object) && memberInfo != null) { - return new NotSupportedException(SR.Format(SR.SerializationNotSupportedCollection, propertyType, $"{parentType}.{memberInfo.Name}")); + return new NotSupportedException(SR.Format(SR.SerializationNotSupported, propertyType, $"{parentType}.{memberInfo.Name}")); } - return new NotSupportedException(SR.Format(SR.SerializationNotSupportedCollectionType, propertyType)); - } - - [DoesNotReturn] - public static void ThrowInvalidOperationException_SerializerCycleDetected(int maxDepth) - { - throw new JsonException(SR.Format(SR.SerializerCycleDetected, maxDepth)); + return new NotSupportedException(SR.Format(SR.SerializationNotSupportedType, propertyType)); } [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType) + public static NotSupportedException ThrowNotSupportedException_SerializationNotSupported(Type propertyType, Type? parentType = null, MemberInfo? memberInfo = null) { - var ex = new JsonException(SR.Format(SR.DeserializeUnableToConvertValue, propertyType)); - ex.AppendPathInformation = true; - throw ex; + throw GetNotSupportedException_SerializationNotSupported(propertyType, parentType, memberInfo); } [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType, string path, Exception? innerException = null) + public static void ThrowInvalidOperationException_SerializerCycleDetected(int maxDepth) { - string message = SR.Format(SR.DeserializeUnableToConvertValue, propertyType) + $" Path: {path}."; - throw new JsonException(message, path, null, null, innerException); + throw new JsonException(SR.Format(SR.SerializerCycleDetected, maxDepth)); } [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowJsonException_DepthTooLarge(int currentDepth, JsonSerializerOptions options) + public static void ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType) { - throw new JsonException(SR.Format(SR.DepthTooLarge, currentDepth, options.EffectiveMaxDepth)); + var ex = new JsonException(SR.Format(SR.DeserializeUnableToConvertValue, propertyType)); + ex.AppendPathInformation = true; + throw ex; } [DoesNotReturn] @@ -161,9 +154,11 @@ public static void ThrowInvalidOperationException_SerializerDictionaryKeyNull(Ty [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void ReThrowWithPath(in ReadStack readStack, JsonReaderException ex) + public static void ReThrowWithPath(in ReadStack state, JsonReaderException ex) { - string path = readStack.JsonPath(); + Debug.Assert(ex.Path == null); + + string path = state.JsonPath(); string message = ex.Message; // Insert the "Path" portion before "LineNumber" and "BytePositionInLine". @@ -182,14 +177,14 @@ public static void ReThrowWithPath(in ReadStack readStack, JsonReaderException e [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void ReThrowWithPath(in ReadStack readStack, in Utf8JsonReader reader, Exception ex) + public static void ReThrowWithPath(in ReadStack state, in Utf8JsonReader reader, Exception ex) { JsonException jsonException = new JsonException(null, ex); - AddExceptionInformation(readStack, reader, jsonException); + AddExceptionInformation(state, reader, jsonException); throw jsonException; } - public static void AddExceptionInformation(in ReadStack readStack, in Utf8JsonReader reader, JsonException ex) + public static void AddExceptionInformation(in ReadStack state, in Utf8JsonReader reader, JsonException ex) { long lineNumber = reader.CurrentState._lineNumber; ex.LineNumber = lineNumber; @@ -197,7 +192,7 @@ public static void AddExceptionInformation(in ReadStack readStack, in Utf8JsonRe long bytePositionInLine = reader.CurrentState._bytePositionInLine; ex.BytePositionInLine = bytePositionInLine; - string path = readStack.JsonPath(); + string path = state.JsonPath(); ex.Path = path; string? message = ex._message; @@ -205,10 +200,10 @@ public static void AddExceptionInformation(in ReadStack readStack, in Utf8JsonRe if (string.IsNullOrEmpty(message)) { // Use a default message. - Type? propertyType = readStack.Current.JsonPropertyInfo?.RuntimePropertyType; + Type? propertyType = state.Current.JsonPropertyInfo?.RuntimePropertyType; if (propertyType == null) { - propertyType = readStack.Current.JsonClassInfo?.Type; + propertyType = state.Current.JsonClassInfo?.Type; } message = SR.Format(SR.DeserializeUnableToConvertValue, propertyType); @@ -224,16 +219,16 @@ public static void AddExceptionInformation(in ReadStack readStack, in Utf8JsonRe [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void ReThrowWithPath(in WriteStack writeStack, Exception ex) + public static void ReThrowWithPath(in WriteStack state, Exception ex) { JsonException jsonException = new JsonException(null, ex); - AddExceptionInformation(writeStack, jsonException); + AddExceptionInformation(state, jsonException); throw jsonException; } - public static void AddExceptionInformation(in WriteStack writeStack, JsonException ex) + public static void AddExceptionInformation(in WriteStack state, JsonException ex) { - string path = writeStack.PropertyPath(); + string path = state.PropertyPath(); ex.Path = path; string? message = ex._message; @@ -280,7 +275,7 @@ public static void ThrowInvalidOperationException_SerializationDataExtensionProp [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowNotSupportedException_DeserializeCreateObjectDelegateIsNull(Type invalidType) + public static void ThrowNotSupportedException_DeserializeNoParameterlessConstructor(Type invalidType) { if (invalidType.IsInterface) { @@ -315,34 +310,20 @@ public static void ThrowJsonException_MetadataValueWasNotString(JsonTokenType to [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties() + public static void ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(ReadOnlySpan propertyName, ref ReadStack state) { + state.Current.JsonPropertyName = propertyName.ToArray(); ThrowJsonException(SR.MetadataReferenceCannotContainOtherProperties); } [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties_Dictionary(ref ReadStackFrame current) - { - current.KeyName = null; - ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(); - } - - [DoesNotReturn] - [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowJsonException_MetadataIdIsNotFirstProperty() + public static void ThrowJsonException_MetadataIdIsNotFirstProperty(ReadOnlySpan propertyName, ref ReadStack state) { + state.Current.JsonPropertyName = propertyName.ToArray(); ThrowJsonException(SR.MetadataIdIsNotFirstProperty); } - [DoesNotReturn] - [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowJsonException_MetadataIdIsNotFirstProperty_Dictionary(ref ReadStackFrame current) - { - current.KeyName = null; - ThrowJsonException_MetadataIdIsNotFirstProperty(); - } - [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowJsonException_MetadataMissingIdBeforeValues() @@ -357,7 +338,7 @@ public static void ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSi // Set PropertyInfo or KeyName to write down the conflicting property name in JsonException.Path if (state.Current.IsProcessingDictionary()) { - state.Current.KeyName = reader.GetString(); + state.Current.JsonPropertyNameAsString = reader.GetString(); } else { @@ -369,8 +350,11 @@ public static void ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSi [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowJsonException_MetadataDuplicateIdFound(string id) + public static void ThrowJsonException_MetadataDuplicateIdFound(string id, ref ReadStack state) { + // Set so JsonPath throws exception with $id in it. + state.Current.JsonPropertyName = JsonSerializer.s_metadataId.EncodedUtf8Bytes.ToArray(); + ThrowJsonException(SR.Format(SR.MetadataDuplicateIdFound, id)); } @@ -383,10 +367,9 @@ public static void ThrowJsonException_MetadataInvalidReferenceToValueType(Type p [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowJsonException_MetadataPreservedArrayInvalidProperty(in Utf8JsonReader reader, ref ReadStack state) + public static void ThrowJsonException_MetadataPreservedArrayInvalidProperty(Type propertyType, in Utf8JsonReader reader) { string propertyName = reader.GetString()!; - Type propertyType = JsonSerializer.GetValuesPropertyInfoFromJsonPreservableArrayRef(ref state.Current).DeclaredPropertyType; ThrowJsonException(SR.Format(SR.MetadataPreservedArrayFailed, SR.Format(SR.MetadataPreservedArrayInvalidProperty, propertyName), @@ -408,5 +391,26 @@ public static void ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmu { ThrowJsonException(SR.Format(SR.MetadataCannotParsePreservedObjectToImmutable, propertyType)); } + + [DoesNotReturn] + internal static void ThrowUnexpectedMetadataException( + ReadOnlySpan propertyName, + ref Utf8JsonReader reader, + ref ReadStack state) + { + MetadataPropertyName name = JsonSerializer.GetMetadataPropertyName(propertyName); + if (name == MetadataPropertyName.Id) + { + ThrowJsonException_MetadataIdIsNotFirstProperty(propertyName, ref state); + } + else if (name == MetadataPropertyName.Ref) + { + ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(propertyName, ref state); + } + else + { + ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(propertyName, ref state, reader); + } + } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/Array.ReadTests.cs b/src/libraries/System.Text.Json/tests/Serialization/Array.ReadTests.cs index 4032c28032778f..45b4d2c5ecbaca 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Array.ReadTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Array.ReadTests.cs @@ -629,5 +629,45 @@ static void TestRoundTrip(ClassWithNonNullEnumerableGetters obj) obj = JsonSerializer.Deserialize(inputJsonWithNullCollections); TestRoundTrip(obj); } + + [Fact] + public static void DoNotDependOnPropertyGetterWhenDeserializingCollections() + { + Dealer dealer = new Dealer { NetworkCodeList = new List { "Network1", "Network2" } }; + + string serialized = JsonSerializer.Serialize(dealer); + Assert.Equal(@"{""NetworkCodeList"":[""Network1"",""Network2""]}", serialized); + + dealer = JsonSerializer.Deserialize(serialized); + + List expected = new List { "Network1", "Network2" }; + int i = 0; + + foreach (string str in dealer.NetworkCodeList) + { + Assert.Equal(expected[i], str); + i++; + } + + Assert.Equal("Network1,Network2", dealer.Networks); + } + + class Dealer + { + private string _networks; + + [JsonIgnore] + public string Networks + { + get => _networks; + set => _networks = value ?? string.Empty; + } + + public IEnumerable NetworkCodeList + { + get => !string.IsNullOrEmpty(Networks) ? Networks?.Split(',') : new string[0]; + set => Networks = (value != null) ? string.Join(",", value) : string.Empty; + } + } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.BadConverters.cs b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.BadConverters.cs index 493e80a5eb520d..d7b8bf8ed491e2 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.BadConverters.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.BadConverters.cs @@ -132,11 +132,12 @@ public static void AttributeOnClassFail() Assert.Contains(expectedSubStr, ex.Message.Substring(pos + expectedSubStr.Length)); } - private class ConverterThatReturnsNull : JsonConverterFactory + private class ConverterFactoryThatReturnsNull : JsonConverterFactory { public override bool CanConvert(Type typeToConvert) { - return true; + // To verify the nullable converter, don't convert Nullable. + return Nullable.GetUnderlyingType(typeToConvert) == null; } public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) @@ -149,10 +150,18 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer public static void ConverterThatReturnsNullFail() { var options = new JsonSerializerOptions(); - options.Converters.Add(new ConverterThatReturnsNull()); + options.Converters.Add(new ConverterFactoryThatReturnsNull()); + + // A null return value from CreateConverter() will generate a NotSupportedException with the type name. + NotSupportedException ex = Assert.Throws(() => JsonSerializer.Serialize(0, options)); + Assert.Contains(typeof(int).ToString(), ex.Message); + + ex = Assert.Throws(() => JsonSerializer.Deserialize("0", options)); + Assert.Contains(typeof(int).ToString(), ex.Message); - Assert.Throws(() => JsonSerializer.Serialize(0, options)); - Assert.Throws(() => JsonSerializer.Deserialize("0", options)); + // This will invoke the Nullable converter which should detect a null converter. + ex = Assert.Throws(() => JsonSerializer.Deserialize("0", options)); + Assert.Contains(typeof(int).ToString(), ex.Message); } private class Level1 @@ -232,8 +241,8 @@ public static void ConverterReadTooLittle() } catch (JsonException ex) { - Assert.Contains("$.Level2.Level3s[1]", ex.ToString()); - Assert.Equal("$.Level2.Level3s[1]", ex.Path); + Assert.Contains("$.Level2.Level3s[0]", ex.ToString()); + Assert.Equal("$.Level2.Level3s[0]", ex.Path); } } @@ -252,8 +261,8 @@ public static void ConverterReadTooMuch() } catch (JsonException ex) { - Assert.Contains("$.Level2.Level3s[1]", ex.ToString()); - Assert.Equal("$.Level2.Level3s[1]", ex.Path); + Assert.Contains("$.Level2.Level3s[0]", ex.ToString()); + Assert.Equal("$.Level2.Level3s[0]", ex.Path); } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/CyclicTests.cs b/src/libraries/System.Text.Json/tests/Serialization/CyclicTests.cs index f4b138bfa45bd6..62ebd5d06fd8ac 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/CyclicTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/CyclicTests.cs @@ -45,7 +45,8 @@ public static void WriteCyclicFail(int depth) JsonException ex = Assert.Throws(() => JsonSerializer.Serialize(rootObj, options)); // Exception should contain the path and MaxDepth. - string expectedPath = "$" + string.Concat(Enumerable.Repeat(".Parent", depth)); + // Since the last Parent property is null, the serializer moves onto the Children property. + string expectedPath = "$" + string.Concat(Enumerable.Repeat(".Parent", depth - 1)); Assert.Contains(expectedPath, ex.Path); Assert.Contains(depth.ToString(), ex.ToString()); } diff --git a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs index dfdeb5cf5d4b74..5e061ce37610ee 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs @@ -949,11 +949,22 @@ public override bool CanConvert(Type typeToConvert) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new MyStuffConverter(); + if (typeToConvert == typeof(IClass)) + { + return new MyStuffConverterForIClass(); + } + else if (typeToConvert == typeof(MyClass)) + { + return new MyStuffConverterForMyClass(); + } + else + { + throw new InvalidOperationException(); + } } } - private class MyStuffConverter : JsonConverter + private class MyStuffConverterForIClass : JsonConverter { public override IClass Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -966,6 +977,19 @@ public override void Write(Utf8JsonWriter writer, IClass value, JsonSerializerOp } } + private class MyStuffConverterForMyClass : JsonConverter + { + public override MyClass Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new MyClass(); + } + + public override void Write(Utf8JsonWriter writer, MyClass value, JsonSerializerOptions options) + { + writer.WriteNumberValue(1); + } + } + // This method generates 316 unique test cases for nested dictionaries up to 4 // levels deep, along with matching JSON, encompassing the various planes of // dictionaries that can be combined: generic, non-generic, BCL, user-derived, @@ -1938,7 +1962,8 @@ public IEnumerator> GetEnumerator() IEnumerator IEnumerable.GetEnumerator() { - return GetEnumerator(); + // This GetEnumerator() should not be called. + throw new NotImplementedException(); } public bool Remove(string key) @@ -2008,11 +2033,11 @@ public static void DictionaryOfTOnlyWithStringPoco() Assert.Equal(Json, json); } - public class DictionaryThatHasIncomatibleEnumerator : IDictionary + public class DictionaryThatHasIncompatibleEnumerator : IDictionary { - IDictionary _inner = new Dictionary(); + Hashtable _inner = new Hashtable(); - public TValue this[string key] + public object this[string key] { get { @@ -2024,72 +2049,66 @@ public TValue this[string key] } } - public ICollection Keys => _inner.Keys; + public ICollection Keys => _inner.Keys; - public ICollection Values => _inner.Values; + public ICollection Values => _inner.Values; public int Count => _inner.Count; public bool IsReadOnly => _inner.IsReadOnly; - public void Add(string key, TValue value) - { - _inner.Add(key, value); - } + public bool IsFixedSize => _inner.IsFixedSize; - public void Add(KeyValuePair item) - { - _inner.Add(item); - } + public bool IsSynchronized => throw new NotImplementedException(); - public void Clear() - { - throw new NotImplementedException(); - } + public object SyncRoot => throw new NotImplementedException(); - public bool Contains(KeyValuePair item) + public object this[object key] { - return _inner.Contains(item); + get + { + return _inner[key]; + } + set + { + _inner[key] = value; + } } - public bool ContainsKey(string key) + public void Add(object key, object value) { - return _inner.ContainsKey(key); + _inner.Add(key, value); } - public void CopyTo(KeyValuePair[] array, int arrayIndex) + public void Clear() { - // CopyTo should not be called. throw new NotImplementedException(); } - public IEnumerator> GetEnumerator() + IEnumerator IEnumerable.GetEnumerator() { - // The generic GetEnumerator() should not be called for this test. throw new NotImplementedException(); } - IEnumerator IEnumerable.GetEnumerator() + public bool Contains(object key) { - // Create an incompatible converter. - return new int[] { 100, 200 }.GetEnumerator(); + throw new NotImplementedException(); } - public bool Remove(string key) + public IDictionaryEnumerator GetEnumerator() { - // Remove should not be called. - throw new NotImplementedException(); + // Throw NotSupportedException so we can detect this GetEnumerator was called. + throw new NotSupportedException(); } - public bool Remove(KeyValuePair item) + public void Remove(object key) { - // Remove should not be called. throw new NotImplementedException(); } - public bool TryGetValue(string key, out TValue value) + public void CopyTo(Array array, int index) { - return _inner.TryGetValue(key, out value); + throw new NotImplementedException(); } } @@ -2098,26 +2117,34 @@ public static void VerifyDictionaryThatHasIncomatibleEnumeratorWithInt() { const string Json = @"{""One"":1,""Two"":2}"; - DictionaryThatHasIncomatibleEnumerator dictionary; - dictionary = JsonSerializer.Deserialize>(Json); - Assert.Equal(1, dictionary["One"]); - Assert.Equal(2, dictionary["Two"]); + DictionaryThatHasIncompatibleEnumerator dictionary; + dictionary = JsonSerializer.Deserialize(Json); + Assert.Equal(1, ((JsonElement)dictionary["One"]).GetInt32()); + Assert.Equal(2, ((JsonElement)dictionary["Two"]).GetInt32()); Assert.Throws(() => JsonSerializer.Serialize(dictionary)); } - [Fact] public static void VerifyDictionaryThatHasIncomatibleEnumeratorWithPoco() { const string Json = @"{""One"":{""Id"":1},""Two"":{""Id"":2}}"; - DictionaryThatHasIncomatibleEnumerator dictionary; - dictionary = JsonSerializer.Deserialize>(Json); - Assert.Equal(1, dictionary["One"].Id); - Assert.Equal(2, dictionary["Two"].Id); + DictionaryThatHasIncompatibleEnumerator dictionary; + dictionary = JsonSerializer.Deserialize(Json); + Assert.Equal(1, ((JsonElement)dictionary["One"]).GetProperty("Id").GetInt32()); + Assert.Equal(2, ((JsonElement)dictionary["Two"]).GetProperty("Id").GetInt32()); Assert.Throws(() => JsonSerializer.Serialize(dictionary)); } + + [Fact] + public static void VerifyIDictionaryWithNonStringKey() + { + IDictionary dictionary = new Hashtable(); + dictionary.Add(1, "value"); + Assert.Throws(() => JsonSerializer.Serialize(dictionary)); + } + public class ClassWithoutParameterlessCtor { public ClassWithoutParameterlessCtor(int num) { } diff --git a/src/libraries/System.Text.Json/tests/Serialization/ExceptionTests.cs b/src/libraries/System.Text.Json/tests/Serialization/ExceptionTests.cs index f876a34ffdb378..d374f7c3eff20f 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ExceptionTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ExceptionTests.cs @@ -429,5 +429,33 @@ public class ChildClass public Dictionary MyDictionary { get; set; } public ChildClass[] Children { get; set; } } + + private class ClassWithInvalidArray + { + public int[,] UnsupportedArray { get; set; } + } + + private class ClassWithInvalidDictionary + { + public Dictionary UnsupportedDictionary { get; set; } + } + + [Fact] + public static void ClassWithUnsupportedCollectionTypes() + { + Exception ex; + + ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""UnsupportedArray"":[]}")); + + // The exception should contain the parent type and the property name. + Assert.Contains("ClassWithInvalidArray.UnsupportedArray", ex.ToString()); + + ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""UnsupportedDictionary"":{}}")); + Assert.Contains("System.Int32[,]", ex.ToString()); + + // The exception for element types do not contain the parent type and the property name + // since the verification occurs later and is no longer bound to the parent type. + Assert.DoesNotContain("ClassWithInvalidDictionary.UnsupportedDictionary", ex.ToString()); + } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs b/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs index aa319b2017aa3e..b23ef614c13071 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Xunit; @@ -653,8 +655,9 @@ public static void ExtensionProperty_InvalidDictionary() ClassWithInvalidExtensionPropertyStringString obj1 = new ClassWithInvalidExtensionPropertyStringString(); Assert.Throws(() => JsonSerializer.Serialize(obj1)); + // This fails with NotSupportedException since all Dictionaries currently need to have a string TKey. ClassWithInvalidExtensionPropertyObjectString obj2 = new ClassWithInvalidExtensionPropertyObjectString(); - Assert.Throws(() => JsonSerializer.Serialize(obj2)); + Assert.Throws(() => JsonSerializer.Serialize(obj2)); } private class ClassWithExtensionPropertyAlreadyInstantiated @@ -687,5 +690,70 @@ private class ClassWithMultipleDictionaries public Dictionary ActualDictionary { get; set; } } + + [Fact] + public static void CustomObjectConverterInExtensionProperty() + { + const string Json = "{\"hello\": \"world\"}"; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonObjectConverter()); + + ClassWithExtensionPropertyAsObject obj = JsonSerializer.Deserialize(Json, options); + object overflowProp = obj.MyOverflow["hello"]; + Assert.IsType(overflowProp); + Assert.Equal("world!!!", ((string)overflowProp)); + + string newJson = JsonSerializer.Serialize(obj, options); + Assert.Equal("{\"hello\":\"world!!!\"}", newJson); + } + + private class JsonObjectConverter : JsonConverter + { + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetString() + "!!!"; + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + // Since we are converter for object, the string converter will be called instead of this. + throw new InvalidOperationException(); + } + } + + [Fact] + public static void CustomJsonElementConverterInExtensionProperty() + { + const string Json = "{\"hello\": \"world\"}"; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonElementConverter()); + + ClassWithExtensionPropertyAsJsonElement obj = JsonSerializer.Deserialize(Json, options); + JsonElement overflowProp = obj.MyOverflow["hello"]; + Assert.Equal(JsonValueKind.Undefined, overflowProp.ValueKind); + + string newJson = JsonSerializer.Serialize(obj, options); + Assert.Equal("{\"hello\":{\"Hi\":\"There\"}}", newJson); + } + + private class JsonElementConverter : JsonConverter + { + public override JsonElement Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Just return an empty JsonElement. + reader.Skip(); + return new JsonElement(); + } + + public override void Write(Utf8JsonWriter writer, JsonElement value, JsonSerializerOptions options) + { + // Write a string we can test against easily. + writer.WriteStartObject(); + writer.WriteString("Hi", "There"); + writer.WriteEndObject(); + } + } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs index cc9f383f26c4cf..1fe8fcbb152f99 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs @@ -159,26 +159,17 @@ public static void JsonIgnoreAttribute_UnsupportedCollection() // Using new options instance to prevent using previously cached metadata. JsonSerializerOptions options = new JsonSerializerOptions(); - string serialized = JsonSerializer.Serialize(new ClassWithUnsupportedDictionary(), options); - - // Object keys are fine on serialization if the keys are strings. - Assert.Contains(@"""MyConcurrentDict"":null", serialized); - Assert.Contains(@"""MyIDict"":null", serialized); - Assert.Contains(@"""MyDict"":null", serialized); + + // Unsupported collections will throw on serialize by default. + Assert.Throws(() => JsonSerializer.Serialize(new ClassWithUnsupportedDictionary(), options)); // Unsupported collections will throw on deserialize by default. options = new JsonSerializerOptions(); Assert.Throws(() => JsonSerializer.Deserialize(wrapperJson, options)); options = new JsonSerializerOptions(); - serialized = JsonSerializer.Serialize(new WrapperForClassWithUnsupportedDictionary(), options); - - // Object keys are fine on serialization if the keys are strings. - Assert.Contains(@"{""MyClass"":{", serialized); - Assert.Contains(@"""MyConcurrentDict"":null", serialized); - Assert.Contains(@"""MyIDict"":null", serialized); - Assert.Contains(@"""MyDict"":null", serialized); - Assert.Contains("}}", serialized); + // Unsupported collections will throw on serialize by default. + Assert.Throws(() => JsonSerializer.Serialize(new WrapperForClassWithUnsupportedDictionary(), options)); // When ignored, we can serialize and deserialize without exceptions. options = new JsonSerializerOptions(); diff --git a/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs b/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs index ff14e2d4c810b7..a11af8356c842b 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs @@ -22,6 +22,14 @@ private class EmployeeWithContacts } #region Root Object + [Fact] + public static void ObjectWithoutMetadata() + { + string json = "{}"; + Employee employee = JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve); + Assert.NotNull(employee); + } + [Fact] //Employee list as a property and then use reference to itself on nested Employee. public static void ObjectReferenceLoop() { @@ -250,6 +258,14 @@ private static void TestRefTask() #endregion #region Root Dictionary + [Fact] + public static void DictionaryWithoutMetadata() + { + string json = "{}"; + Dictionary dictionary = JsonSerializer.Deserialize>(json, s_deserializerOptionsPreserve); + Assert.NotNull(dictionary); + } + [Fact] //Employee list as a property and then use reference to itself on nested Employee. public static void DictionaryReferenceLoop() { @@ -766,7 +782,7 @@ public static void JsonPathObject() } [Fact] - public static void JsonPathImcompletePropertyAfterMetadata() + public static void JsonPathIncompletePropertyAfterMetadata() { string json = @"{ @@ -774,7 +790,8 @@ public static void JsonPathImcompletePropertyAfterMetadata() ""Nam"; JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); - Assert.Equal("$.$id", ex.Path); + // Since $id was parsed correctly, the path should just be "$". + Assert.Equal("$", ex.Path); } [Fact] @@ -786,7 +803,8 @@ public static void JsonPathIncompleteMetadataAfterProperty() ""$i"; JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); - Assert.Equal("$.Name", ex.Path); + // Since "Name" was parsed correctly, the path should just be "$". + Assert.Equal("$", ex.Path); } [Fact] @@ -889,6 +907,8 @@ public static void ThrowOnStructWithReference() ]"; JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, s_deserializerOptionsPreserve)); + Assert.Equal("$[1].$ref", ex.Path); + Assert.Contains($"'{typeof(EmployeeStruct)}'", ex.Message); } #endregion @@ -912,11 +932,11 @@ public static void ImmutableEnumerableAsRoot() JsonException ex; ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, s_deserializerOptionsPreserve)); - Assert.Equal("$", ex.Path); + Assert.Equal("$.$id", ex.Path); Assert.Contains($"'{typeof(ImmutableList)}'", ex.Message); ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); - Assert.Equal("$", ex.Path); + Assert.Equal("$.$id", ex.Path); Assert.Contains($"'{typeof(EmployeeWithImmutable[])}'", ex.Message); } @@ -930,7 +950,7 @@ public static void ImmutableDictionaryAsRoot() }"; JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, s_deserializerOptionsPreserve)); - Assert.Equal("$", ex.Path); + Assert.Equal("$.$id", ex.Path); Assert.Contains($"'{typeof(ImmutableDictionary)}'", ex.Message); } @@ -949,7 +969,7 @@ public static void ImmutableEnumerableAsProperty() JsonException ex; ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); - Assert.Equal("$.Subordinates", ex.Path); + Assert.Equal("$.Subordinates.$id", ex.Path); Assert.Contains($"'{typeof(ImmutableList)}'", ex.Message); json = @@ -962,7 +982,7 @@ public static void ImmutableEnumerableAsProperty() }"; ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); - Assert.Equal("$.SubordinatesArray", ex.Path); + Assert.Equal("$.SubordinatesArray.$id", ex.Path); Assert.Contains($"'{typeof(EmployeeWithImmutable[])}'", ex.Message); } @@ -979,7 +999,7 @@ public static void ImmutableDictionaryAsProperty() }"; JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); - Assert.Equal("$.Contacts", ex.Path); + Assert.Equal("$.Contacts.$id", ex.Path); Assert.Contains($"'{typeof(ImmutableDictionary)}'", ex.Message); } @@ -1094,7 +1114,7 @@ public static void ReferenceObjectsShouldNotContainMoreProperties() }"; JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); - Assert.Equal("$.Manager", ex.Path); + Assert.Equal("$.Manager.$ref", ex.Path); //Regular dictionary key before $ref json = @"{ @@ -1105,7 +1125,7 @@ public static void ReferenceObjectsShouldNotContainMoreProperties() }"; ex = Assert.Throws(() => JsonSerializer.Deserialize>>(json, s_deserializerOptionsPreserve)); - Assert.Equal("$.Angela", ex.Path); + Assert.Equal("$.Angela.$ref", ex.Path); //Regular property after $ref json = @"{ @@ -1118,7 +1138,7 @@ public static void ReferenceObjectsShouldNotContainMoreProperties() }"; ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); - Assert.Equal("$.Manager", ex.Path); + Assert.Equal("$.Manager.Name", ex.Path); //Metadata property before $ref json = @"{ @@ -1131,7 +1151,7 @@ public static void ReferenceObjectsShouldNotContainMoreProperties() }"; ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); - Assert.Equal("$.Manager", ex.Path); + Assert.Equal("$.Manager.$ref", ex.Path); //Metadata property after $ref json = @"{ @@ -1144,7 +1164,7 @@ public static void ReferenceObjectsShouldNotContainMoreProperties() }"; ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); - Assert.Equal("$.Manager", ex.Path); + Assert.Equal("$.Manager.$id", ex.Path); } [Fact] @@ -1164,6 +1184,25 @@ public static void ReferenceObjectBeforePreservedObject() Assert.Contains("'1'", ex.Message); Assert.Equal("$[0].$ref", ex.Path); } + + [Theory] + [MemberData(nameof(ReadSuccessCases))] + public static void ReadTestClassesWithExtensionOption(Type classType, byte[] data) + { + var options = new JsonSerializerOptions { ReferenceHandling = ReferenceHandling.Preserve }; + object obj = JsonSerializer.Deserialize(data, classType, options); + Assert.IsAssignableFrom(obj); + ((ITestClass)obj).Verify(); + } + + public static IEnumerable ReadSuccessCases + { + get + { + return TestData.ReadSuccessCases; + } + } + #endregion #region Preserved objects ($id) @@ -1181,7 +1220,7 @@ public static void MoreThanOneId() JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); - Assert.Equal("$", ex.Path); + Assert.Equal("$.$id", ex.Path); } [Fact] @@ -1196,7 +1235,7 @@ public static void IdIsNotFirstProperty() }"; JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); - Assert.Equal("$", ex.Path); + Assert.Equal("$.$id", ex.Path); json = @"{ ""Name"": ""Angela"", @@ -1204,7 +1243,7 @@ public static void IdIsNotFirstProperty() }"; ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, s_deserializerOptionsPreserve)); - Assert.Equal("$", ex.Path); + Assert.Equal("$.$id", ex.Path); } [Fact] @@ -1258,8 +1297,8 @@ public static void PreservedArrayWithoutValues() }"; JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, s_deserializerOptionsPreserve)); - // Not sure if is ok for Path to have this value. - Assert.Equal("$.$id", ex.Path); + Assert.Equal("$", ex.Path); + Assert.Contains("$values", ex.Message); Assert.Contains(typeof(List).ToString(), ex.Message); } @@ -1325,7 +1364,7 @@ public static void PreservedArrayExtraProperties() JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, s_deserializerOptionsPreserve)); - Assert.Equal("$", ex.Path); + Assert.Equal("$.LeadingProperty", ex.Path); Assert.Contains(typeof(List).ToString(), ex.Message); json = @"{ @@ -1336,7 +1375,7 @@ public static void PreservedArrayExtraProperties() ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, s_deserializerOptionsPreserve)); - Assert.Equal("$", ex.Path); + Assert.Equal("$.TrailingProperty", ex.Path); Assert.Contains(typeof(List).ToString(), ex.Message); Assert.Contains("TrailingProperty", ex.Message); } diff --git a/src/libraries/System.Text.Json/tests/Serialization/Stream.ReadTests.cs b/src/libraries/System.Text.Json/tests/Serialization/Stream.ReadTests.cs index d6d9818eee6f8d..d5387642f72fe2 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Stream.ReadTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Stream.ReadTests.cs @@ -230,5 +230,84 @@ void VerifyElement(int index) Assert.Equal(2, value[index].GetProperty("Id").GetInt32()); } } + + [Theory] + [InlineData(1)] + [InlineData(16)] + [InlineData(32000)] + public static async Task ReadPrimitiveWithWhitespace(int bufferSize) + { + byte[] data = Encoding.UTF8.GetBytes("42" + new string(' ', 16 * 1024)); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.DefaultBufferSize = bufferSize; + + using (MemoryStream stream = new MemoryStream(data)) + { + int i = await JsonSerializer.DeserializeAsync(stream, options); + Assert.Equal(42, i); + Assert.Equal(16386, stream.Position); + } + } + + [Theory] + [InlineData(1)] + [InlineData(16)] + [InlineData(32000)] + public static async Task ReadObjectWithWhitespace(int bufferSize) + { + byte[] data = Encoding.UTF8.GetBytes("{}" + new string(' ', 16 * 1024)); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.DefaultBufferSize = bufferSize; + + using (MemoryStream stream = new MemoryStream(data)) + { + SimpleTestClass obj = await JsonSerializer.DeserializeAsync(stream, options); + Assert.Equal(16386, stream.Position); + } + } + + [Theory] + [InlineData(1)] + [InlineData(16)] + [InlineData(32000)] + public static async Task ReadPrimitiveWithWhitespaceAndThenInvalid(int bufferSize) + { + byte[] data = Encoding.UTF8.GetBytes("42" + new string(' ', 16 * 1024) + "!"); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.DefaultBufferSize = bufferSize; + + using (MemoryStream stream = new MemoryStream(data)) + { + JsonException ex = await Assert.ThrowsAsync(async () => await JsonSerializer.DeserializeAsync(stream, options)); + Assert.Equal(16387, stream.Position); + + // We should get an exception like: '!' is invalid after a single JSON value. + Assert.Contains("!", ex.ToString()); + } + } + + [Theory] + [InlineData(1)] + [InlineData(16)] + [InlineData(32000)] + public static async Task ReadObjectWithWhitespaceAndThenInvalid(int bufferSize) + { + byte[] data = Encoding.UTF8.GetBytes("{}" + new string(' ', 16 * 1024) + "!"); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.DefaultBufferSize = bufferSize; + + using (MemoryStream stream = new MemoryStream(data)) + { + JsonException ex = await Assert.ThrowsAsync(async () => await JsonSerializer.DeserializeAsync(stream, options)); + Assert.Equal(16387, stream.Position); + + // We should get an exception like: '!' is invalid after a single JSON value. + Assert.Contains("!", ex.ToString()); + } + } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/Stream.WriteTests.cs b/src/libraries/System.Text.Json/tests/Serialization/Stream.WriteTests.cs index e19db6995b7393..409b89740e61fe 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Stream.WriteTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Stream.WriteTests.cs @@ -314,10 +314,12 @@ public static async Task VeryLargeJsonFileTest(int payloadSize, bool ignoreNull, [InlineData(2, false, false)] [InlineData(4, false, false)] [InlineData(8, false, false)] - [InlineData(16, false, false)] + [InlineData(16, false, false)] // This results a reader\writer depth of 324 which currently works on all test platforms. public static async Task DeepNestedJsonFileTest(int depthFactor, bool ignoreNull, bool writeIndented) { - int length = 10 * depthFactor; + const int ListLength = 10; + + int length = ListLength * depthFactor; List[] orders = new List[length]; orders[0] = PopulateLargeObject(1); for (int i = 1; i < length; i++ ) @@ -328,7 +330,7 @@ public static async Task DeepNestedJsonFileTest(int depthFactor, bool ignoreNull JsonSerializerOptions options = new JsonSerializerOptions() { - MaxDepth = depthFactor * 64, + MaxDepth = (ListLength * depthFactor * 2) + 4, // Order-to-RelatedOrder has a depth of 2. IgnoreNullValues = ignoreNull, WriteIndented = writeIndented }; @@ -356,10 +358,12 @@ public static async Task DeepNestedJsonFileTest(int depthFactor, bool ignoreNull [Theory] [InlineData(1)] - [InlineData(16)] - public static async Task DeepNestedJsonFileCircularDependencyTest(int depthFactor) + [InlineData(4)] + public static async Task NestedJsonFileCircularDependencyTest(int depthFactor) { - int length = 10 * depthFactor; + const int ListLength = 2; + + int length = ListLength * depthFactor; List[] orders = new List[length]; orders[0] = PopulateLargeObject(1000); for (int i = 1; i < length; i++) @@ -367,14 +371,18 @@ public static async Task DeepNestedJsonFileCircularDependencyTest(int depthFacto orders[i] = PopulateLargeObject(1); orders[i - 1][0].RelatedOrder = orders[i]; } - orders[length - 1][0].RelatedOrder = orders[0]; JsonSerializerOptions options = new JsonSerializerOptions() { - MaxDepth = depthFactor * 64, IgnoreNullValues = true }; + // Ensure no exception for default settings (MaxDepth=64) and no cycle. + JsonSerializer.Serialize(orders[0], options); + + // Create a cycle. + orders[length - 1][0].RelatedOrder = orders[0]; + Assert.Throws (() => JsonSerializer.Serialize(orders[0], options)); using (var memoryStream = new MemoryStream()) diff --git a/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.ConcurrentCollections.cs b/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.ConcurrentCollections.cs new file mode 100644 index 00000000000000..ed649d36a0e617 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.ConcurrentCollections.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Concurrent; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static partial class ValueTests + { + [Fact] + public static void Read_ConcurrentCollection() + { + ConcurrentDictionary cd = JsonSerializer.Deserialize>(@"{""key"":""value""}"); + Assert.Equal(1, cd.Count); + Assert.Equal("value", cd["key"]); + + ConcurrentQueue qc = JsonSerializer.Deserialize>(@"[""1""]"); + Assert.Equal(1, qc.Count); + bool found = qc.TryPeek(out string val); + Assert.True(found); + Assert.Equal("1", val); + + ConcurrentStack qs = JsonSerializer.Deserialize>(@"[""1""]"); + Assert.Equal(1, qs.Count); + found = qs.TryPeek(out val); + Assert.True(found); + Assert.Equal("1", val); + } + + [Fact] + public static void Read_ConcurrentCollection_Throws() + { + // Not supported. Not IList, and we don't detect the add method for this collection. + Assert.Throws(() => JsonSerializer.Deserialize>(@"[""1""]")); + + // Not supported. Not IList, and we don't detect the add method for this collection. + Assert.Throws(() => JsonSerializer.Deserialize>(@"[""1""]")); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.GenericCollections.cs b/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.GenericCollections.cs index c305556dc1aefd..30f39ea96c668a 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.GenericCollections.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.GenericCollections.cs @@ -1141,12 +1141,6 @@ public static void ReadSimpleTestClass_GenericWrappers_NoAddMethod_Throws() Assert.Throws(() => JsonSerializer.Deserialize(SimpleTestClassWithStringToStringIReadOnlyDictionaryWrapper.s_json)); } - [Fact] - public static void ReadPrimitiveStringCollection_Throws() - { - Assert.Throws(() => JsonSerializer.Deserialize(@"[""1"", ""2""]")); - } - [Fact] public static void ReadReadOnlyCollections_Throws() { diff --git a/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.SpecializedCollections.cs b/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.SpecializedCollections.cs new file mode 100644 index 00000000000000..57c6902471071e --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.SpecializedCollections.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Specialized; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static partial class ValueTests + { + [Fact] + public static void Read_SpecializedCollection() + { + BitVector32 bv32 = JsonSerializer.Deserialize(@"{""Data"":4}"); + // Data property is skipped because it doesn't have a setter. + Assert.Equal(0, bv32.Data); + + HybridDictionary hd = JsonSerializer.Deserialize(@"{""key"":""value""}"); + Assert.Equal(1, hd.Count); + Assert.Equal("value", ((JsonElement)hd["key"]).GetString()); + + IOrderedDictionary iod = JsonSerializer.Deserialize(@"{""key"":""value""}"); + Assert.Equal(1, iod.Count); + Assert.Equal("value", ((JsonElement)iod["key"]).GetString()); + + ListDictionary ld = JsonSerializer.Deserialize(@"{""key"":""value""}"); + Assert.Equal(1, ld.Count); + Assert.Equal("value", ((JsonElement)ld["key"]).GetString()); + } + + [Fact] + public static void Read_SpecializedCollection_Throws() + { + // Add method for this collection only accepts strings, even though it only implements IList which usually + // indicates that the element type is typeof(object). + Assert.Throws(() => JsonSerializer.Deserialize(@"[""1"", ""2""]")); + + // Not supported. Not IList, and we don't detect the add method for this collection. + Assert.Throws(() => JsonSerializer.Deserialize(@"[{""Key"": ""key"",""Value"":""value""}]")); + + // Int key is not allowed. + Assert.Throws(() => JsonSerializer.Deserialize(@"{1:""value""}")); + + // Runtime type in this case is IOrderedDictionary (we don't replace with concrete type), which we can't instantiate. + Assert.Throws(() => JsonSerializer.Deserialize(@"{""first"":""John"",""second"":""Jane"",""third"":""Jet""}")); + + // Not supported. Not IList, and we don't detect the add method for this collection. + Assert.Throws(() => JsonSerializer.Deserialize(@"[""NameValueCollection""]")); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.ConcurrentCollections.cs b/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.ConcurrentCollections.cs new file mode 100644 index 00000000000000..cb78774b831d19 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.ConcurrentCollections.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Concurrent; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static partial class ValueTests + { + [Fact] + public static void Write_ConcurrentCollection() + { + Assert.Equal(@"[""1""]", JsonSerializer.Serialize(new BlockingCollection { "1" })); + + Assert.Equal(@"[""1""]", JsonSerializer.Serialize(new ConcurrentBag { "1" })); + + Assert.Equal(@"{""key"":""value""}", JsonSerializer.Serialize(new ConcurrentDictionary { ["key"] = "value" })); + + ConcurrentQueue qc = new ConcurrentQueue(); + qc.Enqueue("1"); + Assert.Equal(@"[""1""]", JsonSerializer.Serialize(qc)); + + ConcurrentStack qs = new ConcurrentStack(); + qs.Push("1"); + Assert.Equal(@"[""1""]", JsonSerializer.Serialize(qs)); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.GenericCollections.cs b/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.GenericCollections.cs index a803f71e0349f4..721dc0b751e92a 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.GenericCollections.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.GenericCollections.cs @@ -892,7 +892,6 @@ public static void WriteGenericCollectionWrappers() } [Fact] - // Regression test for https://github.com/dotnet/corefx/issues/39770. public static void ConvertIEnumerableValueTypesThenSerialize() { IEnumerable valueAs = Enumerable.Range(0, 5).Select(x => new ValueA { Value = x }).ToList(); diff --git a/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.ObjectModelCollections.cs b/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.ObjectModelCollections.cs index 8e371832144d7e..d1559ef1f9a028 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.ObjectModelCollections.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.ObjectModelCollections.cs @@ -24,12 +24,12 @@ public static void Write_ObjectModelCollection() Assert.Equal("[true,false]", JsonSerializer.Serialize>(kc)); ReadOnlyCollection roc = new ReadOnlyCollection(new List { true, false }); - Assert.Equal("[true,false]", JsonSerializer.Serialize(oc)); + Assert.Equal("[true,false]", JsonSerializer.Serialize(roc)); ReadOnlyObservableCollection rooc = new ReadOnlyObservableCollection(oc); Assert.Equal("[true,false]", JsonSerializer.Serialize(rooc)); - ReadOnlyDictionary rod = new ReadOnlyDictionary(new Dictionary { ["true"] = false } ); + ReadOnlyDictionary rod = new ReadOnlyDictionary(new Dictionary { ["true"] = false }); Assert.Equal(@"{""true"":false}", JsonSerializer.Serialize(rod)); } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.SpecializedCollections.cs b/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.SpecializedCollections.cs new file mode 100644 index 00000000000000..2b88bbbff7c810 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.SpecializedCollections.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Specialized; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static partial class ValueTests + { + [Fact] + public static void Write_SpecializedCollection() + { + Assert.Equal(@"{""Data"":4}", JsonSerializer.Serialize(new BitVector32(4))); + Assert.Equal(@"{""Data"":4}", JsonSerializer.Serialize(new BitVector32(4))); + + Assert.Equal(@"{""key"":""value""}", JsonSerializer.Serialize(new HybridDictionary { ["key"] = "value" })); + Assert.Equal(@"{""key"":""value""}", JsonSerializer.Serialize(new HybridDictionary { ["key"] = "value" })); + + Assert.Equal(@"{""key"":""value""}", JsonSerializer.Serialize(new OrderedDictionary { ["key"] = "value" })); + Assert.Equal(@"{""key"":""value""}", JsonSerializer.Serialize(new OrderedDictionary { ["key"] = "value" })); + Assert.Equal(@"{""key"":""value""}", JsonSerializer.Serialize(new OrderedDictionary { ["key"] = "value" })); + + Assert.Equal(@"{""key"":""value""}", JsonSerializer.Serialize(new ListDictionary { ["key"] = "value" })); + Assert.Equal(@"{""key"":""value""}", JsonSerializer.Serialize(new ListDictionary { ["key"] = "value" })); + + Assert.Equal(@"[""1"",""2""]", JsonSerializer.Serialize(new StringCollection { "1", "2" })); + Assert.Equal(@"[""1"",""2""]", JsonSerializer.Serialize(new StringCollection { "1", "2" })); + + Assert.Equal(@"[{""Key"":""key"",""Value"":""value""}]", JsonSerializer.Serialize(new StringDictionary { ["key"] = "value" })); + Assert.Equal(@"[{""Key"":""key"",""Value"":""value""}]", JsonSerializer.Serialize(new StringDictionary { ["key"] = "value" })); + + // Element type returned by .GetEnumerator for this type is string, specifically the key. + Assert.Equal(@"[""key""]", JsonSerializer.Serialize(new NameValueCollection { ["key"] = "value" })); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.cs b/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.cs index af5f8e28b17f22..f2a0ba67e8454a 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.cs @@ -22,6 +22,7 @@ public static void WriteStringWithRelaxedEscaper() Assert.NotEqual(expected, JsonSerializer.Serialize(inputString)); } + // todo: move this to object tests; it is not a value test. // https://github.com/dotnet/corefx/issues/40979 [Fact] public static void EscapingShouldntStackOverflow_40979() diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj index 6661230c60b5ab..d101e9fe03f8b7 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj @@ -94,16 +94,20 @@ + + + +