From 5b083defd67e1ddd124478327954c1f7431f447e Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Mon, 27 Jan 2020 17:01:16 -0600 Subject: [PATCH 1/3] New converter model --- .../src/System.Text.Json.csproj | 51 +- .../Text/Json/Serialization/ClassType.cs | 18 +- .../Converters/DefaultArrayConverter.cs | 39 -- .../Converters/DefaultEnumerableConverter.cs | 92 ---- .../DefaultImmutableDictionaryConverter.cs | 72 --- .../DefaultImmutableEnumerableConverter.cs | 143 ------ .../Converters/JsonArrayConverter.cs | 74 +++ .../JsonDictionaryDefaultConverter.cs | 313 ++++++++++++ .../JsonDictionaryOfStringTValueConverter.cs | 89 ++++ .../Converters/JsonICollectionOfTConverter.cs | 110 +++++ .../Converters/JsonIDictionaryConverter.cs | 113 +++++ .../JsonIDictionaryOfStringTValueConverter.cs | 108 ++++ .../Converters/JsonIEnumerableConverter.cs | 80 +++ .../JsonIEnumerableConverterFactory.cs | 158 ++++++ .../JsonIEnumerableDefaultConverter.cs | 295 +++++++++++ .../Converters/JsonIEnumerableOfTConverter.cs | 72 +++ .../Converters/JsonIListConverter.cs | 104 ++++ .../Converters/JsonIListOfTConverter.cs | 104 ++++ .../Converters/JsonListOfTConverter.cs | 67 +++ .../Converters/JsonObjectDefaultConverter.cs | 422 ++++++++++++++++ .../Converters/JsonObjectFactoryConverter.cs | 33 ++ .../JsonValueConverterKeyValuePair.cs | 84 ++-- .../Converters/JsonValueConverterNullable.cs | 41 ++ .../JsonValueConverterNullableFactory.cs | 37 ++ .../DefaultIDictionaryConverter.cs | 18 - .../Serialization/DefaultReferenceResolver.cs | 9 +- .../Json/Serialization/ExtensionMethods.cs | 71 +++ .../JsonClassInfo.AddProperty.cs | 144 +----- .../Text/Json/Serialization/JsonClassInfo.cs | 245 +++------ .../Serialization/JsonConverter.ReadAhead.cs | 74 +++ .../Text/Json/Serialization/JsonConverter.cs | 50 +- .../Serialization/JsonConverterFactory.cs | 41 +- .../Json/Serialization/JsonConverterOfT.cs | 346 ++++++++++++- .../Serialization/JsonDictionaryConverter.cs | 19 +- .../Serialization/JsonEnumerableConverter.cs | 13 - .../Serialization/JsonIEnumerableConverter.cs | 18 + .../Json/Serialization/JsonObjectConverter.cs | 16 + .../Json/Serialization/JsonPropertyInfo.cs | 465 ++---------------- .../Serialization/JsonPropertyInfoCommon.cs | 244 --------- .../JsonPropertyInfoNotNullable.cs | 151 ------ ...sonPropertyInfoNotNullableContravariant.cs | 151 ------ .../Serialization/JsonPropertyInfoNullable.cs | 189 ------- .../JsonPropertyInfoOfTConverter.cs | 190 +++++++ .../JsonResumableConverterOfT.cs | 41 ++ .../JsonSerializer.Read.HandleArray.cs | 347 ------------- .../JsonSerializer.Read.HandleDictionary.cs | 151 ------ .../JsonSerializer.Read.HandleMetadata.cs | 250 +++++++--- .../JsonSerializer.Read.HandleNull.cs | 96 ---- .../JsonSerializer.Read.HandleObject.cs | 176 ------- .../JsonSerializer.Read.HandlePropertyName.cs | 231 +++------ .../JsonSerializer.Read.HandleValue.cs | 43 -- .../JsonSerializer.Read.Helpers.cs | 16 +- .../JsonSerializer.Read.Stream.cs | 34 +- .../JsonSerializer.Read.Utf8JsonReader.cs | 38 +- .../Json/Serialization/JsonSerializer.Read.cs | 194 ++------ .../JsonSerializer.Write.HandleDictionary.cs | 197 -------- .../JsonSerializer.Write.HandleEnumerable.cs | 111 ----- .../JsonSerializer.Write.HandleMetadata.cs | 67 +++ .../JsonSerializer.Write.HandleObject.cs | 169 ------- .../JsonSerializer.Write.Helpers.cs | 63 +-- .../JsonSerializer.Write.Stream.cs | 20 +- .../JsonSerializer.Write.Utf8JsonWriter.cs | 35 ++ .../Serialization/JsonSerializer.Write.cs | 65 +-- .../JsonSerializerOptions.Converters.cs | 24 +- .../Serialization/JsonSerializerOptions.cs | 60 ++- .../Serialization/JsonValueConverterOfT.cs | 40 ++ .../Text/Json/Serialization/MemberAccessor.cs | 4 +- .../Text/Json/Serialization/ReadStack.cs | 308 +++++++++--- .../Text/Json/Serialization/ReadStackFrame.cs | 368 +++----------- .../ReflectionEmitMemberAccessor.cs | 20 +- .../Serialization/ReflectionMemberAccessor.cs | 72 +-- .../Serialization/StackFrameObjectState.cs | 26 + ...teStatus.cs => StackFramePropertyState.cs} | 14 +- .../Text/Json/Serialization/WriteStack.cs | 226 +++++++-- .../Json/Serialization/WriteStackFrame.cs | 266 ++++------ .../Text/Json/ThrowHelper.Serialization.cs | 37 +- .../tests/Serialization/Array.ReadTests.cs | 40 ++ .../CustomConverterTests.BadConverters.cs | 20 +- .../tests/Serialization/CyclicTests.cs | 3 +- .../tests/Serialization/DictionaryTests.cs | 84 ++-- .../tests/Serialization/ExtensionDataTests.cs | 3 +- .../ReferenceHandlingTests.Deserialize.cs | 44 +- .../tests/Serialization/Stream.ReadTests.cs | 79 +++ .../tests/Serialization/Value.WriteTests.cs | 1 + 84 files changed, 4672 insertions(+), 4584 deletions(-) delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultArrayConverter.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultEnumerableConverter.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultImmutableDictionaryConverter.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultImmutableEnumerableConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonArrayConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonDictionaryDefaultConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonDictionaryOfStringTValueConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonICollectionOfTConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIDictionaryConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIDictionaryOfStringTValueConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableConverterFactory.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableDefaultConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableOfTConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIListConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIListOfTConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonListOfTConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectDefaultConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectFactoryConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterNullable.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterNullableFactory.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultIDictionaryConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ExtensionMethods.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.ReadAhead.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonEnumerableConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIEnumerableConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonObjectConverter.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoCommon.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullable.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullableContravariant.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfTConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleNull.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleObject.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleEnumerable.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonValueConverterOfT.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFrameObjectState.cs rename src/libraries/System.Text.Json/src/System/Text/Json/Serialization/{ExtensionDataWriteStatus.cs => StackFramePropertyState.cs} (54%) 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 43c1fa300ec80..fc6d3457db68d 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -24,6 +24,7 @@ + @@ -51,11 +52,26 @@ - - - + + + + + + + + + + + + + + + + + + @@ -64,7 +80,6 @@ - @@ -72,6 +87,8 @@ + + @@ -80,48 +97,37 @@ - - - + - + - + - - - - + - - + - - - - - - + @@ -129,6 +135,7 @@ + @@ -139,6 +146,8 @@ + + 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 e4dad76cb6acf..69448f1ba8333 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 a7e4602c1fb9a..0000000000000 --- 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 5f4635bc90508..0000000000000 --- 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 e19a5f51d7547..0000000000000 --- 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 b347bb59ea1bd..0000000000000 --- 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 0000000000000..56608bba73b0d --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonArrayConverter.cs @@ -0,0 +1,74 @@ +// 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 +{ + /// + /// Converter for System.Array. + /// + internal sealed class JsonArrayConverter + : JsonIEnumerableDefaultConverter + where TCollection: IEnumerable + { + internal override bool CanHaveMetadata => false; + + protected override void Add(TElement value, ref ReadStack state) + { + ((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) + { + List list = (List)state.Current.ReturnValue!; + state.Current.ReturnValue = list.ToArray(); + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + { + 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; + } + + state.Current.EndElement(); + } + } + + 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 0000000000000..468d33d6ec174 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonDictionaryDefaultConverter.cs @@ -0,0 +1,313 @@ +// 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.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.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 = state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!.ConverterBase as JsonConverter; + if (converter == null) + { + state.Current.JsonClassInfo.ElementClassInfo.PolicyProperty.ThrowCollectionNotSupportedException(); + } + + 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 JsonConverter GetValueConverter(ref WriteStack state) + { + JsonConverter converter = (JsonConverter)state.Current.DeclaredJsonPropertyInfo.ConverterBase; + if (converter == null) + { + state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!.ThrowCollectionNotSupportedException(); + } + + return converter!; + } + + internal override sealed 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) + { + while (true) + { + // Read the key name. + reader.Read(); + + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + state.Current.KeyName = reader.GetString(); + + // Read the value and add. + reader.Read(); + TValue element = elementConverter.Read(ref reader, typeof(TValue), options); + Add(element, options, ref state); + } + } + else + { + while (true) + { + // Read the key name. + reader.Read(); + + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + state.Current.KeyName = reader.GetString(); + + // Read the value and add. + reader.Read(); + elementConverter.TryRead(ref reader, typeof(TValue), options, ref state, out TValue element); + Add(element, options, ref state); + } + } + } + else + { + if (state.Current.ObjectState < StackFrameObjectState.StartToken) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + state.Current.ObjectState = StackFrameObjectState.StartToken; + } + + // Handle the metadata properties. + if (shouldReadPreservedReferences && state.Current.ObjectState < StackFrameObjectState.MetataPropertyValue) + { + if (this.ResolveMetadata(ref reader, ref state, out value)) + { + if (state.Current.ObjectState == StackFrameObjectState.MetadataRefPropertyEndObject) + { + return true; + } + } + else + { + return false; + } + } + + if (state.Current.ObjectState < StackFrameObjectState.CreatedObject) + { + CreateCollection(ref state); + + if (state.Current.MetadataId != null) + { + if (!CanHaveMetadata) + { + ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(TypeToConvert); + } + + value = (TCollection)state.Current.ReturnValue!; + if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, value)) + { + // Reset so JsonPath throws exception with $id in it. + state.Current.MetadataPropertyName = MetadataPropertyName.Id; + + ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId); + } + } + + state.Current.ObjectState = StackFrameObjectState.CreatedObject; + } + + JsonConverter elementConverter = GetElementConverter(ref state); + while (true) + { + if (state.Current.PropertyState < StackFramePropertyState.ReadName) + { + 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 = JsonSerializer.GetSpan(ref reader); + if (propertyName.Length > 0 && propertyName[0] == '$') + { + ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(propertyName, ref state, reader); + } + } + + state.Current.KeyName = 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) + { + // Read the value and add. + 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 override sealed bool OnTryWrite( + Utf8JsonWriter writer, + TCollection dictionary, + JsonSerializerOptions options, + ref WriteStack state) + { + bool success; + + if (dictionary == null) + { + writer.WriteNullValue(); + success = true; + } + else + { + if (!state.Current.ProcessedStartToken) + { + state.Current.ProcessedStartToken = true; + writer.WriteStartObject(); + + if (options.ReferenceHandling.ShouldWritePreservedReferences()) + { + if (JsonSerializer.WriteReferenceForObject(this, dictionary, ref state, writer) == MetadataPropertyName.Ref) + { + writer.WriteEndObject(); + return true; + } + } + + state.Current.DeclaredJsonPropertyInfo = state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!; + } + + 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 0000000000000..ab2343c2749c4 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonDictionaryOfStringTValueConverter.cs @@ -0,0 +1,89 @@ +// 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; + +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) + { + string key = state.Current.KeyName!; + ((TCollection)state.Current.ReturnValue!)[key] = value; + } + + protected override void CreateCollection(ref ReadStack state) + { + if (state.Current.JsonClassInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(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 + { + 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 + { + 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.EndElement(); + } 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 0000000000000..739dec4433da5 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonICollectionOfTConverter.cs @@ -0,0 +1,110 @@ +// 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) + { + ((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); + } + else + { + TCollection returnValue = (TCollection)classInfo.CreateObject!()!; + + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(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 = (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; + } + + state.Current.EndElement(); + } 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 0000000000000..df905f0afeee8 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIDictionaryConverter.cs @@ -0,0 +1,113 @@ +// 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 +{ + /// + /// 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) + { + string key = state.Current.KeyName!; + ((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); + } + else + { + TCollection returnValue = (TCollection)classInfo.CreateObject!()!; + + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(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 + { + enumerator = (IDictionaryEnumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetValueConverter(ref state); + do + { + if (!(enumerator.Key is string key)) + { + // todo: add test for this. + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.DeclaredJsonPropertyInfo.RuntimePropertyType!); + Diagnostics.Debug.Assert(false); + return false; + } + + 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.EndElement(); + } 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 0000000000000..a3858ad106a53 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIDictionaryOfStringTValueConverter.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; + +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) + { + string key = state.Current.KeyName!; + ((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); + } + else + { + TCollection returnValue = (TCollection)classInfo.CreateObject!()!; + + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(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 + { + enumerator = (Dictionary.Enumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetValueConverter(ref state); + do + { + 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.EndElement(); + } 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 0000000000000..049b3a8992fcd --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableConverter.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; +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) + { + ((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; + } + + state.Current.EndElement(); + } 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 0000000000000..7ecfa753752ba --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableConverterFactory.cs @@ -0,0 +1,158 @@ +// 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.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_IDictionaryConverter = new JsonIDictionaryConverter(); + private static readonly JsonIEnumerableConverter s_IEnumerableConverter = new JsonIEnumerableConverter(); + private static readonly JsonIListConverter s_IListConverter = 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.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.JsonIListConverter`1")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonIListOfTConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.JsonListOfTConverter`2")] + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + JsonConverter? converter = null; + Type converterType; + 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<> + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericBaseClass(typeof(List<>))) != null) + { + converterType = typeof(JsonListOfTConverter<,>); + elementType = actualTypeToConvert.GetGenericArguments()[0]; + } + // Dictionary + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericBaseClass(typeof(Dictionary<,>))) != null) + { + if (actualTypeToConvert.GetGenericArguments()[0] == typeof(string)) + { + converterType = typeof(JsonDictionaryOfStringTValueConverter<,>); + elementType = actualTypeToConvert.GetGenericArguments()[1]; + } + else + { + return null; + } + } + // IDictionary + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IDictionary<,>))) != null) + { + if (actualTypeToConvert.GetGenericArguments()[0] == typeof(string)) + { + converterType = typeof(JsonIDictionaryOfStringTValueConverter<,>); + elementType = actualTypeToConvert.GetGenericArguments()[1]; + } + else + { + return null; + } + } + // IList<> + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IList<>))) != null) + { + converterType = typeof(JsonIListOfTConverter<,>); + elementType = actualTypeToConvert.GetGenericArguments()[0]; + } + // ICollection<> + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(ICollection<>))) != null) + { + converterType = typeof(JsonICollectionOfTConverter<,>); + elementType = actualTypeToConvert.GetGenericArguments()[0]; + } + // IEnumerable<> + else if (typeToConvert.IsInterface && + (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.IsAbstract || typeToConvert.IsInterface) + { + return s_IDictionaryConverter; + } + + converterType = typeof(JsonIDictionaryConverter<>); + } + else if (typeof(IList).IsAssignableFrom(typeToConvert)) + { + if (typeToConvert.IsAbstract || typeToConvert.IsInterface) + { + return s_IListConverter; + } + + converterType = typeof(JsonIListConverter<>); + } + else + { + Debug.Assert(typeof(IEnumerable).IsAssignableFrom(typeToConvert)); + if (typeToConvert == typeof(IEnumerable)) + { + return s_IEnumerableConverter; + } + + 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.NonPublic | 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 0000000000000..7062b350165a6 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableDefaultConverter.cs @@ -0,0 +1,295 @@ +// 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.Converters +{ + /// + /// Default base class implementation of JsonIEnumerableConverter{TCollection, TElement}. + /// + internal abstract class JsonIEnumerableDefaultConverter : JsonIEnumerableConverter + { + protected abstract void Add(TElement value, ref ReadStack state); + + protected virtual 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 = state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!.ConverterBase as JsonConverter; + if (converter == null) + { + state.Current.JsonClassInfo.ElementClassInfo.PolicyProperty.ThrowCollectionNotSupportedException(); + } + + return converter!; + } + + protected static JsonConverter GetElementConverter(ref WriteStack state) + { + JsonConverter? converter = state.Current.DeclaredJsonPropertyInfo.ConverterBase as JsonConverter; + if (converter == null) + { + state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!.ThrowCollectionNotSupportedException(); + } + + 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.Read(); + 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 + { + while (true) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + + // Obtain the CLR value from the JSON and apply to the object. + elementConverter.TryRead(ref reader, typeof(TElement), options, ref state, out TElement element); + Add(element, ref state); + } + } + } + else + { + if (state.Current.ObjectState < StackFrameObjectState.StartToken) + { + if (reader.TokenType == JsonTokenType.StartArray) + { + state.Current.ObjectState = StackFrameObjectState.MetataPropertyValue; + } + 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.MetataPropertyValue) + { + if (this.ResolveMetadata(ref reader, ref state, out value)) + { + if (state.Current.ObjectState == StackFrameObjectState.MetadataRefPropertyEndObject) + { + return true; + } + } + else + { + 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)) + { + // Reset so JsonPath throws exception with $id in it. + state.Current.MetadataPropertyName = MetadataPropertyName.Id; + + ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId); + } + } + + state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!; + state.Current.ObjectState = StackFrameObjectState.CreatedObject; + } + + if (state.Current.ObjectState < StackFrameObjectState.ReadElements) + { + JsonConverter elementConverter = GetElementConverter(ref state); + + // Main loop for processing 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) + { + // Clear the MetadataPropertyName in case we were processing $values since + // we are not longer processing $values. + state.Current.MetadataPropertyName = MetadataPropertyName.NoMetadata; + + break; + } + + state.Current.PropertyState = StackFramePropertyState.ReadValueIsEnd; + } + + if (state.Current.PropertyState < StackFramePropertyState.TryRead) + { + // Obtain the CLR value from the JSON and apply to the object. + 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) + { + ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(typeToConvert, reader, ref state); + } + } + } + + if (state.Current.ObjectState < StackFrameObjectState.EndTokenValidation) + { + if (state.Current.MetadataId != null) + { + if (reader.TokenType != JsonTokenType.EndObject) + { + ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(typeToConvert, reader, ref state); + } + } + } + } + + ConvertCollection(ref state, options); + value = (TCollection)state.Current.ReturnValue!; + return true; + } + + internal override sealed 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 0000000000000..c62792cd810ce --- /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) + { + ((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 + { + 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; + } + + state.Current.EndElement(); + } 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/JsonIListConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIListConverter.cs new file mode 100644 index 0000000000000..09aca5462f3b4 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIListConverter.cs @@ -0,0 +1,104 @@ +// 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 +{ + /// Converter for System.Collections.IList. + internal sealed class JsonIListConverter : JsonIEnumerableDefaultConverter + where TCollection : IList + { + protected override void Add(object value, ref ReadStack state) + { + ((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); + } + else + { + TCollection returnValue = (TCollection)classInfo.CreateObject!()!; + + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(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 = JsonSerializerOptions.GetObjectConverter(); + 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; + } + + state.Current.EndElement(); + } 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 0000000000000..4558d08f6359d --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIListOfTConverter.cs @@ -0,0 +1,104 @@ +// 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; + +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) + { + ((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); + } + else + { + TCollection returnValue = (TCollection)classInfo.CreateObject!()!; + + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(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 = (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; + } + + state.Current.EndElement(); + } 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/JsonListOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonListOfTConverter.cs new file mode 100644 index 0000000000000..ba7210ec7f091 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonListOfTConverter.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.Generic; + +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) + { + ((TCollection)state.Current.ReturnValue!).Add(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + if (state.Current.JsonClassInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(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; + 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; + } + + state.Current.EndElement(); + } + } + + return true; + } + } +} 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 0000000000000..ba1b2df9b6f47 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectDefaultConverter.cs @@ -0,0 +1,422 @@ +// 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!()!; + + // Read all properties. + while (true) + { + // Read the property name or EndObject. + reader.Read(); + + JsonTokenType tokenType = reader.TokenType; + if (tokenType == JsonTokenType.EndObject) + { + break; + } + + if (tokenType != JsonTokenType.PropertyName) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + JsonSerializer.LookupProperty( + obj, + ref reader, + options, + ref state, + out JsonPropertyInfo jsonPropertyInfo, + out bool useExtensionProperty); + + // Skip the property if not found. + if (!jsonPropertyInfo.ShouldDeserialize) + { + reader.TrySkip(); + continue; + } + + // Set the property value. + reader.Read(); + + if (!useExtensionProperty) + { + jsonPropertyInfo.ReadJsonAndSetMember(obj, ref state, ref reader); + } + else + { + jsonPropertyInfo.ReadJsonAndAddExtensionProperty(obj, ref state, ref reader); + } + } + } + else + { + if (state.Current.ObjectState < StackFrameObjectState.StartToken) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + state.Current.ObjectState = StackFrameObjectState.StartToken; + } + + // Handle the metadata properties. + if (state.Current.ObjectState < StackFrameObjectState.MetataPropertyValue) + { + if (shouldReadPreservedReferences) + { + if (this.ResolveMetadata(ref reader, ref state, out value)) + { + if (state.Current.ObjectState == StackFrameObjectState.MetadataRefPropertyEndObject) + { + if (!CanHaveMetadata) + { + ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(TypeToConvert); + } + + return true; + } + } + else + { + return false; + } + } + + state.Current.ObjectState = StackFrameObjectState.MetataPropertyValue; + } + + 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)) + { + // Reset so JsonPath throws exception with $id in it. + state.Current.MetadataPropertyName = MetadataPropertyName.Id; + + ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId); + } + } + + state.Current.ReturnValue = obj; + state.Current.ObjectState = StackFrameObjectState.CreatedObject; + } + else + { + obj = state.Current.ReturnValue!; + Debug.Assert(obj != null); + } + + // Read all properties. + while (true) + { + // Determine the property. + if (state.Current.PropertyState < StackFramePropertyState.ReadName) + { + 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); + } + + JsonSerializer.LookupProperty( + obj, + ref reader, + options, + ref state, + out jsonPropertyInfo, + out bool useExtensionProperty); + + state.Current.UseExtensionProperty = useExtensionProperty; + } + else + { + 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) + { + writer.WriteEndObject(); + return true; + } + } + + JsonPropertyInfo? dataExtensionProperty = state.Current.JsonClassInfo.DataExtensionProperty; + + int propertyCount; + JsonPropertyInfo[]? propertyCacheArray = state.Current.JsonClassInfo.PropertyCacheArray; + if (propertyCacheArray != null) + { + propertyCount = propertyCacheArray.Length; + } + else + { + propertyCount = 0; + } + + 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) + { + writer.WriteEndObject(); + return true; + } + } + + state.Current.ProcessedStartToken = true; + } + + JsonPropertyInfo? dataExtensionProperty = state.Current.JsonClassInfo.DataExtensionProperty; + + int propertyCount; + JsonPropertyInfo[]? propertyCacheArray = state.Current.JsonClassInfo.PropertyCacheArray; + if (propertyCacheArray != null) + { + propertyCount = propertyCacheArray.Length; + } + else + { + propertyCount = 0; + } + + 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/JsonObjectFactoryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectFactoryConverter.cs new file mode 100644 index 0000000000000..397f560dbd62a --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectFactoryConverter.cs @@ -0,0 +1,33 @@ +// 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.Reflection; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Converter factory for all object-based types (non-enumerable and non-primitive). + /// + internal class JsonObjectFactoryConverter : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) + { + // If the IEnumerableConverterFactory doesn't support the collection, ObjectConverterFactory doesn't either. + return !typeof(IEnumerable).IsAssignableFrom(typeToConvert); + } + + 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/JsonValueConverterKeyValuePair.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterKeyValuePair.cs index a5ea1db7e23d6..2c7f2455e6534 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) { @@ -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.Read(); + k = JsonSerializer.Deserialize(ref reader, options, ref state, KeyName); keySet = true; } else if (propertyName == ValueName) { - v = ReadProperty(ref reader, typeToConvert, options); + reader.Read(); + v = JsonSerializer.Deserialize(ref reader, options, ref state, ValueName); valueSet = true; } else @@ -62,15 +68,17 @@ public override KeyValuePair Read(ref Utf8JsonReader reader, Type } propertyName = reader.GetString()!; - if (propertyName == ValueName) + if (propertyName == KeyName) { - v = ReadProperty(ref reader, typeToConvert, options); - valueSet = true; + reader.Read(); + 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.Read(); + v = JsonSerializer.Deserialize(ref reader, options, ref state, ValueName); + valueSet = true; } else { @@ -89,52 +97,22 @@ public override KeyValuePair Read(ref Utf8JsonReader reader, Type ThrowHelper.ThrowJsonException(); } - return new KeyValuePair(k, v); + value = new KeyValuePair(k, v); + return true; } - private T ReadProperty(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + internal override bool OnTryWrite(Utf8JsonWriter writer, KeyValuePair value, JsonSerializerOptions options, ref WriteStack state) { - 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!; - } + writer.WriteStartObject(); - private void WriteProperty(Utf8JsonWriter writer, T value, JsonEncodedText name, JsonSerializerOptions? options) - { - Type typeToConvert = typeof(T); + writer.WritePropertyName(_keyName); + JsonSerializer.Serialize(writer, value.Key, options, ref state, KeyName); - writer.WritePropertyName(name); + writer.WritePropertyName(_valueName); + JsonSerializer.Serialize(writer, value.Value, options, ref state, ValueName); - // 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); - } - } - - 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 0000000000000..b182e36e9f7da --- /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 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 0000000000000..78d039d1641d1 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterNullableFactory.cs @@ -0,0 +1,37 @@ +// 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.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) + { + Type valueTypeToConvert = typeToConvert.GetGenericArguments()[0]; + + JsonConverter? valueConverter = options.GetConverter(valueTypeToConvert); + if (valueConverter == null) + { + // todo: add test for this + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(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 720562f5482e4..0000000000000 --- 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 4f18e6ad0e270..c6a5c3dd09315 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/ExtensionMethods.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ExtensionMethods.cs new file mode 100644 index 0000000000000..20729b17e3db9 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ExtensionMethods.cs @@ -0,0 +1,71 @@ +// 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 +{ + internal static class ExtensionMethods + { + internal static Type? GetCompatibleGenericBaseClass(this Type type, Type baseType) + { + Debug.Assert(baseType.IsGenericType); + Debug.Assert(!baseType.IsInterface); + + 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); + + 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; + } + } +} 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 fb91bafd4e78e..287601e78144f 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_SerializationNotSupportedCollection(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 e6fc4cc6cce7f..5c12c2a2f6541 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; @@ -11,6 +10,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Converters; namespace System.Text.Json { @@ -28,9 +28,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; @@ -42,6 +39,9 @@ internal sealed partial class JsonClassInfo public delegate object? ConstructorDelegate(); public ConstructorDelegate? CreateObject { get; private set; } + public delegate TCollection ConstructorDelegate(ICollection elements); + public delegate TCollection ConstructorDelegate(IEnumerable elements); + public ClassType ClassType { get; private set; } public JsonPropertyInfo? DataExtensionProperty { get; private set; } @@ -118,20 +118,17 @@ public JsonClassInfo(Type type, JsonSerializerOptions options) { Type = type; Options = options; + JsonConverter? converter; ClassType = GetClassType( 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, + out converter, options); - // Ignore properties on enumerable. switch (ClassType) { case ClassType.Object: @@ -196,29 +193,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_SerializationNotSupportedCollection(type); } break; default: @@ -233,13 +230,25 @@ private bool DetermineExtensionDataProperty(Dictionary if (jsonPropertyInfo != null) { Type declaredPropertyType = jsonPropertyInfo.DeclaredPropertyType; - if (!typeof(IDictionary).IsAssignableFrom(declaredPropertyType) && - !typeof(IDictionary).IsAssignableFrom(declaredPropertyType)) + if (typeof(Dictionary).IsAssignableFrom(declaredPropertyType) || + typeof(Dictionary).IsAssignableFrom(declaredPropertyType) || + typeof(IDictionary).IsAssignableFrom(declaredPropertyType) || + typeof(IDictionary).IsAssignableFrom(declaredPropertyType)) + { + JsonConverter? converter = Options.GetConverter(declaredPropertyType); + if (converter == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(declaredPropertyType); + } + } + else { ThrowHelper.ThrowInvalidOperationException_SerializationDataExtensionPropertyInvalid(this, jsonPropertyInfo); } DataExtensionProperty = jsonPropertyInfo; + jsonPropertyInfo.EscapedName = null; + return true; } @@ -400,8 +409,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 +430,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 +523,72 @@ 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; + return ClassType.Invalid; } - if (type.IsArray) - { - elementType = type.GetElementType(); - addMethod = default; - return ClassType.Enumerable; - } + // 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). - if (type.FullName != null) + Type converterRuntimeType = converter.RuntimeType; + if (type == converterRuntimeType) { - 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; - } + runtimeType = type; } - - - { - 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; - } - } - - if (typeof(IDictionary).IsAssignableFrom(type)) + else { - elementType = typeof(object); - addMethod = default; - if (type.IsInterface) { - Type concreteDictionaryType = typeof(Dictionary); - - if (type.IsAssignableFrom(concreteDictionaryType)) - { - runtimeType = concreteDictionaryType; - } + runtimeType = converterRuntimeType; } - - return ClassType.Dictionary; - } - - { - Type? genericIEnumerableType = type.GetInterface("System.Collections.Generic.IEnumerable`1"); - - if (genericIEnumerableType != null) + else if (converterRuntimeType.IsInterface) { - elementType = genericIEnumerableType.GetGenericArguments()[0]; + runtimeType = type; } else { - elementType = typeof(object); - } - } - - if (typeof(IList).IsAssignableFrom(type)) - { - addMethod = default; - - if (type.IsInterface) - { - Type concreteListType = typeof(List<>).MakeGenericType(elementType); - if (type.IsAssignableFrom(concreteListType)) + // Use the most derived version from the converter.RuntimeType or converter.TypeToConvert. + if (type.IsAssignableFrom(converterRuntimeType)) { - runtimeType = concreteListType; + runtimeType = converterRuntimeType; } - } - } - else if (type.IsInterface) - { - addMethod = default; - - Type concreteType = typeof(List<>).MakeGenericType(elementType); - if (type.IsAssignableFrom(concreteType)) - { - runtimeType = concreteType; - } - else - { - concreteType = typeof(HashSet<>).MakeGenericType(elementType); - if (type.IsAssignableFrom(concreteType)) + else if (converterRuntimeType.IsAssignableFrom(type)) { - runtimeType = concreteType; + runtimeType = type; } - } - } - else - { - addMethod = default; - - if (checkForAddMethod) - { - Type? genericICollectionType = type.GetInterface("System.Collections.Generic.ICollection`1"); - if (genericICollectionType != null) + else if (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.GetNotSupportedException_SerializationNotSupportedCollection(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/JsonConverter.ReadAhead.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.ReadAhead.cs new file mode 100644 index 0000000000000..dd8a66035bb86 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.ReadAhead.cs @@ -0,0 +1,74 @@ +// 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 + { + // 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 + // array brace. + state.InitialReaderState = reader.CurrentState; + state.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 in + // HandleValue below. + + reader = new Utf8JsonReader(reader.OriginalSpan.Slice(checked((int)state.InitialReaderBytesConsumed)), + isFinalBlock: reader.IsFinalBlock, + state: state.InitialReaderState); + + Debug.Assert(reader.BytesConsumed == 0); + state.BytesConsumed += state.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 token for HandleValue. + reader.Read(); + 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 ea06f03a3a42a..eff19aeef33ae 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,53 @@ 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 any metadata (properties starting with $). + /// + internal virtual bool CanHaveMetadata => !TypeToConvert.IsValueType; + + internal bool CanBePolymorphic { get; set; } + + /// + /// Can the converter have $values metadata. + /// + internal virtual bool CanHaveValuesMetadata => false; + + 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 d96ec3574ef2c..05eb5a1e55391 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 override sealed ClassType ClassType { - Debug.Assert(CanConvert(typeToConvert)); - return CreateConverter(typeToConvert, options); + get + { + return ClassType.Invalid; + } } /// @@ -32,7 +34,38 @@ 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() + { + throw new NotSupportedException(); + } + + internal sealed override Type? ElementType => null; + + internal JsonConverter GetConverterInternal(Type typeToConvert, JsonSerializerOptions options) + { + Debug.Assert(CanConvert(typeToConvert)); + return CreateConverter(typeToConvert, options)!; + } + + internal override sealed bool TryReadAsObject( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options, + ref ReadStack state, + out object? value) + { + throw new NotSupportedException(); + } + + internal override sealed bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state) + { + throw new NotSupportedException(); + } + + internal override sealed 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 df8577a2d3e6b..f603b219aa3fa 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 { /// @@ -10,10 +12,20 @@ namespace System.Text.Json.Serialization /// The to convert. public abstract class JsonConverter : JsonConverter { + private Type _typeToConvert = typeof(T); + /// /// 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 +40,57 @@ public override bool CanConvert(Type typeToConvert) return typeToConvert == typeof(T); } + internal override ClassType ClassType => ClassType.Value; + + internal override sealed JsonPropertyInfo CreateJsonPropertyInfo() + { + return new JsonPropertyInfo(); + } + + internal override Type? ElementType => null; + + /// + /// Is the converter built-in. + /// + internal bool IsInternalConverter; + + // This non-generic API is sealed as it just forwards to the generic version. + internal override sealed 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 override sealed 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 +103,285 @@ 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 + { + state.Current.OriginalPropertyTokenType = reader.TokenType; + state.Current.OriginalPropertyDepth = reader.CurrentDepth; + state.Current.OriginalPropertyBytesConsumed = reader.BytesConsumed; + + value = Read(ref reader, typeToConvert, options); + VerifyRead( + state.Current.OriginalPropertyTokenType, + state.Current.OriginalPropertyDepth, + state.Current.OriginalPropertyBytesConsumed != reader.BytesConsumed, + 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, + hasConsumedAnyBytes: true, + 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) + { + 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) + { + if (!state.IsContinuation) + { + state.Current.OriginalPropertyDepth = writer.CurrentDepth; + } + + Write(writer, value, options); + VerifyWrite(state.Current.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) + { + if (!state.IsContinuation) + { + state.Current.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(state.Current.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 override sealed Type TypeToConvert => _typeToConvert; + + internal void VerifyRead(JsonTokenType tokenType, int depth, bool hasConsumedAnyBytes, 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-array or non-object should not make any additional reads. + if (hasConsumedAnyBytes) + { + 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 547b840bd092d..8123eda642a7a 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 ea69e2699f487..0000000000000 --- 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/JsonIEnumerableConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIEnumerableConverter.cs new file mode 100644 index 0000000000000..664e3a259fe8a --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIEnumerableConverter.cs @@ -0,0 +1,18 @@ +// 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 IEnumerable-based collections. + /// + internal abstract class JsonIEnumerableConverter : JsonResumableConverter + { + private Type _elementType = typeof(TElement); + + internal override bool CanHaveValuesMetadata => true; + internal override ClassType ClassType => ClassType.Enumerable; + internal override Type ElementType => _elementType; + } +} 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 0000000000000..a1eb390db4be5 --- /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 override sealed 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 0a4b38f869aab..099023889f582 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,38 @@ // 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 +105,21 @@ private void DetermineSerializationCapabilities() { if (HasGetter) { - ShouldSerialize = true; - - if (HasSetter) + if (ConverterBase == null) { - 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; - } + ThrowCollectionNotSupportedException(); } - } - } - } - /// - /// 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); + ShouldSerialize = true; - if (_dictionaryValuePropertyPolicy == null) - { - // Use the existing PolicyProperty if there is one. - if ((_dictionaryValuePropertyPolicy = ElementClassInfo!.PolicyProperty) == null) + if (HasSetter) { - 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); + ShouldDeserialize = true; } } - - 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 +129,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,22 +139,18 @@ 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) { ParentClassType = parentClassType; @@ -304,34 +158,10 @@ public virtual void Initialize( 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; } @@ -341,7 +171,7 @@ public virtual void Initialize( // The name of the property with any casing policy or the name specified from JsonPropertyNameAttribute. public byte[]? Name { get; private set; } - public string? NameAsString { get; private set; } + public string? NameAsString { get; set; } // Key for fast property name lookup. public ulong PropertyNameKey { get; set; } @@ -349,89 +179,44 @@ 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)!; + IDictionary? dictionaryObject = propValue as IDictionary; - JsonPropertyInfo? propertyInfo; - JsonClassInfo? elementClassInfo = ElementClassInfo; - if (elementClassInfo != null && (propertyInfo = elementClassInfo.PolicyProperty) != null) + if (dictionaryObject != null && reader.TokenType == JsonTokenType.Null) { - if (!state.Current.CollectionPropertyInitialized) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(propertyInfo.RuntimePropertyType); - } - - // 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 DEBUG - VerifyRead(originalTokenType, originalDepth, originalBytesConsumed, ref reader); -#endif + // A null JSON value is treated as a null object reference. + dictionaryObject[state.Current.KeyName!] = null; } else { - JsonTokenType originalTokenType = reader.TokenType; - int originalDepth = reader.CurrentDepth; - long originalBytesConsumed = reader.BytesConsumed; - - OnRead(ref state, ref reader); + JsonConverter converter = JsonSerializerOptions.GetJsonElementConverter(); + if (!converter.TryRead(ref reader, typeof(JsonElement), Options, ref state, out JsonElement jsonElement)) + { + // No need to set a partial object here since JsonElement is a struct that must be read in full. + return false; + } - VerifyRead(originalTokenType, originalDepth, originalBytesConsumed, ref reader); + if (dictionaryObject != null) + { + dictionaryObject[state.Current.KeyName!] = jsonElement; + } + else + { + IDictionary dictionaryJsonElement = (IDictionary)propValue; + dictionaryJsonElement[state.Current.KeyName!] = jsonElement; + } } - } - public void ReadEnumerable(JsonTokenType tokenType, ref ReadStack state, ref Utf8JsonReader reader) - { - Debug.Assert(ShouldDeserialize); - - // 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 - - 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 +224,23 @@ 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); - } - - return _declaredTypeClassInfo; - } - } - - public Type RuntimePropertyType { get; private set; } = null!; + 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) + public void ThrowCollectionNotSupportedException() { - 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); - } + throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(RuntimePropertyType!, ParentClassType, PropertyInfo); } - - 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 69c4246bab922..0000000000000 --- 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 df6f1b4f68ca1..0000000000000 --- 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 80247d3226e00..0000000000000 --- 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 c5dbac440df0f..0000000000000 --- 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 0000000000000..691fcfcf447cf --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfTConverter.cs @@ -0,0 +1,190 @@ +// 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; + + Debug.Assert(EscapedName.HasValue); + 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 + { + // Optimize for internal converters by avoiding the extra call to TryRead. + if (Converter.CanUseDirectReadOrWrite) + { + 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 0000000000000..23097aece90dc --- /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) + { + 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); + 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 136c555e28130..0000000000000 --- 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 03a3ac673faaa..0000000000000 --- 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 898b1b26e9426..e798b4529d56e 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,218 @@ // 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) + internal static bool ResolveMetadata( + this JsonConverter converter, + ref Utf8JsonReader reader, + ref ReadStack state, + out T value) { - Debug.Assert(state.Current.JsonClassInfo!.Options.ReferenceHandling.ShouldReadPreservedReferences()); + if (state.Current.ObjectState < StackFrameObjectState.MetadataPropertyName) + { + // Read the first metadata property name. + if (!reader.Read()) + { + value = default!; + return false; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + if (converter.ClassType == ClassType.Enumerable) + { + ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(converter.TypeToConvert); + } + else + { + ThrowHelper.ThrowJsonException_MetadataIdIsNotFirstProperty(); + } + } + + ReadOnlySpan propertyName = GetSpan(ref reader); + MetadataPropertyName metadata = GetMetadataPropertyName(propertyName); + state.Current.MetadataPropertyName = metadata; + if (metadata == MetadataPropertyName.Ref) + { + if (!converter.CanHaveMetadata) + { + ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(propertyName, ref state, reader); + } + + state.Current.ObjectState = StackFrameObjectState.MetadataRefProperty; + } + else if (metadata == MetadataPropertyName.Id) + { + if (!converter.CanHaveMetadata) + { + if ((converter.ClassType & (ClassType.Dictionary | ClassType.Enumerable)) != 0) + { + ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(converter.TypeToConvert); + } + else + { + ThrowHelper.ThrowJsonException_MetadataInvalidReferenceToValueType(state.Current.JsonClassInfo.Type); + } + } + + state.Current.ObjectState = StackFrameObjectState.MetadataIdProperty; + } + else if (metadata == MetadataPropertyName.Values) + { + ThrowHelper.ThrowJsonException_MetadataMissingIdBeforeValues(); + } + else + { + Debug.Assert(metadata == MetadataPropertyName.NoMetadata); + + // Having a StartObject without metadata properties is not allowed. + if (converter.ClassType == ClassType.Enumerable) + { + ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(converter.TypeToConvert, reader, ref state); + } + + // Skip the read of the first property name, since we already read it above. + state.Current.PropertyState = StackFramePropertyState.ReadName; + value = default!; + return true; + } + } - if (reader.TokenType != JsonTokenType.String) + if (state.Current.ObjectState == StackFrameObjectState.MetadataRefProperty) { - ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType); + if (!reader.Read()) + { + value = default!; + return false; + } + + if (reader.TokenType != JsonTokenType.String) + { + ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType); + } + + string key = reader.GetString()!; + + state.Current.ReturnValue = state.ReferenceResolver.ResolveReferenceOnDeserialize(key!)!; + state.Current.ObjectState = StackFrameObjectState.MetadataRefPropertyEndObject; } + else if (state.Current.ObjectState == StackFrameObjectState.MetadataIdProperty) + { + if (!reader.Read()) + { + value = default!; + return false; + } + + if (reader.TokenType != JsonTokenType.String) + { + ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType); + } + + string id = reader.GetString()!; + state.Current.MetadataId = id; + + // Clear the MetadataPropertyName since we are done processing Id. + state.Current.MetadataPropertyName = MetadataPropertyName.NoMetadata; + + if (converter.ClassType == ClassType.Enumerable && converter.CanHaveValuesMetadata) + { + // Need to Read $values property name. + state.Current.ObjectState = StackFrameObjectState.MetadataValuesPropertyName; + } + else + { + // We are done reading metadata. + state.Current.ObjectState = StackFrameObjectState.MetataPropertyValue; + } + } + + if (state.Current.ObjectState == StackFrameObjectState.MetadataRefPropertyEndObject) + { + if (!reader.Read()) + { + value = default!; + return false; + } + + if (reader.TokenType != JsonTokenType.EndObject) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(converter.TypeToConvert); + } - MetadataPropertyName metadata = state.Current.LastSeenMetadataProperty; - string key = reader.GetString()!; - Debug.Assert(metadata == MetadataPropertyName.Id || metadata == MetadataPropertyName.Ref); + // Clear the MetadataPropertyName since we are done processing Ref. + state.Current.MetadataPropertyName = MetadataPropertyName.NoMetadata; - if (metadata == MetadataPropertyName.Id) + value = (T)state.Current.ReturnValue!; + return true; + } + + if (state.Current.ObjectState == StackFrameObjectState.MetadataValuesPropertyName) { - // 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()) + { + value = default!; + return false; + } - state.ReferenceResolver.AddReferenceOnDeserialize(key, value); + if (reader.TokenType != JsonTokenType.PropertyName) + { + ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(converter.TypeToConvert); + } + + if (reader.GetString() != "$values") + { + ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(converter.TypeToConvert); + } + + state.Current.MetadataPropertyName = MetadataPropertyName.Values; + state.Current.ObjectState = StackFrameObjectState.MetadataValuesPropertyStartArray; } - else if (metadata == MetadataPropertyName.Ref) + + if (state.Current.ObjectState == StackFrameObjectState.MetadataValuesPropertyStartArray) { - state.Current.ReferenceId = key; + if (!reader.Read()) + { + value = default!; + return false; + } + + if (reader.TokenType != JsonTokenType.StartArray) + { + ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(converter.TypeToConvert); + } + + state.Current.ObjectState = StackFrameObjectState.MetataPropertyValue; } + + value = default!; + return true; } - private static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan propertyName, ref ReadStack state, ref Utf8JsonReader reader) + internal static string? GetMetadataPropertyName(in ReadStackFrame frame) { - Debug.Assert(state.Current.JsonClassInfo!.Options.ReferenceHandling.ShouldReadPreservedReferences()); - - if (state.Current.ReferenceId != null) + switch (frame.MetadataPropertyName) { - ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(); + case MetadataPropertyName.Id: + return "$id"; + + case MetadataPropertyName.Ref: + return "$ref"; + + case MetadataPropertyName.Values: + return "$values"; } + return null; + } + + internal static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan propertyName) + { if (propertyName.Length > 0 && propertyName[0] == '$') { switch (propertyName.Length) @@ -68,9 +237,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 +248,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 07c81295facd3..0000000000000 --- 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 8791858f102f6..0000000000000 --- 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 f7d4c5014d985..62a65e9b14e39 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,7 +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.Buffers; using System.Collections; using System.Diagnostics; using System.Runtime.CompilerServices; @@ -11,142 +10,103 @@ 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 in the reader. + /// + // 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 void LookupProperty( + object obj, ref Utf8JsonReader reader, - ref ReadStack state) + JsonSerializerOptions options, + ref ReadStack state, + out JsonPropertyInfo jsonPropertyInfo, + 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); + ReadOnlySpan unescapedPropertyName = GetSpan(ref reader); + ReadOnlySpan propertyName; - bool isProcessingDictObject = state.Current.IsProcessingObject(ClassType.Dictionary); - if ((isProcessingDictObject || state.Current.IsProcessingProperty(ClassType.Dictionary)) && - state.Current.JsonClassInfo.DataExtensionProperty != state.Current.JsonPropertyInfo) + if (reader._stringHasEscaping) { - 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; - } - - state.Current.KeyName = reader.GetString(); + int idx = unescapedPropertyName.IndexOf(JsonConstants.BackSlash); + Debug.Assert(idx != -1); + propertyName = GetUnescapedString(unescapedPropertyName, idx); } else { - Debug.Assert(state.Current.JsonClassInfo.ClassType == ClassType.Object); + propertyName = unescapedPropertyName; + } + + jsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(propertyName, ref state.Current); - state.Current.EndProperty(); + // Increment PropertyIndex so GetProperty() starts with the next property the next time this function is called. + state.Current.PropertyIndex++; - ReadOnlySpan propertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; + // Determine if we should use the extension property. + if (jsonPropertyInfo == JsonPropertyInfo.s_missingProperty) + { if (options.ReferenceHandling.ShouldReadPreservedReferences()) { - MetadataPropertyName metadata = GetMetadataPropertyName(propertyName, ref state, ref reader); - - if (metadata == MetadataPropertyName.NoMetadata) + if (unescapedPropertyName.Length > 0 && unescapedPropertyName[0] == '$') { - if (state.Current.IsPreservedArray) - { - ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(in reader, ref state); - } + // Ensure JsonPath doesn't attempt to use the previous property. + state.Current.JsonPropertyInfo = null!; - HandlePropertyNameDefault(propertyName, ref state, ref reader, options); + ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(unescapedPropertyName, ref state, reader); } - else - { - ResolveMetadataOnObject(metadata, ref state); - } - - state.Current.LastSeenMetadataProperty = metadata; } - else + + JsonPropertyInfo? dataExtProperty = state.Current.JsonClassInfo.DataExtensionProperty; + if (dataExtProperty != null) { - HandlePropertyNameDefault(propertyName, ref state, ref reader, options); + state.Current.JsonPropertyName = propertyName.ToArray(); + state.Current.KeyName = JsonHelpers.Utf8GetString(propertyName); + CreateDataExtensionProperty(obj, dataExtProperty); + jsonPropertyInfo = dataExtProperty; } - } - } - [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); + state.Current.JsonPropertyInfo = jsonPropertyInfo; + useExtensionProperty = true; + return; } - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo!.GetProperty(propertyName, ref state.Current); - if (jsonPropertyInfo == JsonPropertyInfo.s_missingProperty) + // Support JsonException.Path. + Debug.Assert( + jsonPropertyInfo.JsonPropertyName == null || + options.PropertyNameCaseInsensitive || + propertyName.SequenceEqual(jsonPropertyInfo.JsonPropertyName)); + + state.Current.JsonPropertyInfo = jsonPropertyInfo; + + if (jsonPropertyInfo.JsonPropertyName == null) { - JsonPropertyInfo? dataExtProperty = state.Current.JsonClassInfo!.DataExtensionProperty; - if (dataExtProperty == null) + byte[] propertyNameArray = propertyName.ToArray(); + if (options.PropertyNameCaseInsensitive) { - state.Current.JsonPropertyInfo = JsonPropertyInfo.s_missingProperty; + // Each payload can have a different name here; remember the value on the temporary stack. + state.Current.JsonPropertyName = propertyNameArray; } else { - state.Current.JsonPropertyInfo = dataExtProperty; - state.Current.JsonPropertyName = propertyName.ToArray(); - state.Current.KeyName = JsonHelpers.Utf8GetString(propertyName); - state.Current.CollectionPropertyInitialized = true; - - CreateDataExtensionProperty(dataExtProperty, ref state); - } - } - else - { - // Support JsonException.Path. - Debug.Assert( - jsonPropertyInfo.JsonPropertyName == null || - options.PropertyNameCaseInsensitive || - propertyName.SequenceEqual(jsonPropertyInfo.JsonPropertyName)); - - state.Current.JsonPropertyInfo = jsonPropertyInfo; - - if (jsonPropertyInfo.JsonPropertyName == null) - { - 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; - } + // 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; } - private static void CreateDataExtensionProperty( - JsonPropertyInfo jsonPropertyInfo, - ref ReadStack state) + internal static void CreateDataExtensionProperty( + 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 2650b838aabfe..0000000000000 --- 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 0f2339ac9ed61..3b751c7f8f6ac 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,23 +2,27 @@ // 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.Buffers; +using System.Runtime.CompilerServices; namespace System.Text.Json { public static partial class JsonSerializer { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ReadOnlySpan GetSpan(ref Utf8JsonReader reader) + { + ReadOnlySpan propertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; + return propertyName; + } + private static object? ReadCore( Type returnType, JsonSerializerOptions options, 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 008bef29aacff..38a018a15e278 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,14 @@ 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.SetToTop(); + 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 cf1ac4d9c1017..48bc3910d9712 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,26 @@ 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); + + return (T)ReadCoreReEntry(options, ref reader, ref state)!; + } + /// /// Reads one JSON value (including objects or arrays) from the provided reader into a . /// @@ -111,16 +125,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 +141,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 +298,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; @@ -310,7 +320,7 @@ private static void ReadValueCore(JsonSerializerOptions options, ref Utf8JsonRea 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 54683977ed648..b1e7f6cd254d2 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 { @@ -14,181 +14,78 @@ namespace System.Text.Json /// public static partial class JsonSerializer { - private static void ReadCore( + internal 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 continue to 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 85622ba8b6272..0000000000000 --- 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 d01d44727b42b..0000000000000 --- 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 0000000000000..2235ae3fc1875 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.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.Diagnostics; +using System.Text.Json.Serialization; + +namespace System.Text.Json +{ + public static partial class JsonSerializer + { + // 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); + + internal static MetadataPropertyName WriteReferenceForObject( + JsonConverter jsonConverter, + object currentValue, + ref WriteStack state, + Utf8JsonWriter writer) + { + MetadataPropertyName metadataToWrite = state.GetResolvedReferenceHandling(jsonConverter, currentValue, out string? referenceId); + + if (metadataToWrite == MetadataPropertyName.Ref) + { + writer.WriteString(s_metadataRef, referenceId!); + } + 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 = state.GetResolvedReferenceHandling(jsonConverter, currentValue, out string? referenceId); + + if (metadataToWrite == MetadataPropertyName.NoMetadata) + { + writer.WriteStartArray(); + } + else if (metadataToWrite == MetadataPropertyName.Id) + { + writer.WriteStartObject(); + writer.WriteString(s_metadataId, referenceId!); + writer.WritePropertyName(s_metadataValues); + writer.WriteStartArray(); + } + 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 dd42b6ddc7b2a..0000000000000 --- 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 696b773009c9a..eafa2cdd3ce15 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); + 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 4677e032a4843..9eea08cb99529 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,23 @@ 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); + + // Ensures converters support contination due to having to re-populate the buffer from a Stream. + state.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 aa420f900d468..7c9b0f9931d11 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,45 @@ // 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 { + /// + /// Internal version that allows re-entry with preserving ReadStack so that JsonPath works correctly. + /// + 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); + Write(writer, value, options, ref state, jsonConverter); + } + + /// + /// Internal version that allows re-entry with preserving ReadStack so that JsonPath works correctly. + /// + internal static void Serialize(Utf8JsonWriter writer, object? value, Type inputType, JsonSerializerOptions options, ref WriteStack state, string? propertyName = null) + { + if (inputType == null) + { + throw new ArgumentNullException(nameof(inputType)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + JsonConverter jsonConverter = state.Current.InitializeReEntry(inputType, options, propertyName); + Write(writer, value, options, ref state, jsonConverter); + } + /// /// 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 ce6e11fa7eae0..a65f38eac3f0a 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,37 @@ 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 Write(writer, value, options, ref state, jsonConverter); } catch (InvalidOperationException ex) when (ex.Source == ThrowHelper.ExceptionSourceValueToRethrowAsJsonException) { ThrowHelper.ReThrowWithPath(state, ex); + return default; } catch (JsonException ex) { ThrowHelper.AddExceptionInformation(state, ex); throw; } + } - return true; + private static bool Write( + Utf8JsonWriter writer, + object? value, + JsonSerializerOptions options, + ref WriteStack state, + JsonConverter jsonConverter) + { + return jsonConverter.TryWriteAsObject(writer, value, options, ref state); } } } 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 161c1345c8af7..d9f7512ffcf6f 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,23 @@ 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. + // Enumerable and Object converters should always be last since they can convert any IEnumerable + // or non-IEnumerable. + converters.Add(new JsonIEnumerableConverterFactory()); + converters.Add(new JsonObjectFactoryConverter()); Debug.Assert(NumberOfConverters == converters.Count); @@ -65,7 +71,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 +161,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,10 +188,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) { 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 8e3646dac5ac6..fe45cf4497aa0 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,8 @@ using System.Diagnostics; using System.Text.Json.Serialization; using System.Text.Encodings.Web; -using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization.Converters; +using System.Runtime.CompilerServices; namespace System.Text.Json { @@ -20,13 +21,18 @@ 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; + + // Cache common converters. + private static JsonConverter s_objectConverter = null!; + private static JsonConverter s_jsonElementConverter = null!; + private int _defaultBufferSize = BufferSizeDefault; private int _maxDepth; private bool _allowTrailingCommas; @@ -307,6 +313,7 @@ public ReferenceHandling ReferenceHandling set { VerifyMutable(); + _referenceHandling = value ?? throw new ArgumentNullException(nameof(value)); } } @@ -317,12 +324,14 @@ internal MemberAccessor MemberAccessorStrategy { if (_memberAccessorStrategy == null) { -#if BUILDING_INBOX_LIBRARY - _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 + if (RuntimeFeature.IsDynamicCodeSupported) + { + _memberAccessorStrategy = new ReflectionEmitMemberAccessor(); + } + else + { + _memberAccessorStrategy = new ReflectionMemberAccessor(); + } } return _memberAccessorStrategy; @@ -364,30 +373,35 @@ internal JsonWriterOptions GetWriterOptions() }; } - internal bool CreateRangeDelegatesContainsKey(string key) + internal void VerifyMutable() { - return s_createRangeDelegates.ContainsKey(key); - } + // The default options are hidden and thus should be immutable. + Debug.Assert(this != s_defaultOptions); - internal bool TryGetCreateRangeDelegate(string delegateKey, [NotNullWhen(true)] out ImmutableCollectionCreator? createRangeDelegate) - { - return s_createRangeDelegates.TryGetValue(delegateKey, out createRangeDelegate) && createRangeDelegate != null; + if (_haveTypesBeenCreated) + { + ThrowHelper.ThrowInvalidOperationException_SerializerOptionsImmutable(); + } } - internal bool TryAddCreateRangeDelegate(string key, ImmutableCollectionCreator createRangeDelegate) + internal static JsonConverter GetJsonElementConverter() { - return s_createRangeDelegates.TryAdd(key, createRangeDelegate); + if (s_jsonElementConverter == null) + { + s_jsonElementConverter = new JsonConverterJsonElement(); + } + + return s_jsonElementConverter; } - internal void VerifyMutable() + internal static JsonConverter GetObjectConverter() { - // The default options are hidden and thus should be immutable. - Debug.Assert(this != s_defaultOptions); - - if (_haveTypesBeenCreated) + if (s_objectConverter == null) { - ThrowHelper.ThrowInvalidOperationException_SerializerOptionsImmutable(); + s_objectConverter = new JsonConverterObject(); } + + return s_objectConverter; } } } 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 0000000000000..8dc566aab115d --- /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 other features that require per-serialization state. + internal abstract class JsonValueConverter : JsonConverter + { + internal override sealed 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); + 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 1f34061f1f002..5be417b986f7a 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 @@ -50,8 +50,8 @@ private MethodInfo FindImmutableCreateRangeMethod(Type constructingType) return null!; } - public abstract Func CreatePropertyGetter(PropertyInfo propertyInfo); + 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 8303e65ec81cf..fa94a5e8cfe34 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,148 +14,300 @@ 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; + // Support the read-ahead feature. + public JsonReaderState InitialReaderState; + public long InitialReaderBytesConsumed; + + 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); - public bool IsLastFrame => _index == 0; + Current.JsonClassInfo = jsonClassInfo; - // Return a JSONPath using simple dot-notation when possible. When special characters are present, bracket-notation is used: - // $.x.y[0].z - // $['PropertyName.With.Special.Chars'] - public string JsonPath() - { - StringBuilder sb = new StringBuilder("$"); + // The initial JsonPropertyInfo will be used to obtain the converter. + Current.JsonPropertyInfo = jsonClassInfo.PolicyProperty!; - for (int i = 0; i < _index; i++) + if (options.ReferenceHandling.ShouldReadPreservedReferences()) { - AppendStackFrame(sb, _previous[i]); + ReferenceResolver = new DefaultReferenceResolver(writing: false); } - - AppendStackFrame(sb, Current); - return sb.ToString(); } - private void AppendStackFrame(StringBuilder sb, in ReadStackFrame frame) + public void Push() { - // Append the property name. - string? propertyName = GetPropertyName(frame); - AppendPropertyName(sb, propertyName); - - if (frame.JsonClassInfo != null) + if (_continuationCount == 0) { - if (frame.IsProcessingDictionary()) + if (_count == 0) { - // For dictionaries add the key. - AppendPropertyName(sb, frame.KeyName); + // The first stack frame is held in Current. + _count = 1; } - else if (frame.IsProcessingEnumerable()) + else { - IList? list = frame.TempEnumerableValues; - if (list == null && frame.ReturnValue != null) + JsonClassInfo jsonClassInfo; + if ((Current.JsonClassInfo.ClassType & (ClassType.Object | ClassType.Value | ClassType.NewValue)) != 0) { - - list = (IList?)frame.JsonPropertyInfo?.GetValueAsObject(frame.ReturnValue); + // Although ClassType.Value doesn't push, a custom custom converter may re-enter serialization. + jsonClassInfo = Current.JsonPropertyInfo.RuntimeClassInfo; } - if (list != null) + else { - sb.Append(@"["); - sb.Append(list.Count); - sb.Append(@"]"); + 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++; } } } - private void AppendPropertyName(StringBuilder sb, string? propertyName) + public void Pop(bool success) { - if (propertyName != null) + Debug.Assert(_count > 0); + + if (!success) { - if (propertyName.IndexOfAny(SpecialCharacters) != -1) + // Check if we need to initialize the continuation. + if (_continuationCount == 0) { - sb.Append(@"['"); - sb.Append(propertyName); - sb.Append(@"']"); + 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; } - else + + if (_continuationCount == 1) { - sb.Append('.'); - sb.Append(propertyName); + // 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]; } } - private string? GetPropertyName(in ReadStackFrame frame) + // Return a JSONPath using simple dot-notation when possible. When special characters are present, bracket-notation is used: + // $.x.y[0].z + // $['PropertyName.With.Special.Chars'] + public string JsonPath() { - // Attempt to get the JSON property name from the frame. - byte[]? utf8PropertyName = frame.JsonPropertyName; - if (utf8PropertyName == null) + StringBuilder sb = new StringBuilder("$"); + + // If a continuation, always report back full stack. + int count = Math.Max(_count, _continuationCount); + + for (int i = 0; i < count - 1; i++) { - // Attempt to get the JSON property name from the JsonPropertyInfo. - utf8PropertyName = frame.JsonPropertyInfo?.JsonPropertyName; + AppendStackFrame(sb, _previous[i]); } - string? propertyName; - if (utf8PropertyName != null) + if (_continuationCount == 0) { - propertyName = JsonHelpers.Utf8GetString(utf8PropertyName); + AppendStackFrame(sb, Current); } - else + + return sb.ToString(); + + void AppendStackFrame(StringBuilder sb, in ReadStackFrame frame) { - propertyName = null; + // Append the property name. + string? propertyName = GetPropertyName(frame); + AppendPropertyName(sb, propertyName); + + // For metadata properties, include the name. + propertyName = JsonSerializer.GetMetadataPropertyName(in frame); + AppendPropertyName(sb, propertyName); + + if (frame.JsonClassInfo != null) + { + if (frame.IsProcessingDictionary()) + { + // For dictionaries add the key. + AppendPropertyName(sb, frame.KeyName); + } + else if (frame.IsProcessingEnumerable()) + { + IEnumerable enumerable = (IEnumerable)frame.ReturnValue!; + if (enumerable != null) + { + // Once all elements are read, the exception is not within the array. + if (frame.ObjectState < StackFrameObjectState.ReadElements) + { + sb.Append(@"["); + sb.Append(GetCount(enumerable)); + sb.Append(@"]"); + } + } + } + } } - return propertyName; - } + static int GetCount(IEnumerable enumerable) + { + if (enumerable is ICollection collection) + { + return collection.Count; + } - /// - /// Bytes consumed in the current loop - /// - public long BytesConsumed; + int count = 0; + IEnumerator enumerator = enumerable.GetEnumerator(); + while (enumerator.MoveNext()) + { + count++; + } - /// - /// Internal flag to let us know that we need to read ahead in the inner read loop. - /// - internal bool ReadAhead; + return count; + } + + void AppendPropertyName(StringBuilder sb, string? propertyName) + { + if (propertyName != null) + { + if (propertyName.IndexOfAny(SpecialCharacters) != -1) + { + sb.Append(@"['"); + sb.Append(propertyName); + sb.Append(@"']"); + } + else + { + sb.Append('.'); + sb.Append(propertyName); + } + } + } + + string? GetPropertyName(in ReadStackFrame frame) + { + string? propertyName = null; + + // 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 from the property name specified in re-entry. + propertyName = frame.JsonPropertyNameAsString; + } + } + + if (utf8PropertyName != null) + { + propertyName = JsonHelpers.Utf8GetString(utf8PropertyName); + } + + 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 84f6638c11ebd..13abc6e21e12d 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,114 @@ // 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 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; + // 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 re-entry cases. - // Has an array or dictionary property been initialized. - public bool CollectionPropertyInitialized; + // Support Dictionary keys. + public string? KeyName; - // 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 int OriginalPropertyDepth; + public long OriginalPropertyBytesConsumed; + public JsonTokenType OriginalTokenType; + public JsonTokenType OriginalPropertyTokenType; - // 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 MetadataPropertyName MetadataPropertyName; + 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; + MetadataPropertyName = MetadataPropertyName.NoMetadata; - /// - /// 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 + // OriginalPropertyDepth + // OriginalPropertyBytesConsumed + // OriginalPropertyTokenType } - /// - /// Is the current object or property a Dictionary. - /// - public bool IsProcessingDictionary() + public void EndElement() { - return IsProcessingObject(ClassType.Dictionary) || - IsProcessingProperty(ClassType.Dictionary); + OriginalPropertyDepth = 0; + OriginalPropertyBytesConsumed = 0; + OriginalPropertyTokenType = JsonTokenType.None; + KeyName = 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 cd5135ea7959c..09d083ca383ff 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 @@ -141,10 +141,10 @@ public override ImmutableCollectionCreator ImmutableDictionaryCreateRange(Type c return creator; } - 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 +152,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 +174,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 +189,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 +212,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 0560db9adfe5c..46e143311447d 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 @@ -10,20 +10,6 @@ namespace System.Text.Json { 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); @@ -92,69 +78,23 @@ public override ImmutableCollectionCreator ImmutableDictionaryCreateRange(Type c return creator; } - public override Func CreatePropertyGetter(PropertyInfo propertyInfo) + public override Func CreatePropertyGetter(PropertyInfo propertyInfo) { 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!); - }; - } - } - - public override Action CreatePropertySetter(PropertyInfo propertyInfo) - { - 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); - }; - } - } - - private static TDelegate CreateDelegate(MethodInfo methodInfo) - where TDelegate : Delegate - { - return (TDelegate)Delegate.CreateDelegate(typeof(TDelegate), methodInfo); - } - - 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 0000000000000..08a8477117f82 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFrameObjectState.cs @@ -0,0 +1,26 @@ +// 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 +{ + 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. + MetataPropertyValue, // Whether all metadata properties has been read. + + CreatedObject, + ReadElements, + EndToken, + EndTokenValidation, + } +} 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/StackFramePropertyState.cs similarity index 54% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ExtensionDataWriteStatus.cs rename to src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFramePropertyState.cs index f18b11134aef2..28ce1a3e70cb6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ExtensionDataWriteStatus.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFramePropertyState.cs @@ -2,12 +2,16 @@ // 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 +namespace System.Text.Json { - internal enum ExtensionDataWriteStatus : byte + internal enum StackFramePropertyState : byte { - NotStarted = 0, - Writing = 1, - Finished = 2 + None = 0, + + ReadName, + Name, + ReadValue, + ReadValueIsEnd, + 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 1995ac01ca5e8..9bd1937c46d1b 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,186 @@ 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) { - Push(); - Current.JsonClassInfo = nextClassInfo; - Current.CurrentValue = nextValue; + JsonClassInfo jsonClassInfo = options.GetOrAddClass(type); + Debug.Assert(jsonClassInfo.ClassType != ClassType.Invalid); - ClassType classType = nextClassInfo.ClassType; + Current.JsonClassInfo = jsonClassInfo; - if (classType == ClassType.Enumerable || nextClassInfo.ClassType == ClassType.Dictionary) + if ((jsonClassInfo.ClassType & (ClassType.Enumerable | ClassType.Dictionary)) == 0) + { + Current.DeclaredJsonPropertyInfo = jsonClassInfo.PolicyProperty!; + } + + if (options.ReferenceHandling.ShouldWritePreservedReferences()) + { + ReferenceResolver = new DefaultReferenceResolver(writing: true); + } + } + + public MetadataPropertyName GetResolvedReferenceHandling(JsonConverter converter, object value, out string? referenceId) + { + if (!converter.CanHaveMetadata) + { + referenceId = default; + return MetadataPropertyName.NoMetadata; + } + + if (ReferenceResolver.TryGetOrAddReferenceOnSerialize(value, out referenceId)) + { + return MetadataPropertyName.Ref; + } + + return MetadataPropertyName.Id; + } + + 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) { - Current.PopStackOnEndCollection = true; - Current.JsonPropertyInfo = Current.JsonClassInfo.PolicyProperty; + // 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 +200,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 75354126a7d2a..fe826d0234b80 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,134 @@ 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 dictionaries. + /// + public bool IgnoreDictionaryKeyPolicy; + + /// + /// The class (POCO or IEnumerable) that is being populated. + /// + public JsonClassInfo JsonClassInfo; + + /// + /// The key name for a dictionary value. + /// + public string KeyName; + + /// + /// Validation state for a class. + /// + public int OriginalDepth; + public int OriginalPropertyDepth; + + // 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 EndElement() { - JsonClassInfo = options.GetOrAddClass(type); - if ((JsonClassInfo.ClassType & (ClassType.Value | ClassType.Enumerable | ClassType.Dictionary)) != 0) - { - JsonPropertyInfo = JsonClassInfo.PolicyProperty; - } + OriginalPropertyDepth = 0; + PropertyState = StackFramePropertyState.None; } - public void WriteObjectOrArrayStart(ClassType classType, Utf8JsonWriter writer, JsonSerializerOptions options, bool writeNull = false) + public void EndProperty() { - 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(); - } - } + DeclaredJsonPropertyInfo = null!; + JsonPropertyNameAsString = null; + KeyName = null!; + OriginalPropertyDepth = 0; + PolymorphicJsonPropertyInfo = null; + PropertyState = StackFramePropertyState.None; } - private void WriteObjectOrArrayStart(ClassType classType, JsonEncodedText propertyName, Utf8JsonWriter writer, bool writeNull) + /// + /// Return the property that contains the correct polymorphic properties including + /// the ClassType and ConverterBase. + /// + /// + public JsonPropertyInfo GetPolymorphicJsonPropertyInfo() { - 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); - } + return PolymorphicJsonPropertyInfo != null ? PolymorphicJsonPropertyInfo : DeclaredJsonPropertyInfo; } - public void WritePreservedObjectOrArrayStart(ClassType classType, Utf8JsonWriter writer, JsonSerializerOptions options, string referenceId) + /// + /// Initializes the state for polymorphic or re-entry cases. + /// + public JsonConverter InitializeReEntry(Type type, JsonSerializerOptions options, string? propertyName = null) { - if (JsonPropertyInfo?.EscapedName.HasValue == true) - { - writer.WriteStartObject(JsonPropertyInfo.EscapedName!.Value); - } - else if (KeyName != null) + JsonClassInfo newClassInfo = options.GetOrAddClass(type); + if (newClassInfo.ClassType == ClassType.Invalid) { - writer.WriteStartObject(KeyName); - } - else - { - writer.WriteStartObject(); + ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(type); } + // todo: check if type==newtype and skip below? - writer.WriteString(s_metadataId, referenceId); + // Set for exception handling calculation of JsonPath. + JsonPropertyNameAsString = propertyName; - 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; - } - } - - public void WriteReferenceObject(Utf8JsonWriter writer, JsonSerializerOptions options, string referenceId) - { - 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(); + PolymorphicJsonPropertyInfo = newClassInfo.PolicyProperty!; + return PolymorphicJsonPropertyInfo.ConverterBase; } public void Reset() { - CurrentValue = null; CollectionEnumerator = null; - ExtensionDataStatus = ExtensionDataWriteStatus.NotStarted; - JsonClassInfo = null; - PropertyEnumeratorIndex = 0; - PopStackOnEndCollection = false; - PopStackOnEndObject = false; - StartObjectWritten = false; + EnumeratorIndex = 0; + IgnoreDictionaryKeyPolicy = false; + JsonClassInfo = null!; + OriginalDepth = 0; + ProcessedStartToken = false; + ProcessedEndToken = false; EndProperty(); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void EndProperty() - { - JsonPropertyInfo = null; - KeyName = null; - MoveToNextProperty = false; - WriteWrappingBraceOnEndPreservedArray = false; - } - - public void EndDictionary() - { - CollectionEnumerator = null; - PopStackOnEndCollection = false; - } - - public void EndArray() - { - CollectionEnumerator = null; - PopStackOnEndCollection = 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 a03e0c3e233d3..bc2035af6f6e4 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 @@ -32,6 +32,12 @@ public static NotSupportedException GetNotSupportedException_SerializationNotSup } [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static NotSupportedException ThrowNotSupportedException_SerializationNotSupportedCollection(Type propertyType, Type? parentType = null, MemberInfo? memberInfo = null) + { + throw GetNotSupportedException_SerializationNotSupportedCollection(propertyType, parentType, memberInfo); + } + public static void ThrowInvalidOperationException_SerializerCycleDetected(int maxDepth) { throw new JsonException(SR.Format(SR.SerializerCycleDetected, maxDepth)); @@ -161,9 +167,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 +190,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 +205,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 +213,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 +232,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 +288,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) { @@ -383,10 +391,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, ref ReadStack state) { string propertyName = reader.GetString()!; - Type propertyType = JsonSerializer.GetValuesPropertyInfoFromJsonPreservableArrayRef(ref state.Current).DeclaredPropertyType; ThrowJsonException(SR.Format(SR.MetadataPreservedArrayFailed, SR.Format(SR.MetadataPreservedArrayInvalidProperty, propertyName), 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 4032c28032778..ccbe540f043f9 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 493e80a5eb520..89fc783615d93 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.BadConverters.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.BadConverters.cs @@ -132,7 +132,7 @@ 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) { @@ -149,10 +149,14 @@ 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()); - Assert.Throws(() => JsonSerializer.Serialize(0, options)); - Assert.Throws(() => JsonSerializer.Deserialize("0", options)); + // 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); } private class Level1 @@ -232,8 +236,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 +256,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 f4b138bfa45bd..62ebd5d06fd8a 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 dfdeb5cf5d4b7..1a49826da025f 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs @@ -1938,7 +1938,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 +2009,11 @@ public static void DictionaryOfTOnlyWithStringPoco() Assert.Equal(Json, json); } - public class DictionaryThatHasIncomatibleEnumerator : IDictionary + public class DictionaryThatHasIncomatibleEnumerator : IDictionary { - IDictionary _inner = new Dictionary(); + Hashtable _inner = new Hashtable(); - public TValue this[string key] + public object this[string key] { get { @@ -2024,72 +2025,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,23 +2093,22 @@ 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"]); + DictionaryThatHasIncomatibleEnumerator 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); + DictionaryThatHasIncomatibleEnumerator 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)); } diff --git a/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs b/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs index aa319b2017aa3..6d070a641000a 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs @@ -653,8 +653,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 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 ff14e2d4c810b..3b81aaa2e6717 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs @@ -766,7 +766,7 @@ public static void JsonPathObject() } [Fact] - public static void JsonPathImcompletePropertyAfterMetadata() + public static void JsonPathIncompletePropertyAfterMetadata() { string json = @"{ @@ -774,7 +774,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 +787,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 +891,8 @@ public static void ThrowOnStructWithReference() ]"; JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, s_deserializerOptionsPreserve)); + Assert.Equal("$[0].$id", ex.Path); + Assert.Contains($"'{typeof(EmployeeStruct)}'", ex.Message); } #endregion @@ -912,11 +916,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 +934,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 +953,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 +966,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 +983,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 +1098,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 +1109,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 +1122,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 before $ref json = @"{ @@ -1131,7 +1135,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 +1148,7 @@ public static void ReferenceObjectsShouldNotContainMoreProperties() }"; ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); - Assert.Equal("$.Manager", ex.Path); + Assert.Equal("$.Manager.$ref", ex.Path); } [Fact] @@ -1181,7 +1185,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 +1200,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 +1208,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 +1262,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); } @@ -1285,7 +1289,7 @@ public static void PreservedArrayValuesContainsNull() JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, s_deserializerOptionsPreserve)); - Assert.Equal("$.$values", ex.Path); + Assert.Equal("$.$values", ex.Path); } [Fact] 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 d6d9818eee6f8..d5387642f72fe 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/Value.WriteTests.cs b/src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.cs index af5f8e28b17f2..f2a0ba67e8454 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() From 7f09632f1be44989182f9471c6261df5292324a7 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Mon, 27 Jan 2020 17:02:49 -0600 Subject: [PATCH 2/3] Add support for additional collections --- .../src/System.Text.Json.csproj | 12 +- .../JsonConcurrentQueueOfTConverter.cs | 68 ++++++ .../JsonConcurrentStackOfTConverter.cs | 68 ++++++ .../JsonIEnumerableConverterFactory.cs | 92 +++++++- .../JsonIEnumerableWithAddMethodConverter.cs | 86 +++++++ ...adOnlyDictionaryOfStringTValueConverter.cs | 65 ++++++ .../Converters/JsonISetOfTConverter.cs | 102 ++++++++ ...utableDictionaryOfStringTValueConverter.cs | 80 +++++++ .../JsonImmutableEnumerableOfTConverter.cs | 82 +++++++ .../Converters/JsonQueueOfTConverter.cs | 67 ++++++ .../Converters/JsonStackOfTConverter.cs | 67 ++++++ .../Json/Serialization/ExtensionMethods.cs | 221 +++++++++++++++++- .../ImmutableCollectionCreator.cs | 91 -------- .../Text/Json/Serialization/MemberAccessor.cs | 41 +--- .../ReflectionEmitMemberAccessor.cs | 100 ++++---- .../Serialization/ReflectionMemberAccessor.cs | 63 ++--- .../tests/Serialization/DictionaryTests.cs | 28 ++- .../Serialization/PropertyVisibilityTests.cs | 19 +- .../Value.ReadTests.ConcurrentCollections.cs | 40 ++++ .../Value.ReadTests.GenericCollections.cs | 6 - .../Value.ReadTests.SpecializedCollections.cs | 52 +++++ .../Value.WriteTests.ConcurrentCollections.cs | 30 +++ .../Value.WriteTests.GenericCollections.cs | 1 - ...Value.WriteTests.ObjectModelCollections.cs | 4 +- ...Value.WriteTests.SpecializedCollections.cs | 38 +++ .../tests/System.Text.Json.Tests.csproj | 4 + 26 files changed, 1259 insertions(+), 268 deletions(-) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonConcurrentQueueOfTConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonConcurrentStackOfTConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableWithAddMethodConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIReadOnlyDictionaryOfStringTValueConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonISetOfTConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonImmutableDictionaryOfStringTValueConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonImmutableEnumerableOfTConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonQueueOfTConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonStackOfTConverter.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ImmutableCollectionCreator.cs create mode 100644 src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.ConcurrentCollections.cs create mode 100644 src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.SpecializedCollections.cs create mode 100644 src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.ConcurrentCollections.cs create mode 100644 src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.SpecializedCollections.cs 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 fc6d3457db68d..887481f7c4847 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -24,7 +24,6 @@ - @@ -54,8 +53,10 @@ - + + + @@ -66,12 +67,19 @@ + + + + + + + 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 0000000000000..4e695e8782115 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonConcurrentQueueOfTConverter.cs @@ -0,0 +1,68 @@ +// 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; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class JsonConcurrentQueueOfTConverter : JsonIEnumerableDefaultConverter + where TCollection : ConcurrentQueue + { + protected override void Add(TElement value, ref ReadStack state) + { + ((TCollection)state.Current.ReturnValue!).Enqueue(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + if (state.Current.JsonClassInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(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 + { + 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; + } + + state.Current.EndElement(); + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType => typeof(Queue); + } +} 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 0000000000000..fe184a7c73cd0 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonConcurrentStackOfTConverter.cs @@ -0,0 +1,68 @@ +// 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; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class JsonConcurrentStackOfTConverter : JsonIEnumerableDefaultConverter + where TCollection : ConcurrentStack + { + protected override void Add(TElement value, ref ReadStack state) + { + ((TCollection)state.Current.ReturnValue!).Push(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + if (state.Current.JsonClassInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(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 + { + 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; + } + + state.Current.EndElement(); + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType => TypeToConvert; + } +} 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 index 7ecfa753752ba..ac4643dfbaa81 100644 --- 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 @@ -3,6 +3,7 @@ // 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; @@ -25,13 +26,23 @@ public override bool CanConvert(Type 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; @@ -51,13 +62,13 @@ public override bool CanConvert(Type typeToConvert) converterType = typeof(JsonArrayConverter<,>); elementType = typeToConvert.GetElementType(); } - // List<> + // List<> or deriving from List<> else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericBaseClass(typeof(List<>))) != null) { converterType = typeof(JsonListOfTConverter<,>); elementType = actualTypeToConvert.GetGenericArguments()[0]; } - // Dictionary + // Dictionary or deriving from Dictionary else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericBaseClass(typeof(Dictionary<,>))) != null) { if (actualTypeToConvert.GetGenericArguments()[0] == typeof(string)) @@ -70,7 +81,20 @@ public override bool CanConvert(Type typeToConvert) return null; } } - // IDictionary + // Immutable dictionaries from System.Collections.Immutable, e.g. ImmutableDictionary + else if (typeToConvert.IsImmutableDictionaryType()) + { + if (typeToConvert.GetGenericArguments()[0] == typeof(string)) + { + converterType = typeof(JsonImmutableDictionaryOfStringTValueConverter<,>); + elementType = typeToConvert.GetGenericArguments()[1]; + } + else + { + return null; + } + } + // IDictionary or deriving from IDictionary else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IDictionary<,>))) != null) { if (actualTypeToConvert.GetGenericArguments()[0] == typeof(string)) @@ -83,21 +107,69 @@ public override bool CanConvert(Type typeToConvert) return null; } } + // IReadOnlyDictionary or deriving from IReadOnlyDictionary + else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IReadOnlyDictionary<,>))) != null) + { + if (actualTypeToConvert.GetGenericArguments()[0] == typeof(string)) + { + converterType = typeof(JsonIReadOnlyDictionaryOfStringTValueConverter<,>); + elementType = actualTypeToConvert.GetGenericArguments()[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]; } - // IEnumerable<> - else if (typeToConvert.IsInterface && - (actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IEnumerable<>))) != null) + // 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]; @@ -105,7 +177,7 @@ public override bool CanConvert(Type typeToConvert) // Check for non-generics after checking for generics. else if (typeof(IDictionary).IsAssignableFrom(typeToConvert)) { - if (typeToConvert.IsAbstract || typeToConvert.IsInterface) + if (typeToConvert == typeof(IDictionary)) { return s_IDictionaryConverter; } @@ -114,13 +186,17 @@ public override bool CanConvert(Type typeToConvert) } else if (typeof(IList).IsAssignableFrom(typeToConvert)) { - if (typeToConvert.IsAbstract || typeToConvert.IsInterface) + if (typeToConvert == typeof(IList)) { return s_IListConverter; } converterType = typeof(JsonIListConverter<>); } + else if (typeToConvert.IsNonGenericStackOrQueue()) + { + converterType = typeof(JsonIEnumerableWithAddMethodConverter<>); + } else { Debug.Assert(typeof(IEnumerable).IsAssignableFrom(typeToConvert)); 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 0000000000000..26f2dca39f5f7 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIEnumerableWithAddMethodConverter.cs @@ -0,0 +1,86 @@ +// 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.Diagnostics; +using System.Reflection; + +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.AddMethodDelegate != null); + ((Action)state.Current.AddMethodDelegate)((TCollection)state.Current.ReturnValue!, value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + JsonClassInfo classInfo = state.Current.JsonClassInfo; + + if (classInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(classInfo.Type); + } + + state.Current.ReturnValue = classInfo.CreateObject()!; + state.Current.AddMethodDelegate = GetOrAddEnumerableAddMethodDelegate(classInfo.Type, 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; + } + + state.Current.EndElement(); + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType => TypeToConvert; + + private readonly ConcurrentDictionary> _delegates = new ConcurrentDictionary>(); + + internal Action GetOrAddEnumerableAddMethodDelegate(Type type, JsonSerializerOptions options) + { + if (!_delegates.TryGetValue(type, out Action? result)) + { + // We verified this exists when we created the converter in the enumerable converter factory. + result = options.MemberAccessorStrategy.CreateAddMethodDelegate(); + } + + return result; + } + } +} 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 0000000000000..2c5a2c0a072af --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonIReadOnlyDictionaryOfStringTValueConverter.cs @@ -0,0 +1,65 @@ +// 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; + +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) + { + string key = state.Current.KeyName!; + ((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 + { + enumerator = (Dictionary.Enumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetValueConverter(ref state); + do + { + 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.EndElement(); + } 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 0000000000000..57954c9fc351b --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonISetOfTConverter.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.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) + { + ((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); + } + else + { + TCollection returnValue = (TCollection)classInfo.CreateObject!()!; + + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(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 = (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; + } + + state.Current.EndElement(); + } 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 0000000000000..ca19e5b001b59 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonImmutableDictionaryOfStringTValueConverter.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.Collections.Concurrent; + +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) + { + string key = state.Current.KeyName!; + ((Dictionary)state.Current.ReturnValue!)[key] = value; + } + + internal override bool CanHaveMetadata => 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 + { + enumerator = (Dictionary.Enumerator)state.Current.CollectionEnumerator; + } + + JsonConverter converter = GetValueConverter(ref state); + do + { + 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.EndElement(); + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType => TypeToConvert; + + 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 0000000000000..d7e271a763e68 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonImmutableEnumerableOfTConverter.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.Generic; +using System.Collections.Concurrent; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class JsonImmutableEnumerableOfTConverter : JsonIEnumerableDefaultConverter + where TCollection : IEnumerable + { + protected override void Add(TElement value, ref ReadStack state) + { + ((List)state.Current.ReturnValue!).Add(value); + } + + internal override bool CanHaveMetadata => 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 + { + 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; + } + + state.Current.EndElement(); + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType => TypeToConvert; + + 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/JsonQueueOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonQueueOfTConverter.cs new file mode 100644 index 0000000000000..c40193e38c17c --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonQueueOfTConverter.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.Generic; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class JsonQueueOfTConverter : JsonIEnumerableDefaultConverter + where TCollection : Queue + { + protected override void Add(TElement value, ref ReadStack state) + { + ((TCollection)state.Current.ReturnValue!).Enqueue(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + if (state.Current.JsonClassInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(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 + { + 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; + } + + state.Current.EndElement(); + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType => typeof(Queue); + } +} 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 0000000000000..feb50c82b3fea --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonStackOfTConverter.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.Generic; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class JsonStackOfTConverter : JsonIEnumerableDefaultConverter + where TCollection : Stack + { + protected override void Add(TElement value, ref ReadStack state) + { + ((TCollection)state.Current.ReturnValue!).Push(value); + } + + protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) + { + if (state.Current.JsonClassInfo.CreateObject == null) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(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 + { + 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; + } + + state.Current.EndElement(); + } while (enumerator.MoveNext()); + + return true; + } + + internal override Type RuntimeType => TypeToConvert; + } +} 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 index 20729b17e3db9..d8fa73bb5ea19 100644 --- 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 @@ -2,18 +2,49 @@ // 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.Reflection; namespace System.Text.Json { 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); - - baseType = baseType.GetGenericTypeDefinition(); + Debug.Assert(baseType == baseType.GetGenericTypeDefinition()); Type baseTypeToCheck = type; @@ -38,8 +69,7 @@ internal static class ExtensionMethods { Debug.Assert(interfaceType.IsGenericType); Debug.Assert(interfaceType.IsInterface); - - interfaceType = interfaceType.GetGenericTypeDefinition(); + Debug.Assert(interfaceType == interfaceType.GetGenericTypeDefinition()); Type interfaceToCheck = type; @@ -67,5 +97,188 @@ internal static class ExtensionMethods return null; } + + internal static Type? GetCompatibleNonGenericBaseClass(this Type type, string baseTypeAssemblyQualifiedNamePrefix) + { + Type baseTypeToCheck = type; + + while (baseTypeToCheck != null && baseTypeToCheck != typeof(object)) + { + if (!baseTypeToCheck.IsGenericType) + { + Type nonGenericTypeToCheck = baseTypeToCheck; + if (nonGenericTypeToCheck.AssemblyQualifiedName!.StartsWith(baseTypeAssemblyQualifiedNamePrefix)) + { + return baseTypeToCheck; + } + } + + baseTypeToCheck = baseTypeToCheck.BaseType!; + } + + 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_SerializationNotSupportedCollection(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_SerializationNotSupportedCollection(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) + { + return type.GetCompatibleNonGenericBaseClass("System.Collections.Stack, System.Collections.NonGeneric") != null || + type.GetCompatibleNonGenericBaseClass("System.Collections.Queue, System.Collections.NonGeneric") != null; + } } } 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 8c093f0f54fed..0000000000000 --- 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/MemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/MemberAccessor.cs index 5be417b986f7a..2ed74e5a08d4d 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,44 +11,11 @@ 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); - - protected MethodInfo ImmutableCollectionCreateRangeMethod(Type constructingType, Type elementType) - { - MethodInfo createRangeMethod = FindImmutableCreateRangeMethod(constructingType); - - 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>, TCollection> CreateImmutableDictionaryCreateRangeDelegate(); public abstract Func CreatePropertyGetter(PropertyInfo propertyInfo); 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 09d083ca383ff..f41f66e9ea960 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 @@ -3,10 +3,11 @@ // See the LICENSE file in the project root for more information. #if BUILDING_INBOX_LIBRARY +using System.Collections; +using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; -using System.Runtime.CompilerServices; namespace System.Text.Json { @@ -55,90 +56,71 @@ 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 a 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) => 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 46e143311447d..3a46fc19fd285 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,9 +2,10 @@ // 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.Reflection; -using System.Runtime.CompilerServices; namespace System.Text.Json { @@ -28,54 +29,32 @@ 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 collectionType = typeof(TCollection); + Type elementType = typeof(object); - Type creatorType = typeof(ImmutableEnumerableCreator<,>).MakeGenericType(elementType, collectionType); - ConstructorInfo constructor = creatorType.GetConstructor( - BindingFlags.Public | - BindingFlags.NonPublic | - BindingFlags.Instance, binder: null, - Type.EmptyTypes, - modifiers: null)!; + // We verified this won't be null when we created the converter for the collection type. + MethodInfo addMethod = (collectionType.GetMethod("Push") ?? collectionType.GetMethod("Enqueue"))!; - ImmutableCollectionCreator creator = (ImmutableCollectionCreator)constructor.Invoke(Array.Empty()); - creator.RegisterCreatorDelegateFromMethod(createRange); - return creator; + return delegate (TCollection collection, object element) + { + addMethod.Invoke(collection, new object[] { element! }); + }; } - [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); - - // Only string keys are allowed. - if (collectionType.GetGenericArguments()[0] != typeof(string)) - { - 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)!; + MethodInfo createRange = typeof(TCollection).GetImmutableEnumerableCreateRangeMethod(typeof(TElement)); + return (Func, TCollection>)createRange.CreateDelegate( + typeof(Func, TCollection>)); + } - ImmutableCollectionCreator creator = (ImmutableCollectionCreator)constructor.Invoke(Array.Empty()); - creator.RegisterCreatorDelegateFromMethod(createRange); - return creator; + public override Func>, TCollection> CreateImmutableDictionaryCreateRangeDelegate() + { + MethodInfo createRange = typeof(TCollection).GetImmutableDictionaryCreateRangeMethod(typeof(TElement)); + return (Func>, TCollection>)createRange.CreateDelegate( + typeof(Func>, TCollection>)); } public override Func CreatePropertyGetter(PropertyInfo propertyInfo) diff --git a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs index 1a49826da025f..bec95b265fe31 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, diff --git a/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs index cc9f383f26c4c..1fe8fcbb152f9 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/Value.ReadTests.ConcurrentCollections.cs b/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.ConcurrentCollections.cs new file mode 100644 index 0000000000000..bb9820394828c --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.ConcurrentCollections.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. + +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); + qc.TryPeek(out string val); + Assert.Equal("1", val); + + ConcurrentStack qs = JsonSerializer.Deserialize>(@"[""1""]"); + Assert.Equal(1, qs.Count); + qs.TryPeek(out val); + 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 c305556dc1aef..30f39ea96c668 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 0000000000000..dc18020d44b7c --- /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 0000000000000..cb78774b831d1 --- /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 a803f71e0349f..721dc0b751e92 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 8e371832144d7..d1559ef1f9a02 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 0000000000000..2b88bbbff7c81 --- /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/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj index 6661230c60b5a..d101e9fe03f8b 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 @@ + + + + From 02c8da3b6c08d446421e415d9892d2a48b1df8c5 Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Tue, 28 Jan 2020 13:02:57 -0600 Subject: [PATCH 3/3] New converter model additional refactoring and feedback --- .../src/Resources/Strings.resx | 10 +- .../src/System.Text.Json.csproj | 6 +- .../src/System/Text/Json/JsonHelpers.cs | 22 +++ .../Converters/JsonArrayConverter.cs | 8 +- .../JsonConcurrentQueueOfTConverter.cs | 9 +- .../JsonConcurrentStackOfTConverter.cs | 9 +- .../JsonDictionaryDefaultConverter.cs | 118 ++++++++-------- .../JsonDictionaryOfStringTValueConverter.cs | 16 ++- .../Converters/JsonICollectionOfTConverter.cs | 20 ++- .../Converters/JsonIDictionaryConverter.cs | 55 ++++---- .../JsonIDictionaryOfStringTValueConverter.cs | 32 +++-- .../Converters/JsonIEnumerableConverter.cs | 17 ++- .../JsonIEnumerableConverterFactory.cs | 35 ++--- .../JsonIEnumerableDefaultConverter.cs | 69 +++++----- .../Converters/JsonIEnumerableOfTConverter.cs | 4 +- .../JsonIEnumerableWithAddMethodConverter.cs | 38 +++--- .../Converters/JsonIListConverter.cs | 30 ++--- .../Converters/JsonIListOfTConverter.cs | 21 ++- ...adOnlyDictionaryOfStringTValueConverter.cs | 14 +- .../Converters/JsonISetOfTConverter.cs | 20 ++- ...utableDictionaryOfStringTValueConverter.cs | 19 ++- .../JsonImmutableEnumerableOfTConverter.cs | 10 +- .../Converters/JsonListOfTConverter.cs | 8 +- ...erter.cs => JsonObjectConverterFactory.cs} | 9 +- .../Converters/JsonObjectDefaultConverter.cs | 61 ++++----- .../Converters/JsonQueueOfTConverter.cs | 9 +- .../Converters/JsonStackOfTConverter.cs | 9 +- .../JsonValueConverterKeyValuePair.cs | 14 +- .../Converters/JsonValueConverterNullable.cs | 2 +- .../JsonValueConverterNullableFactory.cs | 6 +- .../Json/Serialization/ExtensionMethods.cs | 43 ++---- .../JsonClassInfo.AddProperty.cs | 2 +- .../Text/Json/Serialization/JsonClassInfo.cs | 24 +--- ...onverter.cs => JsonCollectionConverter.cs} | 9 +- .../Serialization/JsonConverter.ReadAhead.cs | 25 ++-- .../Text/Json/Serialization/JsonConverter.cs | 9 +- .../Serialization/JsonConverterFactory.cs | 27 ++-- .../Json/Serialization/JsonConverterOfT.cs | 54 ++++---- .../Json/Serialization/JsonObjectConverter.cs | 2 +- .../Json/Serialization/JsonPropertyInfo.cs | 61 +++++---- .../JsonPropertyInfoOfTConverter.cs | 7 +- .../JsonResumableConverterOfT.cs | 6 +- .../JsonSerializer.Read.HandleMetadata.cs | 126 ++++++++---------- .../JsonSerializer.Read.HandlePropertyName.cs | 52 ++++---- .../JsonSerializer.Read.Helpers.cs | 10 -- .../JsonSerializer.Read.Stream.cs | 1 - .../JsonSerializer.Read.Utf8JsonReader.cs | 9 +- .../Json/Serialization/JsonSerializer.Read.cs | 8 +- .../JsonSerializer.Write.HandleMetadata.cs | 34 ++++- .../JsonSerializer.Write.Helpers.cs | 2 +- .../JsonSerializer.Write.Stream.cs | 5 +- .../JsonSerializer.Write.Utf8JsonWriter.cs | 26 +--- .../Serialization/JsonSerializer.Write.cs | 14 +- .../JsonSerializerOptions.Converters.cs | 8 +- .../Serialization/JsonSerializerOptions.cs | 39 +----- .../Serialization/JsonValueConverterOfT.cs | 8 +- .../Text/Json/Serialization/MemberAccessor.cs | 2 +- .../Text/Json/Serialization/ReadStack.cs | 46 +++---- .../Text/Json/Serialization/ReadStackFrame.cs | 20 +-- .../ReflectionEmitMemberAccessor.cs | 11 +- .../Serialization/ReflectionMemberAccessor.cs | 7 +- .../Serialization/StackFrameObjectState.cs | 6 +- .../Serialization/StackFramePropertyState.cs | 14 +- .../Text/Json/Serialization/WriteStack.cs | 18 +-- .../Json/Serialization/WriteStackFrame.cs | 22 +-- .../Text/Json/ThrowHelper.Serialization.cs | 79 ++++++----- .../tests/Serialization/Array.ReadTests.cs | 6 +- .../CustomConverterTests.BadConverters.cs | 7 +- .../tests/Serialization/DictionaryTests.cs | 19 ++- .../tests/Serialization/ExceptionTests.cs | 28 ++++ .../tests/Serialization/ExtensionDataTests.cs | 67 ++++++++++ .../ReferenceHandlingTests.Deserialize.cs | 47 ++++++- .../tests/Serialization/Stream.WriteTests.cs | 24 ++-- .../Value.ReadTests.ConcurrentCollections.cs | 6 +- .../Value.ReadTests.SpecializedCollections.cs | 14 +- 75 files changed, 909 insertions(+), 845 deletions(-) rename src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/{JsonObjectFactoryConverter.cs => JsonObjectConverterFactory.cs} (73%) rename src/libraries/System.Text.Json/src/System/Text/Json/Serialization/{JsonIEnumerableConverter.cs => JsonCollectionConverter.cs} (51%) diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index f1d11f8a343c2..226f99b2b3927 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 887481f7c4847..7e7ba368e04b2 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -77,7 +77,7 @@ - + @@ -109,6 +109,7 @@ + @@ -117,7 +118,6 @@ - @@ -249,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 e9fbe8fc2b9db..ce687afa263ed 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/Converters/JsonArrayConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonArrayConverter.cs index 56608bba73b0d..4d3e2fc2cd9e9 100644 --- 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 @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; +using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { @@ -14,10 +15,11 @@ internal sealed class JsonArrayConverter : JsonIEnumerableDefaultConverter where TCollection: IEnumerable { - internal override bool CanHaveMetadata => false; + 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); } @@ -28,12 +30,14 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti 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; @@ -63,8 +67,6 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, state.Current.EnumeratorIndex = ++index; return false; } - - state.Current.EndElement(); } } 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 index 4e695e8782115..4a4a10e8e2e36 100644 --- 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 @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { @@ -12,6 +13,7 @@ internal sealed class JsonConcurrentQueueOfTConverter : J { protected override void Add(TElement value, ref ReadStack state) { + Debug.Assert(state.Current.ReturnValue is TCollection); ((TCollection)state.Current.ReturnValue!).Enqueue(value); } @@ -19,7 +21,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti { if (state.Current.JsonClassInfo.CreateObject == null) { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(state.Current.JsonClassInfo.Type); + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(state.Current.JsonClassInfo.Type); } state.Current.ReturnValue = state.Current.JsonClassInfo.CreateObject(); @@ -38,6 +40,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, } else { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); enumerator = (IEnumerator)state.Current.CollectionEnumerator; } @@ -56,13 +59,9 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, state.Current.CollectionEnumerator = enumerator; return false; } - - state.Current.EndElement(); } while (enumerator.MoveNext()); return true; } - - internal override Type RuntimeType => typeof(Queue); } } 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 index fe184a7c73cd0..5321d8d716146 100644 --- 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 @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { @@ -12,6 +13,7 @@ internal sealed class JsonConcurrentStackOfTConverter : J { protected override void Add(TElement value, ref ReadStack state) { + Debug.Assert(state.Current.ReturnValue is TCollection); ((TCollection)state.Current.ReturnValue!).Push(value); } @@ -19,7 +21,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti { if (state.Current.JsonClassInfo.CreateObject == null) { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(state.Current.JsonClassInfo.Type); + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(state.Current.JsonClassInfo.Type); } state.Current.ReturnValue = state.Current.JsonClassInfo.CreateObject(); @@ -38,6 +40,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, } else { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); enumerator = (IEnumerator)state.Current.CollectionEnumerator; } @@ -56,13 +59,9 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, state.Current.CollectionEnumerator = enumerator; return false; } - - state.Current.EndElement(); } while (enumerator.MoveNext()); return true; } - - internal override Type RuntimeType => TypeToConvert; } } 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 index 468d33d6ec174..62f64bfc8be1a 100644 --- 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 @@ -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.Converters { /// @@ -16,7 +18,7 @@ internal abstract class JsonDictionaryDefaultConverter protected abstract void Add(TValue value, JsonSerializerOptions options, ref ReadStack state); /// - /// When overridden, converts the temporary collection held in state.ReturnValue to the final collection. + /// 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) { } @@ -30,13 +32,10 @@ protected virtual void CreateCollection(ref ReadStack state) { } protected static JsonConverter GetElementConverter(ref ReadStack state) { - JsonConverter? converter = state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!.ConverterBase as JsonConverter; - if (converter == null) - { - state.Current.JsonClassInfo.ElementClassInfo.PolicyProperty.ThrowCollectionNotSupportedException(); - } + 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!; + return converter; } protected string GetKeyName(string key, ref WriteStack state, JsonSerializerOptions options) @@ -54,18 +53,15 @@ protected string GetKeyName(string key, ref WriteStack state, JsonSerializerOpti return key; } - protected JsonConverter GetValueConverter(ref WriteStack state) + protected static JsonConverter GetValueConverter(ref WriteStack state) { - JsonConverter converter = (JsonConverter)state.Current.DeclaredJsonPropertyInfo.ConverterBase; - if (converter == null) - { - state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!.ThrowCollectionNotSupportedException(); - } + 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!; + return converter; } - internal override sealed bool OnTryRead( + internal sealed override bool OnTryRead( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, @@ -88,10 +84,11 @@ internal override sealed bool OnTryRead( JsonConverter elementConverter = GetElementConverter(ref state); if (elementConverter.CanUseDirectReadOrWrite) { + // Process all elements. while (true) { // Read the key name. - reader.Read(); + reader.ReadWithVerify(); if (reader.TokenType == JsonTokenType.EndObject) { @@ -103,20 +100,21 @@ internal override sealed bool OnTryRead( ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); } - state.Current.KeyName = reader.GetString(); + state.Current.JsonPropertyNameAsString = reader.GetString(); // Read the value and add. - reader.Read(); + 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.Read(); + reader.ReadWithVerify(); if (reader.TokenType == JsonTokenType.EndObject) { @@ -128,10 +126,11 @@ internal override sealed bool OnTryRead( ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); } - state.Current.KeyName = reader.GetString(); + state.Current.JsonPropertyNameAsString = reader.GetString(); - // Read the value and add. - reader.Read(); + 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); } @@ -139,7 +138,9 @@ internal override sealed bool OnTryRead( } else { - if (state.Current.ObjectState < StackFrameObjectState.StartToken) + // Slower path that supports continuation and preserved references. + + if (state.Current.ObjectState == StackFrameObjectState.None) { if (reader.TokenType != JsonTokenType.StartObject) { @@ -150,49 +151,47 @@ internal override sealed bool OnTryRead( } // Handle the metadata properties. - if (shouldReadPreservedReferences && state.Current.ObjectState < StackFrameObjectState.MetataPropertyValue) + if (shouldReadPreservedReferences && state.Current.ObjectState < StackFrameObjectState.MetadataPropertyValue) { - if (this.ResolveMetadata(ref reader, ref state, out value)) + 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) { - if (!CanHaveMetadata) - { - ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(TypeToConvert); - } + Debug.Assert(CanHaveIdMetadata); value = (TCollection)state.Current.ReturnValue!; if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, value)) { - // Reset so JsonPath throws exception with $id in it. - state.Current.MetadataPropertyName = MetadataPropertyName.Id; - - ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId); + 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.ReadName) + if (state.Current.PropertyState == StackFramePropertyState.None) { state.Current.PropertyState = StackFramePropertyState.ReadName; @@ -222,14 +221,14 @@ internal override sealed bool OnTryRead( // Verify property doesn't contain metadata. if (shouldReadPreservedReferences) { - ReadOnlySpan propertyName = JsonSerializer.GetSpan(ref reader); + ReadOnlySpan propertyName = reader.GetSpan(); if (propertyName.Length > 0 && propertyName[0] == '$') { - ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(propertyName, ref state, reader); + ThrowHelper.ThrowUnexpectedMetadataException(propertyName, ref reader, ref state); } } - state.Current.KeyName = reader.GetString(); + state.Current.JsonPropertyNameAsString = reader.GetString(); } if (state.Current.PropertyState < StackFramePropertyState.ReadValue) @@ -245,7 +244,7 @@ internal override sealed bool OnTryRead( if (state.Current.PropertyState < StackFramePropertyState.TryRead) { - // Read the value and add. + // 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) { @@ -264,46 +263,41 @@ internal override sealed bool OnTryRead( return true; } - internal override sealed bool OnTryWrite( + internal sealed override bool OnTryWrite( Utf8JsonWriter writer, TCollection dictionary, JsonSerializerOptions options, ref WriteStack state) { - bool success; - if (dictionary == null) { writer.WriteNullValue(); - success = true; + return true; } - else + + if (!state.Current.ProcessedStartToken) { - if (!state.Current.ProcessedStartToken) - { - state.Current.ProcessedStartToken = true; - writer.WriteStartObject(); + state.Current.ProcessedStartToken = true; + writer.WriteStartObject(); - if (options.ReferenceHandling.ShouldWritePreservedReferences()) + if (options.ReferenceHandling.ShouldWritePreservedReferences()) + { + if (JsonSerializer.WriteReferenceForObject(this, dictionary, ref state, writer) == MetadataPropertyName.Ref) { - if (JsonSerializer.WriteReferenceForObject(this, dictionary, ref state, writer) == MetadataPropertyName.Ref) - { - writer.WriteEndObject(); - return true; - } + return true; } - - state.Current.DeclaredJsonPropertyInfo = state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!; } - success = OnWriteResume(writer, dictionary, options, ref state); - if (success) + state.Current.DeclaredJsonPropertyInfo = state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!; + } + + bool success = OnWriteResume(writer, dictionary, options, ref state); + if (success) + { + if (!state.Current.ProcessedEndToken) { - if (!state.Current.ProcessedEndToken) - { - state.Current.ProcessedEndToken = true; - writer.WriteEndObject(); - } + state.Current.ProcessedEndToken = true; + writer.WriteEndObject(); } } 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 index ab2343c2749c4..ef539d201b9d6 100644 --- 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 @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { @@ -16,7 +17,9 @@ internal sealed class JsonDictionaryOfStringTValueConverter { protected override void Add(TValue value, JsonSerializerOptions options, ref ReadStack state) { - string key = state.Current.KeyName!; + Debug.Assert(state.Current.ReturnValue is TCollection); + + string key = state.Current.JsonPropertyNameAsString!; ((TCollection)state.Current.ReturnValue!)[key] = value; } @@ -24,7 +27,7 @@ protected override void CreateCollection(ref ReadStack state) { if (state.Current.JsonClassInfo.CreateObject == null) { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(state.Current.JsonClassInfo.Type); + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(state.Current.JsonClassInfo.Type); } state.Current.ReturnValue = state.Current.JsonClassInfo.CreateObject(); @@ -47,6 +50,7 @@ protected internal override bool OnWriteResume( } else { + Debug.Assert(state.Current.CollectionEnumerator is Dictionary.Enumerator); enumerator = (Dictionary.Enumerator)state.Current.CollectionEnumerator; } @@ -65,6 +69,12 @@ protected internal override bool OnWriteResume( { do { + if (ShouldFlush(writer, ref state)) + { + state.Current.CollectionEnumerator = enumerator; + return false; + } + TValue element = enumerator.Current.Value; if (state.Current.PropertyState < StackFramePropertyState.Name) { @@ -79,7 +89,7 @@ protected internal override bool OnWriteResume( return false; } - state.Current.EndElement(); + state.Current.EndDictionaryElement(); } while (enumerator.MoveNext()); } 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 index 739dec4433da5..7a63ae126bcd1 100644 --- 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 @@ -16,6 +16,7 @@ internal sealed class JsonICollectionOfTConverter { protected override void Add(TElement value, ref ReadStack state) { + Debug.Assert(state.Current.ReturnValue is ICollection); ((ICollection)state.Current.ReturnValue!).Add(value); } @@ -23,7 +24,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti { JsonClassInfo classInfo = state.Current.JsonClassInfo; - if ((TypeToConvert.IsInterface || TypeToConvert.IsAbstract)) + if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) { if (!TypeToConvert.IsAssignableFrom(RuntimeType)) { @@ -38,17 +39,15 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti { ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); } - else - { - TCollection returnValue = (TCollection)classInfo.CreateObject!()!; - if (returnValue.IsReadOnly) - { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(TypeToConvert); - } + TCollection returnValue = (TCollection)classInfo.CreateObject()!; - state.Current.ReturnValue = returnValue; + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(TypeToConvert); } + + state.Current.ReturnValue = returnValue; } } @@ -69,6 +68,7 @@ protected override bool OnWriteResume( } else { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); enumerator = (IEnumerator)state.Current.CollectionEnumerator; } @@ -87,8 +87,6 @@ protected override bool OnWriteResume( state.Current.CollectionEnumerator = enumerator; return false; } - - state.Current.EndElement(); } while (enumerator.MoveNext()); return true; 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 index df905f0afeee8..814e1b9cc62d7 100644 --- 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 @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; +using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { @@ -12,12 +13,14 @@ namespace System.Text.Json.Serialization.Converters /// representing the dictionary element key and value. /// internal sealed class JsonIDictionaryConverter - : JsonDictionaryDefaultConverter + : JsonDictionaryDefaultConverter where TCollection : IDictionary { - protected override void Add(object value, JsonSerializerOptions options, ref ReadStack state) + protected override void Add(object? value, JsonSerializerOptions options, ref ReadStack state) { - string key = state.Current.KeyName!; + Debug.Assert(state.Current.ReturnValue is IDictionary); + + string key = state.Current.JsonPropertyNameAsString!; ((IDictionary)state.Current.ReturnValue!)[key] = value; } @@ -25,7 +28,7 @@ protected override void CreateCollection(ref ReadStack state) { JsonClassInfo classInfo = state.Current.JsonClassInfo; - if ((TypeToConvert.IsInterface || TypeToConvert.IsAbstract)) + if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) { if (!TypeToConvert.IsAssignableFrom(RuntimeType)) { @@ -40,17 +43,15 @@ protected override void CreateCollection(ref ReadStack state) { ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); } - else - { - TCollection returnValue = (TCollection)classInfo.CreateObject!()!; - if (returnValue.IsReadOnly) - { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(TypeToConvert); - } + TCollection returnValue = (TCollection)classInfo.CreateObject()!; - state.Current.ReturnValue = returnValue; + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(TypeToConvert); } + + state.Current.ReturnValue = returnValue; } } @@ -67,31 +68,31 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio } else { + Debug.Assert(state.Current.CollectionEnumerator is IDictionaryEnumerator); enumerator = (IDictionaryEnumerator)state.Current.CollectionEnumerator; } - JsonConverter converter = GetValueConverter(ref state); + JsonConverter converter = GetValueConverter(ref state); do { - if (!(enumerator.Key is string key)) + if (enumerator.Key is string key) { - // todo: add test for this. - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.DeclaredJsonPropertyInfo.RuntimePropertyType!); - Diagnostics.Debug.Assert(false); - return false; - } + key = GetKeyName(key, ref state, options); + writer.WritePropertyName(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; + } - object? element = enumerator.Value; - if (!converter.TryWrite(writer, element!, options, ref state)) + state.Current.EndDictionaryElement(); + } + else { - state.Current.CollectionEnumerator = enumerator; - return false; + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.DeclaredJsonPropertyInfo!.RuntimePropertyType!); } - - state.Current.EndElement(); } while (enumerator.MoveNext()); return true; 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 index a3858ad106a53..41e03f178d1fe 100644 --- 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 @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { @@ -16,7 +17,9 @@ internal sealed class JsonIDictionaryOfStringTValueConverter.Enumerator)state.Current.CollectionEnumerator; + 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); @@ -86,7 +94,7 @@ protected internal override bool OnWriteResume( return false; } - state.Current.EndElement(); + state.Current.EndDictionaryElement(); } while (enumerator.MoveNext()); return true; 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 index 049b3a8992fcd..2712385e03ec4 100644 --- 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 @@ -13,12 +13,13 @@ namespace System.Text.Json.Serialization.Converters /// /// internal sealed class JsonIEnumerableConverter - : JsonIEnumerableDefaultConverter + : JsonIEnumerableDefaultConverter where TCollection : IEnumerable { - protected override void Add(object value, ref ReadStack state) + protected override void Add(object? value, ref ReadStack state) { - ((List)state.Current.ReturnValue!).Add(value); + Debug.Assert(state.Current.ReturnValue is List); + ((List)state.Current.ReturnValue!).Add(value); } protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) @@ -28,7 +29,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); } - state.Current.ReturnValue = new List(); + state.Current.ReturnValue = new List(); } // Consider overriding ConvertCollection to convert the list to an array since a List is mutable. @@ -54,7 +55,7 @@ protected override bool OnWriteResume( enumerator = state.Current.CollectionEnumerator; } - JsonConverter converter = GetElementConverter(ref state); + JsonConverter converter = GetElementConverter(ref state); do { if (ShouldFlush(writer, ref state)) @@ -63,18 +64,16 @@ protected override bool OnWriteResume( return false; } - if (!converter.TryWrite(writer, enumerator.Current!, options, ref state)) + if (!converter.TryWrite(writer, enumerator.Current, options, ref state)) { state.Current.CollectionEnumerator = enumerator; return false; } - - state.Current.EndElement(); } while (enumerator.MoveNext()); return true; } - internal override Type RuntimeType => typeof(List); + 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 index ac4643dfbaa81..0081359d7165b 100644 --- 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 @@ -16,9 +16,9 @@ namespace System.Text.Json.Serialization.Converters /// internal class JsonIEnumerableConverterFactory : JsonConverterFactory { - private static readonly JsonIDictionaryConverter s_IDictionaryConverter = new JsonIDictionaryConverter(); - private static readonly JsonIEnumerableConverter s_IEnumerableConverter = new JsonIEnumerableConverter(); - private static readonly JsonIListConverter s_IListConverter = new JsonIListConverter(); + 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) { @@ -47,6 +47,7 @@ public override bool CanConvert(Type typeToConvert) { JsonConverter? converter = null; Type converterType; + Type[] genericArgs; Type? elementType = null; Type? actualTypeToConvert; @@ -71,10 +72,11 @@ public override bool CanConvert(Type typeToConvert) // Dictionary or deriving from Dictionary else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericBaseClass(typeof(Dictionary<,>))) != null) { - if (actualTypeToConvert.GetGenericArguments()[0] == typeof(string)) + genericArgs = actualTypeToConvert.GetGenericArguments(); + if (genericArgs[0] == typeof(string)) { converterType = typeof(JsonDictionaryOfStringTValueConverter<,>); - elementType = actualTypeToConvert.GetGenericArguments()[1]; + elementType = genericArgs[1]; } else { @@ -84,10 +86,11 @@ public override bool CanConvert(Type typeToConvert) // Immutable dictionaries from System.Collections.Immutable, e.g. ImmutableDictionary else if (typeToConvert.IsImmutableDictionaryType()) { - if (typeToConvert.GetGenericArguments()[0] == typeof(string)) + genericArgs = typeToConvert.GetGenericArguments(); + if (genericArgs[0] == typeof(string)) { converterType = typeof(JsonImmutableDictionaryOfStringTValueConverter<,>); - elementType = typeToConvert.GetGenericArguments()[1]; + elementType = genericArgs[1]; } else { @@ -97,10 +100,11 @@ public override bool CanConvert(Type typeToConvert) // IDictionary or deriving from IDictionary else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IDictionary<,>))) != null) { - if (actualTypeToConvert.GetGenericArguments()[0] == typeof(string)) + genericArgs = actualTypeToConvert.GetGenericArguments(); + if (genericArgs[0] == typeof(string)) { converterType = typeof(JsonIDictionaryOfStringTValueConverter<,>); - elementType = actualTypeToConvert.GetGenericArguments()[1]; + elementType = genericArgs[1]; } else { @@ -110,10 +114,11 @@ public override bool CanConvert(Type typeToConvert) // IReadOnlyDictionary or deriving from IReadOnlyDictionary else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IReadOnlyDictionary<,>))) != null) { - if (actualTypeToConvert.GetGenericArguments()[0] == typeof(string)) + genericArgs = actualTypeToConvert.GetGenericArguments(); + if (genericArgs[0] == typeof(string)) { converterType = typeof(JsonIReadOnlyDictionaryOfStringTValueConverter<,>); - elementType = actualTypeToConvert.GetGenericArguments()[1]; + elementType = genericArgs[1]; } else { @@ -179,7 +184,7 @@ public override bool CanConvert(Type typeToConvert) { if (typeToConvert == typeof(IDictionary)) { - return s_IDictionaryConverter; + return s_converterForIDictionary; } converterType = typeof(JsonIDictionaryConverter<>); @@ -188,7 +193,7 @@ public override bool CanConvert(Type typeToConvert) { if (typeToConvert == typeof(IList)) { - return s_IListConverter; + return s_converterForIList; } converterType = typeof(JsonIListConverter<>); @@ -202,7 +207,7 @@ public override bool CanConvert(Type typeToConvert) Debug.Assert(typeof(IEnumerable).IsAssignableFrom(typeToConvert)); if (typeToConvert == typeof(IEnumerable)) { - return s_IEnumerableConverter; + return s_converterForIEnumerable; } converterType = typeof(JsonIEnumerableConverter<>); @@ -222,7 +227,7 @@ public override bool CanConvert(Type typeToConvert) converter = (JsonConverter)Activator.CreateInstance( genericType, - BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, + BindingFlags.Instance | BindingFlags.Public, binder: null, args: null, culture: null)!; 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 index 7062b350165a6..b4911e45cbc65 100644 --- 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 @@ -2,38 +2,33 @@ // 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 : JsonIEnumerableConverter + internal abstract class JsonIEnumerableDefaultConverter : JsonCollectionConverter { protected abstract void Add(TElement value, ref ReadStack state); - - protected virtual void CreateCollection(ref ReadStack state, JsonSerializerOptions options) { } + 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 = state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!.ConverterBase as JsonConverter; - if (converter == null) - { - state.Current.JsonClassInfo.ElementClassInfo.PolicyProperty.ThrowCollectionNotSupportedException(); - } + 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!; + return converter; } protected static JsonConverter GetElementConverter(ref WriteStack state) { - JsonConverter? converter = state.Current.DeclaredJsonPropertyInfo.ConverterBase as JsonConverter; - if (converter == null) - { - state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!.ThrowCollectionNotSupportedException(); - } + 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!; + return converter; } internal override bool OnTryRead( @@ -62,7 +57,7 @@ internal override bool OnTryRead( // Fast path that avoids validation and extra indirection. while (true) { - reader.Read(); + reader.ReadWithVerify(); if (reader.TokenType == JsonTokenType.EndArray) { break; @@ -75,15 +70,16 @@ internal override bool OnTryRead( } else { + // Process all elements. while (true) { - reader.Read(); + reader.ReadWithVerify(); if (reader.TokenType == JsonTokenType.EndArray) { break; } - // Obtain the CLR value from the JSON and apply to the object. + // 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); } @@ -91,11 +87,13 @@ internal override bool OnTryRead( } else { - if (state.Current.ObjectState < StackFrameObjectState.StartToken) + // Slower path that supports continuation and preserved references. + + if (state.Current.ObjectState == StackFrameObjectState.None) { if (reader.TokenType == JsonTokenType.StartArray) { - state.Current.ObjectState = StackFrameObjectState.MetataPropertyValue; + state.Current.ObjectState = StackFrameObjectState.MetadataPropertyValue; } else if (shouldReadPreservedReferences) { @@ -113,17 +111,19 @@ internal override bool OnTryRead( } // Handle the metadata properties. - if (shouldReadPreservedReferences && state.Current.ObjectState < StackFrameObjectState.MetataPropertyValue) + if (shouldReadPreservedReferences && state.Current.ObjectState < StackFrameObjectState.MetadataPropertyValue) { - if (this.ResolveMetadata(ref reader, ref state, out value)) + 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; } } @@ -137,10 +137,7 @@ internal override bool OnTryRead( value = (TCollection)state.Current.ReturnValue!; if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, value)) { - // Reset so JsonPath throws exception with $id in it. - state.Current.MetadataPropertyName = MetadataPropertyName.Id; - - ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId); + ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId, ref state); } } @@ -152,7 +149,7 @@ internal override bool OnTryRead( { JsonConverter elementConverter = GetElementConverter(ref state); - // Main loop for processing elements. + // Process all elements. while (true) { if (state.Current.PropertyState < StackFramePropertyState.ReadValue) @@ -170,10 +167,6 @@ internal override bool OnTryRead( { if (reader.TokenType == JsonTokenType.EndArray) { - // Clear the MetadataPropertyName in case we were processing $values since - // we are not longer processing $values. - state.Current.MetadataPropertyName = MetadataPropertyName.NoMetadata; - break; } @@ -182,7 +175,7 @@ internal override bool OnTryRead( if (state.Current.PropertyState < StackFramePropertyState.TryRead) { - // Obtain the CLR value from the JSON and apply to the object. + // Get the value from the converter and add it. if (!elementConverter.TryRead(ref reader, typeof(TElement), options, ref state, out TElement element)) { value = default!; @@ -190,6 +183,7 @@ internal override bool OnTryRead( } Add(element, ref state); + // No need to set PropertyState to TryRead since we're done with this element now. state.Current.EndElement(); } @@ -213,7 +207,12 @@ internal override bool OnTryRead( if (reader.TokenType != JsonTokenType.EndObject) { - ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(typeToConvert, reader, ref state); + if (reader.TokenType == JsonTokenType.PropertyName) + { + state.Current.JsonPropertyName = reader.GetSpan().ToArray(); + } + + ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(typeToConvert, reader); } } } @@ -224,7 +223,7 @@ internal override bool OnTryRead( { if (reader.TokenType != JsonTokenType.EndObject) { - ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(typeToConvert, reader, ref state); + ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(typeToConvert, reader); } } } @@ -235,7 +234,7 @@ internal override bool OnTryRead( return true; } - internal override sealed bool OnTryWrite(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) + internal sealed override bool OnTryWrite(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) { bool success; 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 index c62792cd810ce..d1f2269366f9c 100644 --- 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 @@ -16,6 +16,7 @@ internal sealed class JsonIEnumerableOfTConverter : JsonI { protected override void Add(TElement value, ref ReadStack state) { + Debug.Assert(state.Current.ReturnValue is List); ((List)state.Current.ReturnValue!).Add(value); } @@ -42,6 +43,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, } else { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); enumerator = (IEnumerator)state.Current.CollectionEnumerator; } @@ -60,8 +62,6 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, state.Current.CollectionEnumerator = enumerator; return false; } - - state.Current.EndElement(); } while (enumerator.MoveNext()); return true; 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 index 26f2dca39f5f7..55bd1b27352d4 100644 --- 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 @@ -3,32 +3,32 @@ // See the LICENSE file in the project root for more information. using System.Collections; -using System.Collections.Concurrent; using System.Diagnostics; -using System.Reflection; namespace System.Text.Json.Serialization.Converters { - internal sealed class JsonIEnumerableWithAddMethodConverter : JsonIEnumerableDefaultConverter + internal sealed class JsonIEnumerableWithAddMethodConverter : + JsonIEnumerableDefaultConverter where TCollection : IEnumerable { - protected override void Add(object value, ref ReadStack state) + 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); + ((Action)state.Current.AddMethodDelegate)((TCollection)state.Current.ReturnValue!, value); } protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) { - JsonClassInfo classInfo = state.Current.JsonClassInfo; + JsonClassInfo.ConstructorDelegate? constructorDelegate = state.Current.JsonClassInfo.CreateObject; - if (classInfo.CreateObject == null) + if (constructorDelegate == null) { - ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(classInfo.Type); + ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); } - state.Current.ReturnValue = classInfo.CreateObject()!; - state.Current.AddMethodDelegate = GetOrAddEnumerableAddMethodDelegate(classInfo.Type, options); + state.Current.ReturnValue = constructorDelegate(); + state.Current.AddMethodDelegate = GetAddMethodDelegate(options); } protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) @@ -47,7 +47,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, enumerator = state.Current.CollectionEnumerator; } - JsonConverter converter = GetElementConverter(ref state); + JsonConverter converter = GetElementConverter(ref state); do { if (ShouldFlush(writer, ref state)) @@ -56,31 +56,27 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, return false; } - if (!converter.TryWrite(writer, enumerator.Current!, options, ref state)) + if (!converter.TryWrite(writer, enumerator.Current, options, ref state)) { state.Current.CollectionEnumerator = enumerator; return false; } - - state.Current.EndElement(); } while (enumerator.MoveNext()); return true; } - internal override Type RuntimeType => TypeToConvert; - - private readonly ConcurrentDictionary> _delegates = new ConcurrentDictionary>(); + private Action? _addMethodDelegate; - internal Action GetOrAddEnumerableAddMethodDelegate(Type type, JsonSerializerOptions options) + internal Action GetAddMethodDelegate(JsonSerializerOptions options) { - if (!_delegates.TryGetValue(type, out Action? result)) + if (_addMethodDelegate == null) { // We verified this exists when we created the converter in the enumerable converter factory. - result = options.MemberAccessorStrategy.CreateAddMethodDelegate(); + _addMethodDelegate = options.MemberAccessorStrategy.CreateAddMethodDelegate(); } - return result; + 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 index 09aca5462f3b4..71785fdc2a2f2 100644 --- 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 @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; +using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { @@ -11,23 +12,24 @@ namespace System.Text.Json.Serialization.Converters internal sealed class JsonIListConverter : JsonIEnumerableDefaultConverter where TCollection : IList { - protected override void Add(object value, ref ReadStack state) + protected override void Add(object? value, ref ReadStack state) { - ((IList)state.Current.ReturnValue!).Add(value); + 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.IsInterface || TypeToConvert.IsAbstract) { if (!TypeToConvert.IsAssignableFrom(RuntimeType)) { ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); } - state.Current.ReturnValue = new List(); + state.Current.ReturnValue = new List(); } else { @@ -35,17 +37,15 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti { ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); } - else - { - TCollection returnValue = (TCollection)classInfo.CreateObject!()!; - if (returnValue.IsReadOnly) - { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(TypeToConvert); - } + TCollection returnValue = (TCollection)classInfo.CreateObject()!; - state.Current.ReturnValue = returnValue; + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(TypeToConvert); } + + state.Current.ReturnValue = returnValue; } } @@ -65,7 +65,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, enumerator = state.Current.CollectionEnumerator; } - JsonConverter converter = JsonSerializerOptions.GetObjectConverter(); + JsonConverter converter = GetElementConverter(ref state); do { if (ShouldFlush(writer, ref state)) @@ -81,8 +81,6 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, state.Current.CollectionEnumerator = enumerator; return false; } - - state.Current.EndElement(); } while (enumerator.MoveNext()); return true; @@ -94,7 +92,7 @@ internal override Type RuntimeType { if (TypeToConvert.IsAbstract || TypeToConvert.IsInterface) { - return typeof(List); + 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 index 4558d08f6359d..f227857cbb265 100644 --- 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 @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { @@ -14,6 +15,7 @@ internal sealed class JsonIListOfTConverter : JsonIEnumer { protected override void Add(TElement value, ref ReadStack state) { + Debug.Assert(state.Current.ReturnValue is TCollection); ((TCollection)state.Current.ReturnValue!).Add(value); } @@ -21,7 +23,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti { JsonClassInfo classInfo = state.Current.JsonClassInfo; - if ((TypeToConvert.IsInterface || TypeToConvert.IsAbstract)) + if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) { if (!TypeToConvert.IsAssignableFrom(RuntimeType)) { @@ -36,17 +38,15 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti { ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); } - else - { - TCollection returnValue = (TCollection)classInfo.CreateObject!()!; - if (returnValue.IsReadOnly) - { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(TypeToConvert); - } + TCollection returnValue = (TCollection)classInfo.CreateObject()!; - state.Current.ReturnValue = returnValue; + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(TypeToConvert); } + + state.Current.ReturnValue = returnValue; } } @@ -63,6 +63,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, } else { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); enumerator = (IEnumerator)state.Current.CollectionEnumerator; } @@ -81,8 +82,6 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, state.Current.CollectionEnumerator = enumerator; return false; } - - state.Current.EndElement(); } while (enumerator.MoveNext()); return true; 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 index 2c5a2c0a072af..5dfcc36e064f5 100644 --- 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 @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { @@ -11,7 +12,9 @@ internal sealed class JsonIReadOnlyDictionaryOfStringTValueConverter); + + string key = state.Current.JsonPropertyNameAsString!; ((Dictionary)state.Current.ReturnValue!)[key] = value; } @@ -38,12 +41,19 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio } 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); @@ -54,7 +64,7 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio return false; } - state.Current.EndElement(); + state.Current.EndDictionaryElement(); } while (enumerator.MoveNext()); return true; 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 index 57954c9fc351b..eb9b79e00a210 100644 --- 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 @@ -12,6 +12,7 @@ internal sealed class JsonISetOfTConverter : JsonIEnumera { protected override void Add(TElement value, ref ReadStack state) { + Debug.Assert(state.Current.ReturnValue is TCollection); ((TCollection)state.Current.ReturnValue!).Add(value); } @@ -19,7 +20,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti { JsonClassInfo classInfo = state.Current.JsonClassInfo; - if ((TypeToConvert.IsInterface || TypeToConvert.IsAbstract)) + if (TypeToConvert.IsInterface || TypeToConvert.IsAbstract) { if (!TypeToConvert.IsAssignableFrom(RuntimeType)) { @@ -34,17 +35,15 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti { ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert); } - else - { - TCollection returnValue = (TCollection)classInfo.CreateObject!()!; - if (returnValue.IsReadOnly) - { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(TypeToConvert); - } + TCollection returnValue = (TCollection)classInfo.CreateObject()!; - state.Current.ReturnValue = returnValue; + if (returnValue.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(TypeToConvert); } + + state.Current.ReturnValue = returnValue; } } @@ -61,6 +60,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, } else { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); enumerator = (IEnumerator)state.Current.CollectionEnumerator; } @@ -79,8 +79,6 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, state.Current.CollectionEnumerator = enumerator; return false; } - - state.Current.EndElement(); } while (enumerator.MoveNext()); return true; 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 index ca19e5b001b59..0e12a8e87f446 100644 --- 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 @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.Collections.Concurrent; +using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { @@ -12,11 +12,13 @@ internal sealed class JsonImmutableDictionaryOfStringTValueConverter); + + string key = state.Current.JsonPropertyNameAsString!; ((Dictionary)state.Current.ReturnValue!)[key] = value; } - internal override bool CanHaveMetadata => false; + internal override bool CanHaveIdMetadata => false; protected override void CreateCollection(ref ReadStack state) { @@ -41,12 +43,19 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio } 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); @@ -57,14 +66,12 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio return false; } - state.Current.EndElement(); + state.Current.EndDictionaryElement(); } while (enumerator.MoveNext()); return true; } - internal override Type RuntimeType => TypeToConvert; - private Func>, TCollection>? _creatorDelegate; private Func>, TCollection> GetCreatorDelegate(JsonSerializerOptions options) 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 index d7e271a763e68..d3cd9bcd45512 100644 --- 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 @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.Collections.Concurrent; +using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { @@ -12,10 +12,11 @@ internal sealed class JsonImmutableEnumerableOfTConverter { 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 CanHaveMetadata => false; + internal override bool CanHaveIdMetadata => false; protected override void CreateCollection(ref ReadStack state, JsonSerializerOptions options) { @@ -40,6 +41,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, } else { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); enumerator = (IEnumerator)state.Current.CollectionEnumerator; } @@ -58,15 +60,11 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, state.Current.CollectionEnumerator = enumerator; return false; } - - state.Current.EndElement(); } while (enumerator.MoveNext()); return true; } - internal override Type RuntimeType => TypeToConvert; - private Func, TCollection>? _creatorDelegate; private Func, TCollection> GetCreatorDelegate(JsonSerializerOptions options) 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 index ba7210ec7f091..31c94131fb03a 100644 --- 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 @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { @@ -13,6 +14,7 @@ internal sealed class JsonListOfTConverter { protected override void Add(TElement value, ref ReadStack state) { + Debug.Assert(state.Current.ReturnValue is TCollection); ((TCollection)state.Current.ReturnValue!).Add(value); } @@ -20,7 +22,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti { if (state.Current.JsonClassInfo.CreateObject == null) { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(state.Current.JsonClassInfo.Type); + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(state.Current.JsonClassInfo.Type); } state.Current.ReturnValue = state.Current.JsonClassInfo.CreateObject(); @@ -29,6 +31,8 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti 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); @@ -56,8 +60,6 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, state.Current.EnumeratorIndex = ++index; return false; } - - state.Current.EndElement(); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectFactoryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectConverterFactory.cs similarity index 73% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectFactoryConverter.cs rename to src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectConverterFactory.cs index 397f560dbd62a..00d267a7e01fa 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectFactoryConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonObjectConverterFactory.cs @@ -3,6 +3,7 @@ // 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 @@ -10,12 +11,14 @@ namespace System.Text.Json.Serialization.Converters /// /// Converter factory for all object-based types (non-enumerable and non-primitive). /// - internal class JsonObjectFactoryConverter : JsonConverterFactory + internal class JsonObjectConverterFactory : JsonConverterFactory { public override bool CanConvert(Type typeToConvert) { - // If the IEnumerableConverterFactory doesn't support the collection, ObjectConverterFactory doesn't either. - return !typeof(IEnumerable).IsAssignableFrom(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) 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 index ba1b2df9b6f47..49c701a3348d4 100644 --- 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 @@ -32,11 +32,11 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, obj = state.Current.JsonClassInfo.CreateObject!()!; - // Read all properties. + // Process all properties. while (true) { // Read the property name or EndObject. - reader.Read(); + reader.ReadWithVerify(); JsonTokenType tokenType = reader.TokenType; if (tokenType == JsonTokenType.EndObject) @@ -49,23 +49,23 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); } - JsonSerializer.LookupProperty( + JsonPropertyInfo jsonPropertyInfo = JsonSerializer.LookupProperty( obj, ref reader, options, ref state, - out JsonPropertyInfo jsonPropertyInfo, out bool useExtensionProperty); // Skip the property if not found. if (!jsonPropertyInfo.ShouldDeserialize) { - reader.TrySkip(); + reader.Skip(); + state.Current.EndProperty(); continue; } // Set the property value. - reader.Read(); + reader.ReadWithVerify(); if (!useExtensionProperty) { @@ -75,11 +75,16 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, { 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 { - if (state.Current.ObjectState < StackFrameObjectState.StartToken) + // Slower path that supports continuation and preserved references. + + if (state.Current.ObjectState == StackFrameObjectState.None) { if (reader.TokenType != JsonTokenType.StartObject) { @@ -90,29 +95,26 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, } // Handle the metadata properties. - if (state.Current.ObjectState < StackFrameObjectState.MetataPropertyValue) + if (state.Current.ObjectState < StackFrameObjectState.MetadataPropertyValue) { if (shouldReadPreservedReferences) { - if (this.ResolveMetadata(ref reader, ref state, out value)) + if (JsonSerializer.ResolveMetadata(this, ref reader, ref state)) { if (state.Current.ObjectState == StackFrameObjectState.MetadataRefPropertyEndObject) { - if (!CanHaveMetadata) - { - ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(TypeToConvert); - } - + value = (T)state.Current.ReturnValue!; return true; } } else { + value = default!; return false; } } - state.Current.ObjectState = StackFrameObjectState.MetataPropertyValue; + state.Current.ObjectState = StackFrameObjectState.MetadataPropertyValue; } if (state.Current.ObjectState < StackFrameObjectState.CreatedObject) @@ -127,10 +129,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, { if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, obj)) { - // Reset so JsonPath throws exception with $id in it. - state.Current.MetadataPropertyName = MetadataPropertyName.Id; - - ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId); + ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId, ref state); } } @@ -143,11 +142,11 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, Debug.Assert(obj != null); } - // Read all properties. + // Process all properties. while (true) { // Determine the property. - if (state.Current.PropertyState < StackFramePropertyState.ReadName) + if (state.Current.PropertyState == StackFramePropertyState.None) { state.Current.PropertyState = StackFramePropertyState.ReadName; @@ -177,19 +176,19 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); } - JsonSerializer.LookupProperty( + jsonPropertyInfo = JsonSerializer.LookupProperty( obj, ref reader, options, ref state, - out jsonPropertyInfo, out bool useExtensionProperty); state.Current.UseExtensionProperty = useExtensionProperty; } else { - jsonPropertyInfo = state.Current.JsonPropertyInfo; + Debug.Assert(state.Current.JsonPropertyInfo != null); + jsonPropertyInfo = state.Current.JsonPropertyInfo!; } if (state.Current.PropertyState < StackFramePropertyState.ReadValue) @@ -289,23 +288,18 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializer { if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref) { - writer.WriteEndObject(); return true; } } JsonPropertyInfo? dataExtensionProperty = state.Current.JsonClassInfo.DataExtensionProperty; - int propertyCount; + int propertyCount = 0; JsonPropertyInfo[]? propertyCacheArray = state.Current.JsonClassInfo.PropertyCacheArray; if (propertyCacheArray != null) { propertyCount = propertyCacheArray.Length; } - else - { - propertyCount = 0; - } for (int i = 0; i < propertyCount; i++) { @@ -355,7 +349,6 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializer { if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref) { - writer.WriteEndObject(); return true; } } @@ -365,16 +358,12 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializer JsonPropertyInfo? dataExtensionProperty = state.Current.JsonClassInfo.DataExtensionProperty; - int propertyCount; + int propertyCount = 0; JsonPropertyInfo[]? propertyCacheArray = state.Current.JsonClassInfo.PropertyCacheArray; if (propertyCacheArray != null) { propertyCount = propertyCacheArray.Length; } - else - { - propertyCount = 0; - } while (propertyCount > state.Current.EnumeratorIndex) { 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 index c40193e38c17c..a0cc38d40abb2 100644 --- 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 @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { @@ -11,6 +12,7 @@ internal sealed class JsonQueueOfTConverter : JsonIEnumer { protected override void Add(TElement value, ref ReadStack state) { + Debug.Assert(state.Current.ReturnValue is TCollection); ((TCollection)state.Current.ReturnValue!).Enqueue(value); } @@ -18,7 +20,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti { if (state.Current.JsonClassInfo.CreateObject == null) { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(state.Current.JsonClassInfo.Type); + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(state.Current.JsonClassInfo.Type); } state.Current.ReturnValue = state.Current.JsonClassInfo.CreateObject(); @@ -37,6 +39,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, } else { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); enumerator = (IEnumerator)state.Current.CollectionEnumerator; } @@ -55,13 +58,9 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, state.Current.CollectionEnumerator = enumerator; return false; } - - state.Current.EndElement(); } while (enumerator.MoveNext()); return true; } - - internal override Type RuntimeType => typeof(Queue); } } 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 index feb50c82b3fea..31dac18efe1a2 100644 --- 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 @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { @@ -11,6 +12,7 @@ internal sealed class JsonStackOfTConverter : JsonIEnumer { protected override void Add(TElement value, ref ReadStack state) { + Debug.Assert(state.Current.ReturnValue is TCollection); ((TCollection)state.Current.ReturnValue!).Push(value); } @@ -18,7 +20,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti { if (state.Current.JsonClassInfo.CreateObject == null) { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(state.Current.JsonClassInfo.Type); + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(state.Current.JsonClassInfo.Type); } state.Current.ReturnValue = state.Current.JsonClassInfo.CreateObject(); @@ -37,6 +39,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, } else { + Debug.Assert(state.Current.CollectionEnumerator is IEnumerator); enumerator = (IEnumerator)state.Current.CollectionEnumerator; } @@ -55,13 +58,9 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, state.Current.CollectionEnumerator = enumerator; return false; } - - state.Current.EndElement(); } while (enumerator.MoveNext()); return true; } - - internal override Type RuntimeType => TypeToConvert; } } 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 2c7f2455e6534..70eb76e1102f9 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 @@ -36,7 +36,7 @@ internal override bool OnTryRead( bool valueSet = false; // Get the first property. - reader.Read(); + reader.ReadWithVerify(); if (reader.TokenType != JsonTokenType.PropertyName) { ThrowHelper.ThrowJsonException(); @@ -45,13 +45,13 @@ internal override bool OnTryRead( string propertyName = reader.GetString()!; if (propertyName == KeyName) { - reader.Read(); + reader.ReadWithVerify(); k = JsonSerializer.Deserialize(ref reader, options, ref state, KeyName); keySet = true; } else if (propertyName == ValueName) { - reader.Read(); + reader.ReadWithVerify(); v = JsonSerializer.Deserialize(ref reader, options, ref state, ValueName); valueSet = true; } @@ -61,7 +61,7 @@ internal override bool OnTryRead( } // Get the second property. - reader.Read(); + reader.ReadWithVerify(); if (reader.TokenType != JsonTokenType.PropertyName) { ThrowHelper.ThrowJsonException(); @@ -70,13 +70,13 @@ internal override bool OnTryRead( propertyName = reader.GetString()!; if (propertyName == KeyName) { - reader.Read(); + reader.ReadWithVerify(); k = JsonSerializer.Deserialize(ref reader, options, ref state, KeyName); keySet = true; } else if (propertyName == ValueName) { - reader.Read(); + reader.ReadWithVerify(); v = JsonSerializer.Deserialize(ref reader, options, ref state, ValueName); valueSet = true; } @@ -90,7 +90,7 @@ internal override bool OnTryRead( ThrowHelper.ThrowJsonException(); } - reader.Read(); + reader.ReadWithVerify(); if (reader.TokenType != JsonTokenType.EndObject) { 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 index b182e36e9f7da..525793d5e0565 100644 --- 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 @@ -8,7 +8,7 @@ internal class JsonValueConverterNullable : JsonConverter where T : struc { // 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 JsonConverter _converter; + private readonly JsonConverter _converter; public JsonValueConverterNullable(JsonConverter converter) { 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 index 78d039d1641d1..742b1843f9f74 100644 --- 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 @@ -2,6 +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.Reflection; namespace System.Text.Json.Serialization @@ -15,13 +16,14 @@ public override bool CanConvert(Type typeToConvert) 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) { - // todo: add test for this - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(valueTypeToConvert); + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(valueTypeToConvert); } JsonConverter converter = (JsonConverter)Activator.CreateInstance( 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 index d8fa73bb5ea19..703ba02be213d 100644 --- 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 @@ -2,12 +2,11 @@ // 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.Buffers; using System.Diagnostics; using System.Reflection; -namespace System.Text.Json +namespace System.Text.Json.Serialization { internal static class ExtensionMethods { @@ -46,7 +45,7 @@ internal static class ExtensionMethods Debug.Assert(!baseType.IsInterface); Debug.Assert(baseType == baseType.GetGenericTypeDefinition()); - Type baseTypeToCheck = type; + Type? baseTypeToCheck = type; while (baseTypeToCheck != null && baseTypeToCheck != typeof(object)) { @@ -59,7 +58,7 @@ internal static class ExtensionMethods } } - baseTypeToCheck = baseTypeToCheck.BaseType!; + baseTypeToCheck = baseTypeToCheck.BaseType; } return null; @@ -98,27 +97,6 @@ internal static class ExtensionMethods return null; } - internal static Type? GetCompatibleNonGenericBaseClass(this Type type, string baseTypeAssemblyQualifiedNamePrefix) - { - Type baseTypeToCheck = type; - - while (baseTypeToCheck != null && baseTypeToCheck != typeof(object)) - { - if (!baseTypeToCheck.IsGenericType) - { - Type nonGenericTypeToCheck = baseTypeToCheck; - if (nonGenericTypeToCheck.AssemblyQualifiedName!.StartsWith(baseTypeAssemblyQualifiedNamePrefix)) - { - return baseTypeToCheck; - } - } - - baseTypeToCheck = baseTypeToCheck.BaseType!; - } - - return null; - } - public static bool IsImmutableDictionaryType(this Type type) { if (!type.IsGenericType || !type.Assembly.FullName!.StartsWith("System.Collections.Immutable,")) @@ -178,7 +156,7 @@ public static MethodInfo GetImmutableEnumerableCreateRangeMethod(this Type type, } } - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(type); + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(type); return null!; } @@ -198,7 +176,7 @@ public static MethodInfo GetImmutableDictionaryCreateRangeMethod(this Type type, } } - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(type); + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(type); return null!; } @@ -277,8 +255,13 @@ private static Type GetImmutableDictionaryConstructingType(Type type) public static bool IsNonGenericStackOrQueue(this Type type) { - return type.GetCompatibleNonGenericBaseClass("System.Collections.Stack, System.Collections.NonGeneric") != null || - type.GetCompatibleNonGenericBaseClass("System.Collections.Queue, System.Collections.NonGeneric") != null; + 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/JsonClassInfo.AddProperty.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs index 287601e78144f..87d2a2e981c4c 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 @@ -29,7 +29,7 @@ private JsonPropertyInfo AddProperty(Type propertyType, PropertyInfo propertyInf if (converter == null) { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(propertyType, parentClassType, propertyInfo); + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(propertyType, parentClassType, propertyInfo); } return CreateProperty( 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 5c12c2a2f6541..a6f6961c52324 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 @@ -10,7 +10,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Converters; namespace System.Text.Json { @@ -39,9 +38,6 @@ internal sealed partial class JsonClassInfo public delegate object? ConstructorDelegate(); public ConstructorDelegate? CreateObject { get; private set; } - public delegate TCollection ConstructorDelegate(ICollection elements); - public delegate TCollection ConstructorDelegate(IEnumerable elements); - public ClassType ClassType { get; private set; } public JsonPropertyInfo? DataExtensionProperty { get; private set; } @@ -118,7 +114,6 @@ public JsonClassInfo(Type type, JsonSerializerOptions options) { Type = type; Options = options; - JsonConverter? converter; ClassType = GetClassType( type, @@ -126,7 +121,7 @@ public JsonClassInfo(Type type, JsonSerializerOptions options) propertyInfo: null, out Type? runtimeType, out Type? elementType, - out converter, + out JsonConverter? converter, options); switch (ClassType) @@ -215,7 +210,7 @@ public JsonClassInfo(Type type, JsonSerializerOptions options) break; case ClassType.Invalid: { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(type); + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(type); } break; default: @@ -230,15 +225,13 @@ private bool DetermineExtensionDataProperty(Dictionary if (jsonPropertyInfo != null) { Type declaredPropertyType = jsonPropertyInfo.DeclaredPropertyType; - if (typeof(Dictionary).IsAssignableFrom(declaredPropertyType) || - typeof(Dictionary).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_SerializationNotSupportedCollection(declaredPropertyType); + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(declaredPropertyType); } } else @@ -247,7 +240,6 @@ private bool DetermineExtensionDataProperty(Dictionary } DataExtensionProperty = jsonPropertyInfo; - jsonPropertyInfo.EscapedName = null; return true; } @@ -571,17 +563,13 @@ public static ClassType GetClassType( { runtimeType = converterRuntimeType; } - else if (converterRuntimeType.IsAssignableFrom(type)) - { - runtimeType = type; - } - else if (converter.TypeToConvert.IsAssignableFrom(type)) + else if (converterRuntimeType.IsAssignableFrom(type) || converter.TypeToConvert.IsAssignableFrom(type)) { runtimeType = type; } else { - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(type, parentClassType, propertyInfo); + throw ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(type, parentClassType, propertyInfo); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIEnumerableConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonCollectionConverter.cs similarity index 51% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIEnumerableConverter.cs rename to src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonCollectionConverter.cs index 664e3a259fe8a..054e38f83ea0f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIEnumerableConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonCollectionConverter.cs @@ -5,14 +5,11 @@ namespace System.Text.Json.Serialization { /// - /// Base class for IEnumerable-based collections. + /// Base class for all collections. Collections are assumed to implement System.Collections.IEnumerable. /// - internal abstract class JsonIEnumerableConverter : JsonResumableConverter + internal abstract class JsonCollectionConverter : JsonResumableConverter { - private Type _elementType = typeof(TElement); - - internal override bool CanHaveValuesMetadata => true; internal override ClassType ClassType => ClassType.Enumerable; - internal override Type ElementType => _elementType; + 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 index dd8a66035bb86..da4021f81707a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.ReadAhead.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.ReadAhead.cs @@ -12,6 +12,9 @@ namespace System.Text.Json.Serialization /// 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)] @@ -28,11 +31,10 @@ internal static bool SingleValueReadWithReadAhead(ClassType classType, ref Utf8J 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 - // array brace. - state.InitialReaderState = reader.CurrentState; - state.InitialReaderBytesConsumed = reader.BytesConsumed; + // 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()) { @@ -47,15 +49,14 @@ internal static bool DoSingleValueReadWithReadAhead(ref Utf8JsonReader reader, r 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. + // the current token to either attempt to skip again or to actually read the value. - reader = new Utf8JsonReader(reader.OriginalSpan.Slice(checked((int)state.InitialReaderBytesConsumed)), + reader = new Utf8JsonReader(reader.OriginalSpan.Slice(checked((int)initialReaderBytesConsumed)), isFinalBlock: reader.IsFinalBlock, - state: state.InitialReaderState); + state: initialReaderState); Debug.Assert(reader.BytesConsumed == 0); - state.BytesConsumed += state.InitialReaderBytesConsumed; + state.BytesConsumed += initialReaderBytesConsumed; if (!complete) { @@ -63,8 +64,8 @@ internal static bool DoSingleValueReadWithReadAhead(ref Utf8JsonReader reader, r return false; } - // Success, requeue the reader to the token for HandleValue. - reader.Read(); + // Success, requeue the reader to the start token. + reader.ReadWithVerify(); Debug.Assert(tokenType == reader.TokenType); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs index eff19aeef33ae..1bc90ae29a17d 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 @@ -37,17 +37,12 @@ internal virtual bool HandleNullValue internal bool CanUseDirectReadOrWrite { get; set; } /// - /// Can the converter have any metadata (properties starting with $). + /// Can the converter have $id metadata. /// - internal virtual bool CanHaveMetadata => !TypeToConvert.IsValueType; + internal virtual bool CanHaveIdMetadata => true; internal bool CanBePolymorphic { get; set; } - /// - /// Can the converter have $values metadata. - /// - internal virtual bool CanHaveValuesMetadata => false; - internal abstract JsonPropertyInfo CreateJsonPropertyInfo(); internal abstract Type? ElementType { get; } 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 05eb5a1e55391..352c32ed859c7 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,7 +19,7 @@ public abstract class JsonConverterFactory : JsonConverter /// protected JsonConverterFactory() { } - internal override sealed ClassType ClassType + internal sealed override ClassType ClassType { get { @@ -40,32 +40,41 @@ internal override sealed ClassType ClassType internal override JsonPropertyInfo CreateJsonPropertyInfo() { - throw new NotSupportedException(); + // We should never get here. + Debug.Assert(false); + + throw new InvalidOperationException(); } internal sealed override Type? ElementType => null; - internal JsonConverter GetConverterInternal(Type typeToConvert, JsonSerializerOptions options) + internal JsonConverter? GetConverterInternal(Type typeToConvert, JsonSerializerOptions options) { Debug.Assert(CanConvert(typeToConvert)); - return CreateConverter(typeToConvert, options)!; + return CreateConverter(typeToConvert, options); } - internal override sealed bool TryReadAsObject( + internal sealed override bool TryReadAsObject( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out object? value) { - throw new NotSupportedException(); + // We should never get here. + Debug.Assert(false); + + throw new InvalidOperationException(); } - internal override sealed bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state) + internal sealed override bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state) { - throw new NotSupportedException(); + // We should never get here. + Debug.Assert(false); + + throw new InvalidOperationException(); } - internal override sealed Type TypeToConvert => null!; + 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 f603b219aa3fa..13ff86dfb40e8 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 @@ -12,8 +12,6 @@ namespace System.Text.Json.Serialization /// The to convert. public abstract class JsonConverter : JsonConverter { - private Type _typeToConvert = typeof(T); - /// /// When overidden, constructs a new instance. /// @@ -42,7 +40,7 @@ public override bool CanConvert(Type typeToConvert) internal override ClassType ClassType => ClassType.Value; - internal override sealed JsonPropertyInfo CreateJsonPropertyInfo() + internal sealed override JsonPropertyInfo CreateJsonPropertyInfo() { return new JsonPropertyInfo(); } @@ -52,10 +50,10 @@ internal override sealed JsonPropertyInfo CreateJsonPropertyInfo() /// /// Is the converter built-in. /// - internal bool IsInternalConverter; + internal bool IsInternalConverter { get; set; } // This non-generic API is sealed as it just forwards to the generic version. - internal override sealed bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state) + 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); @@ -69,7 +67,7 @@ internal virtual bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerO } // This non-generic API is sealed as it just forwards to the generic version. - internal override sealed bool TryReadAsObject(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out object? value) + 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) @@ -126,15 +124,16 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali else #endif { - state.Current.OriginalPropertyTokenType = reader.TokenType; - state.Current.OriginalPropertyDepth = reader.CurrentDepth; - state.Current.OriginalPropertyBytesConsumed = reader.BytesConsumed; + JsonTokenType originalPropertyTokenType = reader.TokenType; + int originalPropertyDepth = reader.CurrentDepth; + long originalPropertyBytesConsumed = reader.BytesConsumed; value = Read(ref reader, typeToConvert, options); VerifyRead( - state.Current.OriginalPropertyTokenType, - state.Current.OriginalPropertyDepth, - state.Current.OriginalPropertyBytesConsumed != reader.BytesConsumed, + originalPropertyTokenType, + originalPropertyDepth, + originalPropertyBytesConsumed, + isValueConverter: true, ref reader); } @@ -195,7 +194,8 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali VerifyRead( state.Current.OriginalTokenType, state.Current.OriginalDepth, - hasConsumedAnyBytes: true, + bytesConsumed : 0, + isValueConverter: false, ref reader); // No need to clear state.Current.* since a stack pop will occur. @@ -231,6 +231,7 @@ internal bool TryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions opt if (type != TypeToConvert) { + // Handle polymorphic case and get the new converter. JsonConverter jsonConverter = state.Current.InitializeReEntry(type, options); if (jsonConverter != this) { @@ -242,13 +243,12 @@ internal bool TryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions opt if (ClassType == ClassType.Value) { - if (!state.IsContinuation) - { - state.Current.OriginalPropertyDepth = writer.CurrentDepth; - } + Debug.Assert(!state.IsContinuation); + + int originalPropertyDepth = writer.CurrentDepth; Write(writer, value, options); - VerifyWrite(state.Current.OriginalPropertyDepth, writer); + VerifyWrite(originalPropertyDepth, writer); return true; } @@ -289,10 +289,9 @@ internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, Json if (ClassType == ClassType.Value) { - if (!state.IsContinuation) - { - state.Current.OriginalPropertyDepth = writer.CurrentDepth; - } + Debug.Assert(!state.IsContinuation); + + int originalPropertyDepth = writer.CurrentDepth; // Ignore the naming policy for extension data. state.Current.IgnoreDictionaryKeyPolicy = true; @@ -300,7 +299,7 @@ internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, Json success = dictionaryConverter.OnWriteResume(writer, value, options, ref state); if (success) { - VerifyWrite(state.Current.OriginalPropertyDepth, writer); + VerifyWrite(originalPropertyDepth, writer); } } else @@ -330,9 +329,9 @@ internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, Json return success; } - internal override sealed Type TypeToConvert => _typeToConvert; + internal sealed override Type TypeToConvert => typeof(T); - internal void VerifyRead(JsonTokenType tokenType, int depth, bool hasConsumedAnyBytes, ref Utf8JsonReader reader) + internal void VerifyRead(JsonTokenType tokenType, int depth, long bytesConsumed, bool isValueConverter, ref Utf8JsonReader reader) { switch (tokenType) { @@ -361,8 +360,9 @@ internal void VerifyRead(JsonTokenType tokenType, int depth, bool hasConsumedAny break; default: - // A non-array or non-object should not make any additional reads. - if (hasConsumedAnyBytes) + // 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); } 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 index a1eb390db4be5..86965da892eec 100644 --- 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 @@ -11,6 +11,6 @@ namespace System.Text.Json.Serialization internal abstract class JsonObjectConverter : JsonResumableConverter { internal override ClassType ClassType => ClassType.Object; - internal override sealed Type? ElementType => null; + 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 099023889f582..5e45f08b3d59b 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 @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json.Serialization; @@ -105,10 +106,7 @@ private void DetermineSerializationCapabilities() { if (HasGetter) { - if (ConverterBase == null) - { - ThrowCollectionNotSupportedException(); - } + Debug.Assert(ConverterBase != null); ShouldSerialize = true; @@ -153,6 +151,8 @@ public virtual void Initialize( JsonConverter converter, JsonSerializerOptions options) { + Debug.Assert(converter != null); + ParentClassType = parentClassType; DeclaredPropertyType = declaredPropertyType; RuntimePropertyType = runtimePropertyType; @@ -171,7 +171,7 @@ public virtual void Initialize( // The name of the property with any casing policy or the name specified from JsonPropertyNameAttribute. public byte[]? Name { get; private set; } - public string? NameAsString { get; set; } + public string? NameAsString { get; private set; } // Key for fast property name lookup. public ulong PropertyNameKey { get; set; } @@ -182,31 +182,45 @@ public virtual void Initialize( public bool ReadJsonAndAddExtensionProperty(object obj, ref ReadStack state, ref Utf8JsonReader reader) { object propValue = GetValueAsObject(obj)!; - IDictionary? dictionaryObject = propValue as IDictionary; - if (dictionaryObject != null && reader.TokenType == JsonTokenType.Null) + if (propValue is IDictionary dictionaryObject) { - // A null JSON value is treated as a null object reference. - dictionaryObject[state.Current.KeyName!] = null; + // Handle case where extension property is System.Object-based. + + if (reader.TokenType == JsonTokenType.Null) + { + // 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; + + if (!converter.TryRead(ref reader, typeof(JsonElement), Options, ref state, out object? value)) + { + return false; + } + + dictionaryObject[state.Current.JsonPropertyNameAsString!] = value; + } } else { - JsonConverter converter = JsonSerializerOptions.GetJsonElementConverter(); - if (!converter.TryRead(ref reader, typeof(JsonElement), Options, ref state, out JsonElement jsonElement)) + // Handle case where extension property is JsonElement-based. + + Debug.Assert(propValue is IDictionary); + IDictionary dictionaryJsonElement = (IDictionary)propValue; + + JsonConverter converter = (JsonConverter) + state.Current.JsonPropertyInfo!.RuntimeClassInfo.ElementClassInfo!.PolicyProperty!.ConverterBase; + + if (!converter.TryRead(ref reader, typeof(JsonElement), Options, ref state, out JsonElement value)) { - // No need to set a partial object here since JsonElement is a struct that must be read in full. return false; } - if (dictionaryObject != null) - { - dictionaryObject[state.Current.KeyName!] = jsonElement; - } - else - { - IDictionary dictionaryJsonElement = (IDictionary)propValue; - dictionaryJsonElement[state.Current.KeyName!] = jsonElement; - } + dictionaryJsonElement[state.Current.JsonPropertyNameAsString!] = value; } return true; @@ -237,10 +251,5 @@ public JsonClassInfo RuntimeClassInfo public bool ShouldSerialize { get; private set; } public bool ShouldDeserialize { get; private set; } - - public void ThrowCollectionNotSupportedException() - { - throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(RuntimePropertyType!, ParentClassType, 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 index 691fcfcf447cf..252f8446c3882 100644 --- 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 @@ -104,8 +104,6 @@ public override bool GetMemberAndWriteJson(object obj, ref WriteStack state, Utf if (state.Current.PropertyState < StackFramePropertyState.Name) { state.Current.PropertyState = StackFramePropertyState.Name; - - Debug.Assert(EscapedName.HasValue); writer.WritePropertyName(EscapedName.Value); } @@ -126,7 +124,7 @@ public override bool GetMemberAndWriteJsonExtensionData(object obj, ref WriteSta } else { - state.Current.PolymorphicJsonPropertyInfo = state.Current.DeclaredJsonPropertyInfo.RuntimeClassInfo.ElementClassInfo!.PolicyProperty; + state.Current.PolymorphicJsonPropertyInfo = state.Current.DeclaredJsonPropertyInfo!.RuntimeClassInfo.ElementClassInfo!.PolicyProperty; success = Converter.TryWriteDataExtensionProperty(writer, value, Options, ref state); } @@ -149,9 +147,10 @@ public override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref U } else { - // Optimize for internal converters by avoiding the extra call to TryRead. + // 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)) { 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 index 23097aece90dc..205b45f3c2aa8 100644 --- 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 @@ -16,7 +16,7 @@ public override sealed T Read(ref Utf8JsonReader reader, Type typeToConvert, Jso // Bridge from resumable to value converters. if (options == null) { - options = JsonSerializerOptions.s_defaultOptions; + throw new ArgumentNullException(nameof(options)); } ReadStack state = default; @@ -30,11 +30,11 @@ public override sealed void Write(Utf8JsonWriter writer, T value, JsonSerializer // Bridge from resumable to value converters. if (options == null) { - options = JsonSerializerOptions.s_defaultOptions; + throw new ArgumentNullException(nameof(options)); } WriteStack state = default; - state.InitializeRoot(typeof(T), options); + 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.HandleMetadata.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs index e798b4529d56e..206de8d71ac19 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 @@ -9,64 +9,73 @@ namespace System.Text.Json { public static partial class JsonSerializer { - internal static bool ResolveMetadata( - this JsonConverter converter, + /// + /// 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, - out T value) + ref ReadStack state) { if (state.Current.ObjectState < StackFrameObjectState.MetadataPropertyName) { // Read the first metadata property name. if (!reader.Read()) { - value = default!; 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); } - else - { - ThrowHelper.ThrowJsonException_MetadataIdIsNotFirstProperty(); - } + + // 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 = GetSpan(ref reader); + ReadOnlySpan propertyName = reader.GetSpan(); MetadataPropertyName metadata = GetMetadataPropertyName(propertyName); - state.Current.MetadataPropertyName = metadata; - if (metadata == MetadataPropertyName.Ref) + if (metadata == MetadataPropertyName.Id) { - if (!converter.CanHaveMetadata) + state.Current.JsonPropertyName = propertyName.ToArray(); + if (!converter.CanHaveIdMetadata) { - ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(propertyName, ref state, reader); + ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(converter.TypeToConvert); } - state.Current.ObjectState = StackFrameObjectState.MetadataRefProperty; + state.Current.ObjectState = StackFrameObjectState.MetadataIdProperty; } - else if (metadata == MetadataPropertyName.Id) + else if (metadata == MetadataPropertyName.Ref) { - if (!converter.CanHaveMetadata) + state.Current.JsonPropertyName = propertyName.ToArray(); + if (converter.TypeToConvert.IsValueType) { - if ((converter.ClassType & (ClassType.Dictionary | ClassType.Enumerable)) != 0) - { - ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(converter.TypeToConvert); - } - else - { - ThrowHelper.ThrowJsonException_MetadataInvalidReferenceToValueType(state.Current.JsonClassInfo.Type); - } + ThrowHelper.ThrowJsonException_MetadataInvalidReferenceToValueType(converter.TypeToConvert); } - state.Current.ObjectState = StackFrameObjectState.MetadataIdProperty; + state.Current.ObjectState = StackFrameObjectState.MetadataRefProperty; } else if (metadata == MetadataPropertyName.Values) { - ThrowHelper.ThrowJsonException_MetadataMissingIdBeforeValues(); + state.Current.JsonPropertyName = propertyName.ToArray(); + if (converter.ClassType == ClassType.Enumerable) + { + ThrowHelper.ThrowJsonException_MetadataMissingIdBeforeValues(); + } + else + { + ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(propertyName, ref state, reader); + } } else { @@ -75,12 +84,12 @@ internal static bool ResolveMetadata( // Having a StartObject without metadata properties is not allowed. if (converter.ClassType == ClassType.Enumerable) { - ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(converter.TypeToConvert, reader, ref state); + 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; - value = default!; return true; } } @@ -89,7 +98,6 @@ internal static bool ResolveMetadata( { if (!reader.Read()) { - value = default!; return false; } @@ -100,14 +108,14 @@ internal static bool ResolveMetadata( string key = reader.GetString()!; - state.Current.ReturnValue = state.ReferenceResolver.ResolveReferenceOnDeserialize(key!)!; + // 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) { if (!reader.Read()) { - value = default!; return false; } @@ -116,13 +124,12 @@ internal static bool ResolveMetadata( ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType); } - string id = reader.GetString()!; - state.Current.MetadataId = id; + state.Current.MetadataId = reader.GetString(); // Clear the MetadataPropertyName since we are done processing Id. - state.Current.MetadataPropertyName = MetadataPropertyName.NoMetadata; + state.Current.JsonPropertyName = default; - if (converter.ClassType == ClassType.Enumerable && converter.CanHaveValuesMetadata) + if (converter.ClassType == ClassType.Enumerable) { // Need to Read $values property name. state.Current.ObjectState = StackFrameObjectState.MetadataValuesPropertyName; @@ -130,7 +137,8 @@ internal static bool ResolveMetadata( else { // We are done reading metadata. - state.Current.ObjectState = StackFrameObjectState.MetataPropertyValue; + state.Current.ObjectState = StackFrameObjectState.MetadataPropertyValue; + return true; } } @@ -138,19 +146,17 @@ internal static bool ResolveMetadata( { if (!reader.Read()) { - value = default!; return false; } if (reader.TokenType != JsonTokenType.EndObject) { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(converter.TypeToConvert); - } + // We just read a property. The only valid next tokens are EndObject and PropertyName. + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); - // Clear the MetadataPropertyName since we are done processing Ref. - state.Current.MetadataPropertyName = MetadataPropertyName.NoMetadata; + ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(reader.GetSpan(), ref state); + } - value = (T)state.Current.ReturnValue!; return true; } @@ -158,7 +164,6 @@ internal static bool ResolveMetadata( { if (!reader.Read()) { - value = default!; return false; } @@ -167,12 +172,16 @@ internal static bool ResolveMetadata( ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(converter.TypeToConvert); } - if (reader.GetString() != "$values") + 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_MetadataPreservedArrayValuesNotFound(converter.TypeToConvert); + ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(converter.TypeToConvert, reader); } - state.Current.MetadataPropertyName = MetadataPropertyName.Values; state.Current.ObjectState = StackFrameObjectState.MetadataValuesPropertyStartArray; } @@ -180,39 +189,20 @@ internal static bool ResolveMetadata( { if (!reader.Read()) { - value = default!; return false; } if (reader.TokenType != JsonTokenType.StartArray) { - ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(converter.TypeToConvert); + ThrowHelper.ThrowJsonException_MetadataValuesInvalidToken(reader.TokenType); } - state.Current.ObjectState = StackFrameObjectState.MetataPropertyValue; + state.Current.ObjectState = StackFrameObjectState.MetadataPropertyValue; } - value = default!; return true; } - internal static string? GetMetadataPropertyName(in ReadStackFrame frame) - { - switch (frame.MetadataPropertyName) - { - case MetadataPropertyName.Id: - return "$id"; - - case MetadataPropertyName.Ref: - return "$ref"; - - case MetadataPropertyName.Values: - return "$values"; - } - - return null; - } - internal static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan propertyName) { if (propertyName.Length > 0 && propertyName[0] == '$') 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 62a65e9b14e39..7ecdf4c8396d3 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 @@ -5,41 +5,52 @@ using System.Collections; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; namespace System.Text.Json { public static partial class JsonSerializer { /// - /// Lookup the property given its name in the reader. + /// 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)] - internal static void LookupProperty( + internal static JsonPropertyInfo LookupProperty( object obj, ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state, - out JsonPropertyInfo jsonPropertyInfo, out bool useExtensionProperty) { Debug.Assert(state.Current.JsonClassInfo.ClassType == ClassType.Object); - ReadOnlySpan unescapedPropertyName = GetSpan(ref reader); - ReadOnlySpan propertyName; + JsonPropertyInfo jsonPropertyInfo; + + ReadOnlySpan unescapedPropertyName; + ReadOnlySpan propertyName = reader.GetSpan(); if (reader._stringHasEscaping) { - int idx = unescapedPropertyName.IndexOf(JsonConstants.BackSlash); + int idx = propertyName.IndexOf(JsonConstants.BackSlash); Debug.Assert(idx != -1); - propertyName = GetUnescapedString(unescapedPropertyName, idx); + unescapedPropertyName = GetUnescapedString(propertyName, idx); } else { - propertyName = unescapedPropertyName; + unescapedPropertyName = propertyName; + } + + if (options.ReferenceHandling.ShouldReadPreservedReferences()) + { + if (propertyName.Length > 0 && propertyName[0] == '$') + { + ThrowHelper.ThrowUnexpectedMetadataException(propertyName, ref reader, ref state); + } } - jsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(propertyName, ref state.Current); + 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++; @@ -47,42 +58,30 @@ internal static void LookupProperty( // Determine if we should use the extension property. if (jsonPropertyInfo == JsonPropertyInfo.s_missingProperty) { - if (options.ReferenceHandling.ShouldReadPreservedReferences()) - { - if (unescapedPropertyName.Length > 0 && unescapedPropertyName[0] == '$') - { - // Ensure JsonPath doesn't attempt to use the previous property. - state.Current.JsonPropertyInfo = null!; - - ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(unescapedPropertyName, ref state, reader); - } - } - JsonPropertyInfo? dataExtProperty = state.Current.JsonClassInfo.DataExtensionProperty; if (dataExtProperty != null) { - state.Current.JsonPropertyName = propertyName.ToArray(); - state.Current.KeyName = JsonHelpers.Utf8GetString(propertyName); + state.Current.JsonPropertyNameAsString = JsonHelpers.Utf8GetString(unescapedPropertyName); CreateDataExtensionProperty(obj, dataExtProperty); jsonPropertyInfo = dataExtProperty; } state.Current.JsonPropertyInfo = jsonPropertyInfo; useExtensionProperty = true; - return; + return jsonPropertyInfo; } // Support JsonException.Path. Debug.Assert( jsonPropertyInfo.JsonPropertyName == null || options.PropertyNameCaseInsensitive || - propertyName.SequenceEqual(jsonPropertyInfo.JsonPropertyName)); + unescapedPropertyName.SequenceEqual(jsonPropertyInfo.JsonPropertyName)); state.Current.JsonPropertyInfo = jsonPropertyInfo; if (jsonPropertyInfo.JsonPropertyName == null) { - byte[] propertyNameArray = propertyName.ToArray(); + byte[] propertyNameArray = unescapedPropertyName.ToArray(); if (options.PropertyNameCaseInsensitive) { // Each payload can have a different name here; remember the value on the temporary stack. @@ -98,9 +97,10 @@ internal static void LookupProperty( state.Current.JsonPropertyInfo = jsonPropertyInfo; useExtensionProperty = false; + return jsonPropertyInfo; } - internal static void CreateDataExtensionProperty( + private static void CreateDataExtensionProperty( object obj, JsonPropertyInfo jsonPropertyInfo) { 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 3b751c7f8f6ac..1c5113b3f8bef 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,20 +2,10 @@ // 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.Runtime.CompilerServices; - namespace System.Text.Json { public static partial class JsonSerializer { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static ReadOnlySpan GetSpan(ref Utf8JsonReader reader) - { - ReadOnlySpan propertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; - return propertyName; - } - private static object? ReadCore( Type returnType, JsonSerializerOptions options, 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 38a018a15e278..3f2234a56be38 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 @@ -218,7 +218,6 @@ 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.) - //state.SetToTop(); state.ReadAhead = !isFinalBlock; state.BytesConsumed = 0; 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 48bc3910d9712..df534fa8f4691 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 @@ -22,7 +22,12 @@ internal static T Deserialize(ref Utf8JsonReader reader, JsonSerializerOption state.Current.InitializeReEntry(typeof(T), options, propertyName); - return (T)ReadCoreReEntry(options, ref reader, ref state)!; + 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; } /// @@ -316,7 +321,7 @@ 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); 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 b1e7f6cd254d2..50deaa6ababff 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 @@ -14,7 +14,7 @@ namespace System.Text.Json /// public static partial class JsonSerializer { - internal static void ReadCore( + private static void ReadCore( JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) @@ -35,8 +35,8 @@ internal static void ReadCore( } else { - // For a continuation continue to read ahead here to avoid having to build and - // then tear down the call stack if there is more than one buffer fetch necessary. + // 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)) { state.BytesConsumed += reader.BytesConsumed; @@ -81,7 +81,7 @@ internal static void ReadCore( ref Utf8JsonReader reader, ref ReadStack state) { - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo; + 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); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs index 2235ae3fc1875..151e5325da874 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs @@ -10,9 +10,29 @@ namespace System.Text.Json public static partial class JsonSerializer { // 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); + 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, @@ -20,11 +40,12 @@ internal static MetadataPropertyName WriteReferenceForObject( ref WriteStack state, Utf8JsonWriter writer) { - MetadataPropertyName metadataToWrite = state.GetResolvedReferenceHandling(jsonConverter, currentValue, out string? referenceId); + 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) { @@ -40,7 +61,7 @@ internal static MetadataPropertyName WriteReferenceForCollection( ref WriteStack state, Utf8JsonWriter writer) { - MetadataPropertyName metadataToWrite = state.GetResolvedReferenceHandling(jsonConverter, currentValue, out string? referenceId); + MetadataPropertyName metadataToWrite = GetResolvedReferenceHandling(jsonConverter, currentValue, ref state, out string? referenceId); if (metadataToWrite == MetadataPropertyName.NoMetadata) { @@ -50,8 +71,7 @@ internal static MetadataPropertyName WriteReferenceForCollection( { writer.WriteStartObject(); writer.WriteString(s_metadataId, referenceId!); - writer.WritePropertyName(s_metadataValues); - writer.WriteStartArray(); + writer.WriteStartArray(s_metadataValues); } else { 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 eafa2cdd3ce15..31d2b2180d86c 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 @@ -101,7 +101,7 @@ private static void WriteCore(Utf8JsonWriter writer, object? value, Type type, J } WriteStack state = default; - state.InitializeRoot(type!, options); + state.InitializeRoot(type!, options, supportContinuation: false); WriteCore(writer, value, options, ref state, state.Current.JsonClassInfo!.PolicyProperty!.ConverterBase); } 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 9eea08cb99529..fb42a2b2591c2 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,10 +71,7 @@ private static async Task WriteAsyncCore(Stream utf8Json, object? value, Type in } WriteStack state = default; - state.InitializeRoot(inputType, options); - - // Ensures converters support contination due to having to re-populate the buffer from a Stream. - state.SupportContinuation = true; + state.InitializeRoot(inputType, options, supportContinuation: true); bool isFinalBlock; 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 7c9b0f9931d11..bf31e42226344 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 @@ -3,14 +3,16 @@ // 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 ReadStack so that JsonPath works correctly. + /// 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) @@ -19,26 +21,8 @@ internal static void Serialize(Utf8JsonWriter writer, T value, JsonSerializer } JsonConverter jsonConverter = state.Current.InitializeReEntry(typeof(T), options, propertyName); - Write(writer, value, options, ref state, jsonConverter); - } - - /// - /// Internal version that allows re-entry with preserving ReadStack so that JsonPath works correctly. - /// - internal static void Serialize(Utf8JsonWriter writer, object? value, Type inputType, JsonSerializerOptions options, ref WriteStack state, string? propertyName = null) - { - if (inputType == null) - { - throw new ArgumentNullException(nameof(inputType)); - } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - JsonConverter jsonConverter = state.Current.InitializeReEntry(inputType, options, propertyName); - Write(writer, value, options, ref state, jsonConverter); + bool success = jsonConverter.TryWriteAsObject(writer, value, options, ref state); + Debug.Assert(success); } /// 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 a65f38eac3f0a..a1999acc1829e 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 @@ -21,12 +21,12 @@ private static bool WriteCore( { try { - return Write(writer, value, options, ref state, jsonConverter); + return jsonConverter.TryWriteAsObject(writer, value, options, ref state); } catch (InvalidOperationException ex) when (ex.Source == ThrowHelper.ExceptionSourceValueToRethrowAsJsonException) { ThrowHelper.ReThrowWithPath(state, ex); - return default; + throw; } catch (JsonException ex) { @@ -34,15 +34,5 @@ private static bool WriteCore( throw; } } - - private static bool Write( - Utf8JsonWriter writer, - object? value, - JsonSerializerOptions options, - ref WriteStack state, - JsonConverter jsonConverter) - { - return jsonConverter.TryWriteAsObject(writer, value, options, ref state); - } } } 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 d9f7512ffcf6f..227a5b8c835c9 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 @@ -53,10 +53,11 @@ private static List GetDefaultConverters() converters.Add(new JsonConverterEnum()); converters.Add(new JsonKeyValuePairConverter()); - // Enumerable and Object converters should always be last since they can convert any IEnumerable - // or non-IEnumerable. + // IEnumerable should always be last since they can convert any IEnumerable. converters.Add(new JsonIEnumerableConverterFactory()); - converters.Add(new JsonObjectFactoryConverter()); + + // Object should always be last since it converts any type. + converters.Add(new JsonObjectConverterFactory()); Debug.Assert(NumberOfConverters == converters.Count); @@ -188,7 +189,6 @@ private static List GetDefaultConverters() return converter; } - 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 fe45cf4497aa0..9497f30bb9803 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,8 +6,6 @@ using System.Diagnostics; using System.Text.Json.Serialization; using System.Text.Encodings.Web; -using System.Text.Json.Serialization.Converters; -using System.Runtime.CompilerServices; namespace System.Text.Json { @@ -29,10 +27,6 @@ public sealed partial class JsonSerializerOptions private ReferenceHandling _referenceHandling = ReferenceHandling.Default; private JavaScriptEncoder? _encoder = null; - // Cache common converters. - private static JsonConverter s_objectConverter = null!; - private static JsonConverter s_jsonElementConverter = null!; - private int _defaultBufferSize = BufferSizeDefault; private int _maxDepth; private bool _allowTrailingCommas; @@ -324,14 +318,11 @@ internal MemberAccessor MemberAccessorStrategy { if (_memberAccessorStrategy == null) { - if (RuntimeFeature.IsDynamicCodeSupported) - { - _memberAccessorStrategy = new ReflectionEmitMemberAccessor(); - } - else - { - _memberAccessorStrategy = new ReflectionMemberAccessor(); - } +#if NETFRAMEWORK || NETCOREAPP + _memberAccessorStrategy = new ReflectionEmitMemberAccessor(); +#else + _memberAccessorStrategy = new ReflectionMemberAccessor(); +#endif } return _memberAccessorStrategy; @@ -383,25 +374,5 @@ internal void VerifyMutable() ThrowHelper.ThrowInvalidOperationException_SerializerOptionsImmutable(); } } - - internal static JsonConverter GetJsonElementConverter() - { - if (s_jsonElementConverter == null) - { - s_jsonElementConverter = new JsonConverterJsonElement(); - } - - return s_jsonElementConverter; - } - - internal static JsonConverter GetObjectConverter() - { - if (s_objectConverter == null) - { - s_objectConverter = new JsonConverterObject(); - } - - return s_objectConverter; - } } } 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 index 8dc566aab115d..15a1e24f332de 100644 --- 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 @@ -4,11 +4,11 @@ namespace System.Text.Json.Serialization { - // Used for value converters that need to re-enter the serializer since it will - // support JsonPath and other features that require per-serialization state. + // 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 override sealed ClassType ClassType => ClassType.NewValue; + internal sealed override ClassType ClassType => ClassType.NewValue; public override sealed T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -33,7 +33,7 @@ public override sealed void Write(Utf8JsonWriter writer, T value, JsonSerializer } WriteStack state = default; - state.InitializeRoot(typeof(T), options); + 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 2ed74e5a08d4d..5a30856762895 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 @@ -11,7 +11,7 @@ internal abstract class MemberAccessor { public abstract JsonClassInfo.ConstructorDelegate? CreateConstructor(Type classType); - public abstract Action CreateAddMethodDelegate(); + public abstract Action CreateAddMethodDelegate(); public abstract Func, TCollection> CreateImmutableEnumerableCreateRangeDelegate(); 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 fa94a5e8cfe34..0c8aa502a85be 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 @@ -34,10 +34,6 @@ internal struct ReadStack // A field is used instead of a property to avoid value semantics. public ReadStackFrame Current; - // Support the read-ahead feature. - public JsonReaderState InitialReaderState; - public long InitialReaderBytesConsumed; - public bool IsContinuation => _continuationCount != 0; public bool IsLastContinuation => _continuationCount == _count; @@ -106,7 +102,7 @@ public void Push() 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; + jsonClassInfo = Current.JsonPropertyInfo!.RuntimeClassInfo; } else { @@ -156,7 +152,6 @@ public void Pop(bool success) { // No need for a continuation since there is only one stack frame. _continuationCount = 1; - _count = 1; } else { @@ -215,36 +210,26 @@ public string JsonPath() return sb.ToString(); - void AppendStackFrame(StringBuilder sb, in ReadStackFrame frame) + static void AppendStackFrame(StringBuilder sb, in ReadStackFrame frame) { // Append the property name. string? propertyName = GetPropertyName(frame); AppendPropertyName(sb, propertyName); - // For metadata properties, include the name. - propertyName = JsonSerializer.GetMetadataPropertyName(in frame); - AppendPropertyName(sb, propertyName); - - if (frame.JsonClassInfo != null) + if (frame.JsonClassInfo != null && frame.IsProcessingEnumerable()) { - if (frame.IsProcessingDictionary()) + IEnumerable? enumerable = (IEnumerable?)frame.ReturnValue; + if (enumerable == null) { - // For dictionaries add the key. - AppendPropertyName(sb, frame.KeyName); + return; } - else if (frame.IsProcessingEnumerable()) + + // Once all elements are read, the exception is not within the array. + if (frame.ObjectState < StackFrameObjectState.ReadElements) { - IEnumerable enumerable = (IEnumerable)frame.ReturnValue!; - if (enumerable != null) - { - // Once all elements are read, the exception is not within the array. - if (frame.ObjectState < StackFrameObjectState.ReadElements) - { - sb.Append(@"["); - sb.Append(GetCount(enumerable)); - sb.Append(@"]"); - } - } + sb.Append(@"["); + sb.Append(GetCount(enumerable)); + sb.Append(@"]"); } } } @@ -266,7 +251,7 @@ static int GetCount(IEnumerable enumerable) return count; } - void AppendPropertyName(StringBuilder sb, string? propertyName) + static void AppendPropertyName(StringBuilder sb, string? propertyName) { if (propertyName != null) { @@ -284,7 +269,7 @@ void AppendPropertyName(StringBuilder sb, string? propertyName) } } - string? GetPropertyName(in ReadStackFrame frame) + static string? GetPropertyName(in ReadStackFrame frame) { string? propertyName = null; @@ -296,7 +281,8 @@ void AppendPropertyName(StringBuilder sb, string? propertyName) utf8PropertyName = frame.JsonPropertyInfo?.JsonPropertyName; if (utf8PropertyName == null) { - // Attempt to get the JSON property name from the property name specified in re-entry. + // 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; } } 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 13abc6e21e12d..dfdfd19376c58 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 @@ -11,23 +11,17 @@ namespace System.Text.Json internal struct ReadStackFrame { // Current property values. - public JsonPropertyInfo JsonPropertyInfo; + public JsonPropertyInfo? JsonPropertyInfo; public StackFramePropertyState PropertyState; public bool UseExtensionProperty; // 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 re-entry cases. - - // Support Dictionary keys. - public string? KeyName; + public string? JsonPropertyNameAsString; // This is used for dictionary keys and re-entry cases that specify a property name. // Validation state. public int OriginalDepth; - public int OriginalPropertyDepth; - public long OriginalPropertyBytesConsumed; public JsonTokenType OriginalTokenType; - public JsonTokenType OriginalPropertyTokenType; // Current object (POCO or IEnumerable). public object? ReturnValue; // The current return value used for re-entry. @@ -35,7 +29,6 @@ internal struct ReadStackFrame public StackFrameObjectState ObjectState; // State tracking the current object. // Preserve reference. - public MetadataPropertyName MetadataPropertyName; public string? MetadataId; // For performance, we order the properties by the first deserialize and PropertyIndex helps find the right slot quicker. @@ -52,21 +45,14 @@ public void EndProperty() JsonPropertyNameAsString = null; PropertyState = StackFramePropertyState.None; MetadataId = null; - MetadataPropertyName = MetadataPropertyName.NoMetadata; // No need to clear these since they are overwritten each time: // UseExtensionProperty - // OriginalPropertyDepth - // OriginalPropertyBytesConsumed - // OriginalPropertyTokenType } public void EndElement() { - OriginalPropertyDepth = 0; - OriginalPropertyBytesConsumed = 0; - OriginalPropertyTokenType = JsonTokenType.None; - KeyName = null; + JsonPropertyNameAsString = null; PropertyState = StackFramePropertyState.None; } 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 f41f66e9ea960..c410ef024790f 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,14 +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 -using System.Collections; +#if NETFRAMEWORK || NETCOREAPP using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; -namespace System.Text.Json +namespace System.Text.Json.Serialization { internal sealed class ReflectionEmitMemberAccessor : MemberAccessor { @@ -56,12 +55,12 @@ internal sealed class ReflectionEmitMemberAccessor : MemberAccessor return (JsonClassInfo.ConstructorDelegate)dynamicMethod.CreateDelegate(typeof(JsonClassInfo.ConstructorDelegate)); } - public override Action CreateAddMethodDelegate() + public override Action CreateAddMethodDelegate() { Type collectionType = typeof(TCollection); Type elementType = typeof(object); - // We verified this won't be null when a created the converter that calls this method. + // 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( @@ -78,7 +77,7 @@ public override Action CreateAddMethodDelegate generator.Emit(OpCodes.Callvirt, realMethod); generator.Emit(OpCodes.Ret); - return (Action)dynamicMethod.CreateDelegate(typeof(Action)); + return (Action)dynamicMethod.CreateDelegate(typeof(Action)); } public override Func, TCollection> CreateImmutableEnumerableCreateRangeDelegate() 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 3a46fc19fd285..f04927fa3c0cf 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,12 +2,11 @@ // 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.Reflection; -namespace System.Text.Json +namespace System.Text.Json.Serialization { internal sealed class ReflectionMemberAccessor : MemberAccessor { @@ -29,7 +28,7 @@ internal sealed class ReflectionMemberAccessor : MemberAccessor return () => Activator.CreateInstance(type); } - public override Action CreateAddMethodDelegate() + public override Action CreateAddMethodDelegate() { Type collectionType = typeof(TCollection); Type elementType = typeof(object); @@ -37,7 +36,7 @@ public override Action CreateAddMethodDelegate // We verified this won't be null when we created the converter for the collection type. MethodInfo addMethod = (collectionType.GetMethod("Push") ?? collectionType.GetMethod("Enqueue"))!; - return delegate (TCollection collection, object element) + return delegate (TCollection collection, object? element) { addMethod.Invoke(collection, new object[] { element! }); }; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFrameObjectState.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFrameObjectState.cs index 08a8477117f82..a1530ab5621d9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFrameObjectState.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFrameObjectState.cs @@ -4,6 +4,10 @@ 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, @@ -16,7 +20,7 @@ internal enum StackFrameObjectState : byte MetadataRefPropertyEndObject, // Read EndObject for $ref. MetadataValuesPropertyName, // Read $values property name. MetadataValuesPropertyStartArray, // Read StartArray for $values. - MetataPropertyValue, // Whether all metadata properties has been read. + MetadataPropertyValue, // Whether all metadata properties has been read. CreatedObject, ReadElements, 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 index 28ce1a3e70cb6..0e78bccef5fc1 100644 --- 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 @@ -4,14 +4,18 @@ 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, - Name, - ReadValue, - ReadValueIsEnd, - TryRead, + 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 9bd1937c46d1b..4debd5ed506a5 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 @@ -66,7 +66,7 @@ private void AddCurrent() /// /// Initializes the state for the first type being serialized. /// - public void InitializeRoot(Type type, JsonSerializerOptions options) + public void InitializeRoot(Type type, JsonSerializerOptions options, bool supportContinuation) { JsonClassInfo jsonClassInfo = options.GetOrAddClass(type); Debug.Assert(jsonClassInfo.ClassType != ClassType.Invalid); @@ -82,22 +82,8 @@ public void InitializeRoot(Type type, JsonSerializerOptions options) { ReferenceResolver = new DefaultReferenceResolver(writing: true); } - } - - public MetadataPropertyName GetResolvedReferenceHandling(JsonConverter converter, object value, out string? referenceId) - { - if (!converter.CanHaveMetadata) - { - referenceId = default; - return MetadataPropertyName.NoMetadata; - } - - if (ReferenceResolver.TryGetOrAddReferenceOnSerialize(value, out referenceId)) - { - return MetadataPropertyName.Ref; - } - return MetadataPropertyName.Id; + SupportContinuation = supportContinuation; } public void Push() 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 fe826d0234b80..01292e9815cfd 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 @@ -23,10 +23,10 @@ internal struct WriteStackFrame /// 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; + public JsonPropertyInfo? DeclaredJsonPropertyInfo; /// - /// Used when processing dictionaries. + /// Used when processing extension data dictionaries. /// public bool IgnoreDictionaryKeyPolicy; @@ -35,16 +35,10 @@ internal struct WriteStackFrame /// public JsonClassInfo JsonClassInfo; - /// - /// The key name for a dictionary value. - /// - public string KeyName; - /// /// Validation state for a class. /// public int OriginalDepth; - public int OriginalPropertyDepth; // Class-level state for collections. public bool ProcessedStartToken; @@ -75,9 +69,8 @@ internal struct WriteStackFrame /// public JsonPropertyInfo? PolymorphicJsonPropertyInfo; - public void EndElement() + public void EndDictionaryElement() { - OriginalPropertyDepth = 0; PropertyState = StackFramePropertyState.None; } @@ -85,8 +78,6 @@ public void EndProperty() { DeclaredJsonPropertyInfo = null!; JsonPropertyNameAsString = null; - KeyName = null!; - OriginalPropertyDepth = 0; PolymorphicJsonPropertyInfo = null; PropertyState = StackFramePropertyState.None; } @@ -95,10 +86,9 @@ public void EndProperty() /// Return the property that contains the correct polymorphic properties including /// the ClassType and ConverterBase. /// - /// public JsonPropertyInfo GetPolymorphicJsonPropertyInfo() { - return PolymorphicJsonPropertyInfo != null ? PolymorphicJsonPropertyInfo : DeclaredJsonPropertyInfo; + return PolymorphicJsonPropertyInfo ?? DeclaredJsonPropertyInfo!; } /// @@ -107,10 +97,6 @@ public JsonPropertyInfo GetPolymorphicJsonPropertyInfo() public JsonConverter InitializeReEntry(Type type, JsonSerializerOptions options, string? propertyName = null) { JsonClassInfo newClassInfo = options.GetOrAddClass(type); - if (newClassInfo.ClassType == ClassType.Invalid) - { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupportedCollection(type); - } // todo: check if type==newtype and skip below? 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 bc2035af6f6e4..475f5138a80df 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,23 +21,25 @@ 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)); + return new NotSupportedException(SR.Format(SR.SerializationNotSupportedType, propertyType)); } [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static NotSupportedException ThrowNotSupportedException_SerializationNotSupportedCollection(Type propertyType, Type? parentType = null, MemberInfo? memberInfo = null) + public static NotSupportedException ThrowNotSupportedException_SerializationNotSupported(Type propertyType, Type? parentType = null, MemberInfo? memberInfo = null) { - throw GetNotSupportedException_SerializationNotSupportedCollection(propertyType, parentType, memberInfo); + throw GetNotSupportedException_SerializationNotSupported(propertyType, parentType, memberInfo); } + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowInvalidOperationException_SerializerCycleDetected(int maxDepth) { throw new JsonException(SR.Format(SR.SerializerCycleDetected, maxDepth)); @@ -52,21 +54,6 @@ public static void ThrowJsonException_DeserializeUnableToConvertValue(Type prope throw ex; } - [DoesNotReturn] - [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType, string path, Exception? innerException = null) - { - string message = SR.Format(SR.DeserializeUnableToConvertValue, propertyType) + $" Path: {path}."; - throw new JsonException(message, path, null, null, innerException); - } - - [DoesNotReturn] - [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowJsonException_DepthTooLarge(int currentDepth, JsonSerializerOptions options) - { - throw new JsonException(SR.Format(SR.DepthTooLarge, currentDepth, options.EffectiveMaxDepth)); - } - [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowJsonException_SerializationConverterRead(JsonConverter? converter) @@ -323,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() @@ -365,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 { @@ -377,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)); } @@ -391,7 +367,7 @@ public static void ThrowJsonException_MetadataInvalidReferenceToValueType(Type p [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowJsonException_MetadataPreservedArrayInvalidProperty(Type propertyType, in Utf8JsonReader reader, ref ReadStack state) + public static void ThrowJsonException_MetadataPreservedArrayInvalidProperty(Type propertyType, in Utf8JsonReader reader) { string propertyName = reader.GetString()!; @@ -415,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 ccbe540f043f9..45b4d2c5ecbac 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Array.ReadTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Array.ReadTests.cs @@ -654,13 +654,13 @@ public static void DoNotDependOnPropertyGetterWhenDeserializingCollections() class Dealer { - private string _Networks; + private string _networks; [JsonIgnore] public string Networks { - get => _Networks; - set => _Networks = value ?? string.Empty; + get => _networks; + set => _networks = value ?? string.Empty; } public IEnumerable NetworkCodeList 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 89fc783615d93..d7b8bf8ed491e 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.BadConverters.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.BadConverters.cs @@ -136,7 +136,8 @@ 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) @@ -157,6 +158,10 @@ public static void ConverterThatReturnsNullFail() ex = Assert.Throws(() => JsonSerializer.Deserialize("0", options)); Assert.Contains(typeof(int).ToString(), ex.Message); + + // 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 diff --git a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs index bec95b265fe31..5e061ce37610e 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs @@ -2033,7 +2033,7 @@ public static void DictionaryOfTOnlyWithStringPoco() Assert.Equal(Json, json); } - public class DictionaryThatHasIncomatibleEnumerator : IDictionary + public class DictionaryThatHasIncompatibleEnumerator : IDictionary { Hashtable _inner = new Hashtable(); @@ -2117,8 +2117,8 @@ public static void VerifyDictionaryThatHasIncomatibleEnumeratorWithInt() { const string Json = @"{""One"":1,""Two"":2}"; - DictionaryThatHasIncomatibleEnumerator dictionary; - dictionary = JsonSerializer.Deserialize(Json); + 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)); @@ -2129,13 +2129,22 @@ public static void VerifyDictionaryThatHasIncomatibleEnumeratorWithPoco() { const string Json = @"{""One"":{""Id"":1},""Two"":{""Id"":2}}"; - DictionaryThatHasIncomatibleEnumerator dictionary; - dictionary = JsonSerializer.Deserialize(Json); + 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 f876a34ffdb37..d374f7c3eff20 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 6d070a641000a..b23ef614c1307 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; @@ -688,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/ReferenceHandlingTests.Deserialize.cs b/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs index 3b81aaa2e6717..a11af8356c842 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() { @@ -891,7 +907,7 @@ public static void ThrowOnStructWithReference() ]"; JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, s_deserializerOptionsPreserve)); - Assert.Equal("$[0].$id", ex.Path); + Assert.Equal("$[1].$ref", ex.Path); Assert.Contains($"'{typeof(EmployeeStruct)}'", ex.Message); } #endregion @@ -1122,7 +1138,7 @@ public static void ReferenceObjectsShouldNotContainMoreProperties() }"; ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); - Assert.Equal("$.Manager.$ref", ex.Path); + Assert.Equal("$.Manager.Name", ex.Path); //Metadata property before $ref json = @"{ @@ -1148,7 +1164,7 @@ public static void ReferenceObjectsShouldNotContainMoreProperties() }"; ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); - Assert.Equal("$.Manager.$ref", ex.Path); + Assert.Equal("$.Manager.$id", ex.Path); } [Fact] @@ -1168,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) @@ -1289,7 +1324,7 @@ public static void PreservedArrayValuesContainsNull() JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, s_deserializerOptionsPreserve)); - Assert.Equal("$.$values", ex.Path); + Assert.Equal("$.$values", ex.Path); } [Fact] @@ -1329,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 = @"{ @@ -1340,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.WriteTests.cs b/src/libraries/System.Text.Json/tests/Serialization/Stream.WriteTests.cs index e19db6995b739..409b89740e61f 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 index bb9820394828c..ed649d36a0e61 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.ConcurrentCollections.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.ConcurrentCollections.cs @@ -18,12 +18,14 @@ public static void Read_ConcurrentCollection() ConcurrentQueue qc = JsonSerializer.Deserialize>(@"[""1""]"); Assert.Equal(1, qc.Count); - qc.TryPeek(out string val); + bool found = qc.TryPeek(out string val); + Assert.True(found); Assert.Equal("1", val); ConcurrentStack qs = JsonSerializer.Deserialize>(@"[""1""]"); Assert.Equal(1, qs.Count); - qs.TryPeek(out val); + found = qs.TryPeek(out val); + Assert.True(found); Assert.Equal("1", val); } 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 index dc18020d44b7c..57c6902471071 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.SpecializedCollections.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.SpecializedCollections.cs @@ -32,15 +32,15 @@ public static void Read_SpecializedCollection() [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""]")); + // 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""}]")); + // 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""}")); + // 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""}"));