Support resumable serialization in NullableConverter<T> (#65524)
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Fri, 18 Feb 2022 10:44:23 +0000 (10:44 +0000)
committerGitHub <noreply@github.com>
Fri, 18 Feb 2022 10:44:23 +0000 (10:44 +0000)
* Support resumable serialization in NullableConverter<T>

* use null instead of default

13 files changed:
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonCollectionConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoInternalOfT.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs
src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.AsyncEnumerable.cs

index 0ca9ddb..6d3a805 100644 (file)
@@ -57,7 +57,7 @@ namespace System.Text.Json.Serialization
 
         protected static JsonConverter<TElement> GetElementConverter(ref WriteStack state)
         {
-            JsonConverter<TElement> converter = (JsonConverter<TElement>)state.Current.DeclaredJsonPropertyInfo!.ConverterBase;
+            JsonConverter<TElement> converter = (JsonConverter<TElement>)state.Current.JsonPropertyInfo!.ConverterBase;
             Debug.Assert(converter != null); // It should not be possible to have a null converter at this point.
 
             return converter;
@@ -282,7 +282,7 @@ namespace System.Text.Json.Serialization
                         writer.WriteStartArray();
                     }
 
-                    state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
+                    state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
                 }
 
                 success = OnWriteResume(writer, value, options, ref state);
index cff2ce4..b086d95 100644 (file)
@@ -317,7 +317,7 @@ namespace System.Text.Json.Serialization
                     }
                 }
 
-                state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
+                state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
             }
 
             bool success = OnWriteResume(writer, dictionary, options, ref state);
index a02dd44..e8d78a5 100644 (file)
@@ -66,7 +66,7 @@ namespace System.Text.Json.Serialization.Converters
             }
 
             TElement element = _optionValueGetter(value);
-            state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
+            state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
             return _elementConverter.TryWrite(writer, element, options, ref state);
         }
 
index 35d1640..b11c346 100644 (file)
@@ -67,7 +67,7 @@ namespace System.Text.Json.Serialization.Converters
 
             TElement element = _optionValueGetter(ref value);
 
-            state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
+            state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
             return _elementConverter.TryWrite(writer, element, options, ref state);
         }
 
index 3487713..9e543b4 100644 (file)
@@ -279,7 +279,7 @@ namespace System.Text.Json.Serialization.Converters
                     if (jsonPropertyInfo.ShouldSerialize)
                     {
                         // Remember the current property for JsonPath support if an exception is thrown.
-                        state.Current.DeclaredJsonPropertyInfo = jsonPropertyInfo;
+                        state.Current.JsonPropertyInfo = jsonPropertyInfo;
                         state.Current.NumberHandling = jsonPropertyInfo.NumberHandling;
 
                         bool success = jsonPropertyInfo.GetMemberAndWriteJson(obj, ref state, writer);
@@ -295,7 +295,7 @@ namespace System.Text.Json.Serialization.Converters
                 if (dataExtensionProperty?.ShouldSerialize == true)
                 {
                     // Remember the current property for JsonPath support if an exception is thrown.
-                    state.Current.DeclaredJsonPropertyInfo = dataExtensionProperty;
+                    state.Current.JsonPropertyInfo = dataExtensionProperty;
                     state.Current.NumberHandling = dataExtensionProperty.NumberHandling;
 
                     bool success = dataExtensionProperty.GetMemberAndWriteJsonExtensionData(obj, ref state, writer);
@@ -334,7 +334,7 @@ namespace System.Text.Json.Serialization.Converters
                     Debug.Assert(jsonPropertyInfo != null);
                     if (jsonPropertyInfo.ShouldSerialize)
                     {
-                        state.Current.DeclaredJsonPropertyInfo = jsonPropertyInfo;
+                        state.Current.JsonPropertyInfo = jsonPropertyInfo;
                         state.Current.NumberHandling = jsonPropertyInfo.NumberHandling;
 
                         if (!jsonPropertyInfo.GetMemberAndWriteJson(obj!, ref state, writer))
@@ -366,7 +366,7 @@ namespace System.Text.Json.Serialization.Converters
                     if (dataExtensionProperty?.ShouldSerialize == true)
                     {
                         // Remember the current property for JsonPath support if an exception is thrown.
-                        state.Current.DeclaredJsonPropertyInfo = dataExtensionProperty;
+                        state.Current.JsonPropertyInfo = dataExtensionProperty;
                         state.Current.NumberHandling = dataExtensionProperty.NumberHandling;
 
                         if (!dataExtensionProperty.GetMemberAndWriteJsonExtensionData(obj, ref state, writer))
index ba69c3e..4f671c5 100644 (file)
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Diagnostics;
-
 namespace System.Text.Json.Serialization.Converters
 {
     internal sealed class NullableConverter<T> : JsonConverter<T?> where T : struct
     {
+        internal override ConverterStrategy ConverterStrategy { get; }
+        internal override Type? ElementType => typeof(T);
+        public override bool HandleNull => true;
+
         // It is possible to cache the underlying converter since this is an internal converter and
         // an instance is created only once for each JsonSerializerOptions instance.
-        private readonly JsonConverter<T> _converter;
+        private readonly JsonConverter<T> _elementConverter;
+
+        public NullableConverter(JsonConverter<T> elementConverter)
+        {
+            _elementConverter = elementConverter;
+            ConverterStrategy = elementConverter.ConverterStrategy;
+            IsInternalConverterForNumberType = elementConverter.IsInternalConverterForNumberType;
+            // temporary workaround for JsonConverter base constructor needing to access
+            // ConverterStrategy when calculating `CanUseDirectReadOrWrite`.
+            CanUseDirectReadOrWrite = elementConverter.ConverterStrategy == ConverterStrategy.Value;
+        }
+
+        internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T? value)
+        {
+            if (!state.IsContinuation && reader.TokenType == JsonTokenType.Null)
+            {
+                value = null;
+                return true;
+            }
 
-        public NullableConverter(JsonConverter<T> converter)
+            state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
+            if (_elementConverter.TryRead(ref reader, typeof(T), options, ref state, out T element))
+            {
+                value = element;
+                return true;
+            }
+
+            value = null;
+            return false;
+        }
+
+        internal override bool OnTryWrite(Utf8JsonWriter writer, T? value, JsonSerializerOptions options, ref WriteStack state)
         {
-            _converter = converter;
-            IsInternalConverterForNumberType = converter.IsInternalConverterForNumberType;
+            if (value is null)
+            {
+                writer.WriteNullValue();
+                return true;
+            }
+
+            state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
+            return _elementConverter.TryWrite(writer, value.Value, options, ref state);
         }
 
         public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
         {
-            // We do not check _converter.HandleNull, as the underlying struct cannot be null.
-            // A custom converter for some type T? can handle null.
             if (reader.TokenType == JsonTokenType.Null)
             {
                 return null;
             }
 
-            T value = _converter.Read(ref reader, typeof(T), options);
+            T value = _elementConverter.Read(ref reader, typeof(T), options);
             return value;
         }
 
         public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
         {
-            if (!value.HasValue)
+            if (value is null)
             {
-                // We do not check _converter.HandleNull, as the underlying struct cannot be null.
-                // A custom converter for some type T? can handle null.
                 writer.WriteNullValue();
             }
             else
             {
-                _converter.Write(writer, value.Value, options);
+                _elementConverter.Write(writer, value.Value, options);
             }
         }
 
         internal override T? ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling numberHandling, JsonSerializerOptions options)
         {
-            // We do not check _converter.HandleNull, as the underlying struct cannot be null.
-            // A custom converter for some type T? can handle null.
             if (reader.TokenType == JsonTokenType.Null)
             {
                 return null;
             }
 
-            T value = _converter.ReadNumberWithCustomHandling(ref reader, numberHandling, options);
+            T value = _elementConverter.ReadNumberWithCustomHandling(ref reader, numberHandling, options);
             return value;
         }
 
         internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, T? value, JsonNumberHandling handling)
         {
-            if (!value.HasValue)
+            if (value is null)
             {
-                // We do not check _converter.HandleNull, as the underlying struct cannot be null.
-                // A custom converter for some type T? can handle null.
                 writer.WriteNullValue();
             }
             else
             {
-                _converter.WriteNumberWithCustomHandling(writer, value.Value, handling);
+                _elementConverter.WriteNumberWithCustomHandling(writer, value.Value, handling);
             }
         }
     }
index 3deefaf..7b47f43 100644 (file)
@@ -227,7 +227,7 @@ namespace System.Text.Json.Serialization
             JsonTypeInfo originalJsonTypeInfo = state.Current.JsonTypeInfo;
 #endif
             state.Push();
-            Debug.Assert(TypeToConvert.IsAssignableFrom(state.Current.JsonTypeInfo.Type));
+            Debug.Assert(TypeToConvert == state.Current.JsonTypeInfo.Type);
 
 #if !DEBUG
             // For performance, only perform validation on internal converters on debug builds.
@@ -462,7 +462,7 @@ namespace System.Text.Json.Serialization
             JsonTypeInfo originalJsonTypeInfo = state.Current.JsonTypeInfo;
 #endif
             state.Push();
-            Debug.Assert(TypeToConvert.IsAssignableFrom(state.Current.JsonTypeInfo.Type));
+            Debug.Assert(TypeToConvert == state.Current.JsonTypeInfo.Type);
 
             if (!isContinuation)
             {
@@ -528,7 +528,7 @@ namespace System.Text.Json.Serialization
 
             // Extension data properties change how dictionary key naming policies are applied.
             state.Current.IsWritingExtensionDataProperty = true;
-            state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
+            state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
 
             success = dictionaryConverter.OnWriteResume(writer, value, options, ref state);
             if (success)
index 927eb05..cc044f3 100644 (file)
@@ -80,7 +80,7 @@ namespace System.Text.Json.Serialization.Metadata
         /// <remarks>This API is for use by the output of the System.Text.Json source generator and should not be called directly.</remarks>
         public static JsonTypeInfo<T> CreateValueInfo<T>(JsonSerializerOptions options, JsonConverter converter)
         {
-            JsonTypeInfo<T> info = new JsonTypeInfoInternal<T>(options);
+            JsonTypeInfo<T> info = new JsonTypeInfoInternal<T>(converter, options);
             info.PropertyInfoForTypeInfo = CreateJsonPropertyInfoForClassInfo(typeof(T), info, converter, options);
             converter.ConfigureJsonTypeInfo(info, options);
             return info;
index 5aceedb..bbb899e 100644 (file)
@@ -14,9 +14,10 @@ namespace System.Text.Json.Serialization.Metadata
         /// <summary>
         /// Creates serialization metadata for a type using a simple converter.
         /// </summary>
-        public JsonTypeInfoInternal(JsonSerializerOptions options)
+        public JsonTypeInfoInternal(JsonConverter converter, JsonSerializerOptions options)
             : base(typeof(T), options)
         {
+            ElementType = converter.ElementType;
         }
 
         /// <summary>
@@ -45,6 +46,7 @@ namespace System.Text.Json.Serialization.Metadata
 #pragma warning restore CS8714
 
             PropInitFunc = objectInfo.PropertyMetadataInitializer;
+            ElementType = converter.ElementType;
             SerializeHandler = objectInfo.SerializeHandler;
             PropertyInfoForTypeInfo = JsonMetadataServices.CreateJsonPropertyInfoForClassInfo(typeof(T), this, converter, Options);
             NumberHandling = objectInfo.NumberHandling;
index a69a280..f966a1a 100644 (file)
@@ -106,8 +106,8 @@ namespace System.Text.Json
         internal JsonConverter Initialize(JsonTypeInfo jsonTypeInfo, bool supportContinuation)
         {
             Current.JsonTypeInfo = jsonTypeInfo;
-            Current.DeclaredJsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo;
-            Current.NumberHandling = Current.DeclaredJsonPropertyInfo.NumberHandling;
+            Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo;
+            Current.NumberHandling = Current.JsonPropertyInfo.NumberHandling;
 
             JsonSerializerOptions options = jsonTypeInfo.Options;
             if (options.ReferenceHandlingStrategy != ReferenceHandlingStrategy.None)
@@ -141,9 +141,9 @@ namespace System.Text.Json
                     _count++;
 
                     Current.JsonTypeInfo = jsonTypeInfo;
-                    Current.DeclaredJsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo;
+                    Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo;
                     // Allow number handling on property to win over handling on type.
-                    Current.NumberHandling = numberHandling ?? Current.DeclaredJsonPropertyInfo.NumberHandling;
+                    Current.NumberHandling = numberHandling ?? Current.JsonPropertyInfo.NumberHandling;
                 }
             }
             else
@@ -347,7 +347,7 @@ namespace System.Text.Json
             static void AppendStackFrame(StringBuilder sb, ref WriteStackFrame frame)
             {
                 // Append the property name.
-                string? propertyName = frame.DeclaredJsonPropertyInfo?.ClrName;
+                string? propertyName = frame.JsonPropertyInfo?.ClrName;
                 if (propertyName == null)
                 {
                     // Attempt to get the JSON property name from the property name specified in re-entry.
index e42dd99..de718fc 100644 (file)
@@ -34,7 +34,7 @@ namespace System.Text.Json
         /// For objects, it is either the actual (real) JsonPropertyInfo or the <see cref="JsonTypeInfo.PropertyInfoForTypeInfo"/> for the class.
         /// For collections, it is the <see cref="JsonTypeInfo.PropertyInfoForTypeInfo"/> for the class and current element.
         /// </remarks>
-        public JsonPropertyInfo? DeclaredJsonPropertyInfo;
+        public JsonPropertyInfo? JsonPropertyInfo;
 
         /// <summary>
         /// Used when processing extension data dictionaries.
@@ -90,7 +90,7 @@ namespace System.Text.Json
 
         public void EndProperty()
         {
-            DeclaredJsonPropertyInfo = null!;
+            JsonPropertyInfo = null!;
             JsonPropertyNameAsString = null;
             PolymorphicJsonPropertyInfo = null;
             PropertyState = StackFramePropertyState.None;
@@ -102,7 +102,7 @@ namespace System.Text.Json
         /// </summary>
         public JsonPropertyInfo GetPolymorphicJsonPropertyInfo()
         {
-            return PolymorphicJsonPropertyInfo ?? DeclaredJsonPropertyInfo!;
+            return PolymorphicJsonPropertyInfo ?? JsonPropertyInfo!;
         }
 
         /// <summary>
index a5d476b..96e002f 100644 (file)
@@ -408,7 +408,7 @@ namespace System.Text.Json
             Debug.Assert(!message.Contains(" Path: "));
 
             // Obtain the type to show in the message.
-            Type? propertyType = state.Current.DeclaredJsonPropertyInfo?.PropertyType;
+            Type? propertyType = state.Current.JsonPropertyInfo?.PropertyType;
             if (propertyType == null)
             {
                 propertyType = state.Current.JsonTypeInfo.Type;
index ac2999d..53a5bbf 100644 (file)
@@ -74,6 +74,29 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Equal(1, asyncEnumerable.TotalDisposedEnumerators);
         }
 
+        [Theory]
+        [MemberData(nameof(GetAsyncEnumerableSources))]
+        public async Task WriteNestedAsyncEnumerable_Nullable<TElement>(IEnumerable<TElement> source, int delayInterval, int bufferSize)
+        {
+            // Primarily tests the ability of NullableConverter to flow async serialization state
+
+            JsonSerializerOptions options = new JsonSerializerOptions
+            {
+                DefaultBufferSize = bufferSize,
+                IncludeFields = true,
+            };
+
+            string expectedJson = await JsonSerializerWrapperForString.SerializeWrapper<(IEnumerable<TElement>, bool)?>((source, false), options);
+
+            using var stream = new Utf8MemoryStream();
+            var asyncEnumerable = new MockedAsyncEnumerable<TElement>(source, delayInterval);
+            await JsonSerializerWrapperForStream.SerializeWrapper<(IAsyncEnumerable<TElement>, bool)?>(stream, (asyncEnumerable, false), options);
+
+            JsonTestHelper.AssertJsonEqual(expectedJson, stream.ToString());
+            Assert.Equal(1, asyncEnumerable.TotalCreatedEnumerators);
+            Assert.Equal(1, asyncEnumerable.TotalDisposedEnumerators);
+        }
+
         [Theory, OuterLoop]
         [InlineData(5000, 1000, true)]
         [InlineData(5000, 1000, false)]