Additional custom converter support for System.Object (dotnet/corefx#38741)
authorSteve Harter <steveharter@users.noreply.github.com>
Fri, 21 Jun 2019 16:38:20 +0000 (09:38 -0700)
committerGitHub <noreply@github.com>
Fri, 21 Jun 2019 16:38:20 +0000 (09:38 -0700)
Commit migrated from https://github.com/dotnet/corefx/commit/f3ee3e12107597b27e3be31b81732e266a1c60c5

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleNull.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleObject.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs
src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.BadConverters.cs
src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.Object.cs
src/libraries/System.Text.Json/tests/Serialization/Object.ReadTests.cs
src/libraries/System.Text.Json/tests/Serialization/PolymorphicTests.cs

index 5b10e22..5e59965 100644 (file)
@@ -241,10 +241,11 @@ namespace System.Text.Json
 
         public abstract Type GetConcreteType(Type type);
 
-        private static void GetOriginalValues(ref Utf8JsonReader reader, out JsonTokenType tokenType, out int depth)
+        private static void GetOriginalValues(ref Utf8JsonReader reader, out JsonTokenType tokenType, out int depth, out long bytesConsumed)
         {
             tokenType = reader.TokenType;
             depth = reader.CurrentDepth;
+            bytesConsumed = reader.BytesConsumed;
         }
 
         public virtual void GetPolicies()
@@ -335,13 +336,13 @@ namespace System.Text.Json
             {
                 // Forward the setter to the value-based JsonPropertyInfo.
                 JsonPropertyInfo propertyInfo = ElementClassInfo.GetPolicyProperty();
-                propertyInfo.OnReadEnumerable(tokenType, ref state, ref reader);
+                propertyInfo.ReadEnumerable(tokenType, ref state, ref reader);
             }
             else
             {
-                GetOriginalValues(ref reader, out JsonTokenType originalTokenType, out int originalDepth);
+                GetOriginalValues(ref reader, out JsonTokenType originalTokenType, out int originalDepth, out long bytesConsumed);
                 OnRead(tokenType, ref state, ref reader);
-                VerifyRead(originalTokenType, originalDepth, ref state, ref reader);
+                VerifyRead(originalTokenType, originalDepth, bytesConsumed, ref state, ref reader);
             }
         }
 
@@ -349,9 +350,9 @@ namespace System.Text.Json
         {
             Debug.Assert(ShouldDeserialize);
 
-            GetOriginalValues(ref reader, out JsonTokenType originalTokenType, out int originalDepth);
+            GetOriginalValues(ref reader, out JsonTokenType originalTokenType, out int originalDepth, out long bytesConsumed);
             OnReadEnumerable(tokenType, ref state, ref reader);
-            VerifyRead(originalTokenType, originalDepth, ref state, ref reader);
+            VerifyRead(originalTokenType, originalDepth, bytesConsumed, ref state, ref reader);
         }
 
         public JsonClassInfo RuntimeClassInfo
@@ -374,9 +375,8 @@ namespace System.Text.Json
         public bool ShouldSerialize { get; private set; }
         public bool ShouldDeserialize { get; private set; }
 
-        private void VerifyRead(JsonTokenType originalTokenType, int originalDepth, ref ReadStack state, ref Utf8JsonReader reader)
+        private void VerifyRead(JsonTokenType originalTokenType, int originalDepth, long bytesConsumed, ref ReadStack state, ref Utf8JsonReader reader)
         {
-            // We don't have a single call to ThrowHelper since the line number captured during throw may be useful for diagnostics.
             switch (originalTokenType)
             {
                 case JsonTokenType.StartArray:
@@ -397,9 +397,8 @@ namespace System.Text.Json
 
                 default:
                     // Reading a single property value.
-                    if (reader.TokenType != originalTokenType)
+                    if (reader.TokenType != originalTokenType || reader.BytesConsumed != bytesConsumed)
                     {
-                        // todo issue #38550 blocking this: originalDepth != reader.CurrentDepth + 1
                         ThrowHelper.ThrowJsonException_SerializationConverterRead(reader, state.JsonPath, ConverterBase.ToString());
                     }
 
@@ -424,7 +423,7 @@ namespace System.Text.Json
             {
                 // Forward the setter to the value-based JsonPropertyInfo.
                 JsonPropertyInfo propertyInfo = ElementClassInfo.GetPolicyProperty();
-                propertyInfo.OnWriteEnumerable(ref state.Current, writer);
+                propertyInfo.WriteEnumerable(ref state, writer);
             }
             else
             {
index 02e5042..8e03a67 100644 (file)
@@ -44,18 +44,18 @@ namespace System.Text.Json
 
             Debug.Assert(state.Current.IsProcessingEnumerableOrDictionary);
 
-            if (state.Current.PropertyInitialized)
+            if (state.Current.CollectionPropertyInitialized)
             {
                 // A nested json array so push a new stack frame.
                 Type elementType = jsonPropertyInfo.ElementClassInfo.Type;
 
                 state.Push();
                 state.Current.Initialize(elementType, options);
-                state.Current.PropertyInitialized = true;
+                state.Current.CollectionPropertyInitialized = true;
             }
             else
             {
-                state.Current.PropertyInitialized = true;
+                state.Current.CollectionPropertyInitialized = true;
             }
 
             jsonPropertyInfo = state.Current.JsonPropertyInfo;
index 940ac0f..3963c79 100644 (file)
@@ -23,12 +23,12 @@ namespace System.Text.Json
             Debug.Assert(jsonPropertyInfo != null);
 
             // A nested object or dictionary so push new frame.
-            if (state.Current.PropertyInitialized)
+            if (state.Current.CollectionPropertyInitialized)
             {
                 state.Push();
                 state.Current.JsonClassInfo = jsonPropertyInfo.ElementClassInfo;
                 state.Current.InitializeJsonPropertyInfo();
-                state.Current.PropertyInitialized = true;
+                state.Current.CollectionPropertyInitialized = true;
 
                 ClassType classType = state.Current.JsonClassInfo.ClassType;
                 if (classType == ClassType.Value &&
@@ -56,7 +56,7 @@ namespace System.Text.Json
                 return;
             }
 
-            state.Current.PropertyInitialized = true;
+            state.Current.CollectionPropertyInitialized = true;
 
             if (state.Current.IsProcessingIDictionaryConstructible)
             {
index 324166d..70258d9 100644 (file)
@@ -37,7 +37,7 @@ namespace System.Text.Json
 
             if (state.Current.IsEnumerableProperty || state.Current.IsDictionaryProperty || state.Current.IsIDictionaryConstructibleProperty)
             {
-                bool setPropertyToNull = !state.Current.PropertyInitialized;
+                bool setPropertyToNull = !state.Current.CollectionPropertyInitialized;
                 ApplyObjectToEnumerable(null, ref state, ref reader, setPropertyDirectly: setPropertyToNull);
                 return false;
             }
index 0eb6f5f..6de0ef8 100644 (file)
@@ -46,12 +46,6 @@ namespace System.Text.Json
             {
                 state.Current.TempDictionaryValues = (IDictionary)classInfo.CreateObject();
             }
-            else if (state.Current.JsonClassInfo.ClassType == ClassType.Value)
-            {
-                // Custom converter.
-                state.Current.JsonPropertyInfo.Read(JsonTokenType.StartObject, ref state, ref reader);
-                HandleEndObject(options, ref reader, ref state);
-            }
             else
             {
                 state.Current.ReturnValue = classInfo.CreateObject();
index a20a572..197724d 100644 (file)
@@ -29,7 +29,7 @@ namespace System.Text.Json
         public IList TempEnumerableValues;
 
         // Has an array or dictionary property been initialized.
-        public bool PropertyInitialized;
+        public bool CollectionPropertyInitialized;
 
         // Support IDictionary constructible types, i.e. types that we
         // support by passing and IDictionary to their constructors:
@@ -43,6 +43,9 @@ namespace System.Text.Json
         // The current JSON data for a property does not match a given POCO, so ignore the property (recursively).
         public bool Drain;
 
+        public bool IsCollectionForClass => IsEnumerable || IsDictionary || IsIDictionaryConstructible;
+        public bool IsCollectionForProperty => IsEnumerableProperty || IsDictionaryProperty || IsIDictionaryConstructibleProperty;
+
         public bool IsIDictionaryConstructible => JsonClassInfo.ClassType == ClassType.IDictionaryConstructible;
         public bool IsDictionary => JsonClassInfo.ClassType == ClassType.Dictionary;
 
@@ -69,35 +72,40 @@ namespace System.Text.Json
             [MethodImpl(MethodImplOptions.AggressiveInlining)]
             get
             {
-                if (JsonPropertyInfo == null || SkipProperty)
+                if (SkipProperty)
                 {
                     return false;
                 }
 
-                if (PropertyInitialized)
+                if (CollectionPropertyInitialized)
                 {
-                    if ((IsEnumerable || IsDictionary || IsIDictionaryConstructible) &&
-                        JsonClassInfo.ElementClassInfo.ClassType == ClassType.Unknown)
+                    ClassType elementType;
+
+                    if (IsCollectionForProperty)
                     {
-                        return true;
+                        elementType = JsonPropertyInfo.ElementClassInfo.ClassType;
                     }
-                    else if ((IsEnumerableProperty || IsDictionaryProperty || IsIDictionaryConstructibleProperty) &&
-                        JsonPropertyInfo.ElementClassInfo.ClassType == ClassType.Unknown)
+                    else
                     {
-                        return true;
+                        Debug.Assert(IsCollectionForClass);
+                        elementType = JsonClassInfo.ElementClassInfo.ClassType;
                     }
+
+                    return (elementType == ClassType.Value || elementType == ClassType.Unknown);
+                }
+
+                ClassType type;
+                if (JsonPropertyInfo == null)
+                {
+                    type = JsonClassInfo.ClassType;
+                }
+                else
+                {
+                    type = JsonPropertyInfo.ClassType;
                 }
 
-                // We've got a property info. If we're a Value or polymorphic Value
-                // (ClassType.Unknown), return true.
-                ClassType type = JsonPropertyInfo.ClassType;
-                return type == ClassType.Value || type == ClassType.Unknown ||
-                    KeyName != null  && (
-                    (IsDictionary && JsonClassInfo.ElementClassInfo.ClassType == ClassType.Unknown) ||
-                    (IsDictionaryProperty && JsonPropertyInfo.ElementClassInfo.ClassType == ClassType.Unknown) ||
-                    (IsIDictionaryConstructible && JsonClassInfo.ElementClassInfo.ClassType == ClassType.Unknown) ||
-                    (IsIDictionaryConstructibleProperty && JsonPropertyInfo.ElementClassInfo.ClassType == ClassType.Unknown)
-                    );
+                // If we're a Value or polymorphic Value (ClassType.Unknown), return true.
+                return type == ClassType.Value || type == ClassType.Unknown;
             }
         }
 
@@ -130,7 +138,7 @@ namespace System.Text.Json
 
         public void ResetProperty()
         {
-            PropertyInitialized = false;
+            CollectionPropertyInitialized = false;
             JsonPropertyInfo = null;
             TempEnumerableValues = null;
             TempDictionaryValues = null;
@@ -183,12 +191,12 @@ namespace System.Text.Json
 
         public Type GetElementType()
         {
-            if (IsEnumerableProperty || IsDictionaryProperty || IsIDictionaryConstructibleProperty)
+            if (IsCollectionForProperty)
             {
                 return JsonPropertyInfo.ElementClassInfo.Type;
             }
 
-            if (IsEnumerable || IsDictionary || IsIDictionaryConstructible)
+            if (IsCollectionForClass)
             {
                 return JsonClassInfo.ElementClassInfo.Type;
             }
index d0916e5..f172b7d 100644 (file)
@@ -161,8 +161,8 @@ namespace System.Text.Json.Serialization.Tests
             }
             catch (JsonException ex)
             {
-                Assert.Contains("$.Level2.Level3s[0]", ex.ToString());
-                Assert.Equal(ex.Path, "$.Level2.Level3s[0]");
+                Assert.Contains("$.Level2.Level3s[1]", ex.ToString());
+                Assert.Equal(ex.Path, "$.Level2.Level3s[1]");
             }
         }
 
index 981321f..5cf0c49 100644 (file)
@@ -11,7 +11,7 @@ namespace System.Text.Json.Serialization.Tests
         /// <summary>
         /// A converter that uses Object as it's type.
         /// </summary>
-        private class ObjectConverter : JsonConverter<object>
+        private class ObjectToCustomerOrIntConverter : JsonConverter<object>
         {
             public override bool CanConvert(Type typeToConvert)
             {
@@ -53,7 +53,7 @@ namespace System.Text.Json.Serialization.Tests
         public static void CustomObjectConverter()
         {
             var options = new JsonSerializerOptions();
-            options.Converters.Add(new ObjectConverter());
+            options.Converters.Add(new ObjectToCustomerOrIntConverter());
 
             {
                 var customer = new Customer();
@@ -70,8 +70,15 @@ namespace System.Text.Json.Serialization.Tests
             }
 
             {
-                Customer obj = JsonSerializer.Parse<Customer>("{}", options);
-                Assert.Equal("HelloWorld", obj.Name);
+                object obj = JsonSerializer.Parse<Customer>("{}", options);
+                Assert.IsType<Customer>(obj);
+                Assert.Equal("HelloWorld", ((Customer)obj).Name);
+            }
+
+            {
+                // The converter doesn't handle object.
+                object obj = JsonSerializer.Parse<object>("{}", options);
+                Assert.IsType<JsonElement>(obj);
             }
 
             {
@@ -79,5 +86,68 @@ namespace System.Text.Json.Serialization.Tests
                 Assert.Equal(42, obj);
             }
         }
+
+        /// <summary>
+        /// A converter that converters True and False to a bool.
+        /// </summary>
+        private class ObjectToBoolConverter : JsonConverter<object>
+        {
+            public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+            {
+                if (reader.TokenType == JsonTokenType.True)
+                {
+                    return true;
+                }
+
+                if (reader.TokenType == JsonTokenType.False)
+                {
+                    return false;
+                }
+
+                // Use JsonElement as fallback.
+                var converter = options.GetConverter(typeof(JsonElement)) as JsonConverter<JsonElement>;
+                if (converter != null)
+                {
+                    return converter.Read(ref reader, typeToConvert, options);
+                }
+
+                // Shouldn't get here.
+                throw new JsonException();
+            }
+
+            public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
+            {
+                throw new InvalidOperationException("Directly writing object not supported");
+            }
+
+            public override void Write(Utf8JsonWriter writer, object value, JsonEncodedText propertyName, JsonSerializerOptions options)
+            {
+                throw new InvalidOperationException("Directly writing object not supported");
+            }
+        }
+
+        [Fact]
+        public static void CustomObjectBoolConverter()
+        {
+            var options = new JsonSerializerOptions();
+            options.Converters.Add(new ObjectToBoolConverter());
+
+            {
+                object obj = JsonSerializer.Parse<object>("true", options);
+                Assert.IsType<bool>(obj);
+                Assert.True((bool)obj);
+            }
+
+            {
+                object obj = JsonSerializer.Parse<object>("false", options);
+                Assert.IsType<bool>(obj);
+                Assert.False((bool)obj);
+            }
+
+            {
+                object obj = JsonSerializer.Parse<object>("{}", options);
+                Assert.IsType<JsonElement>(obj);
+            }
+        }
     }
 }
index 3913736..e4ac85b 100644 (file)
@@ -149,16 +149,28 @@ namespace System.Text.Json.Serialization.Tests
             JsonSerializer.Parse<EmptyClass>(SimpleTestClassWithNulls.s_json);
         }
 
-        [Fact]
-        public static void ReadObjectFail()
-        {
-            Assert.Throws<JsonException>(() => JsonSerializer.Parse<SimpleTestClass>("blah"));
-            Assert.Throws<JsonException>(() => JsonSerializer.Parse<object>("blah"));
-
-            Assert.Throws<JsonException>(() => JsonSerializer.Parse<SimpleTestClass>("true"));
-
-            Assert.Throws<JsonException>(() => JsonSerializer.Parse<SimpleTestClass>("null."));
-            Assert.Throws<JsonException>(() => JsonSerializer.Parse<object>("null."));
+        [Theory]
+        [InlineData("blah", true)]
+        [InlineData("null.", true)]
+        [InlineData("{{}", true)]
+        [InlineData("{", true)]
+        [InlineData("}", true)]
+        [InlineData("", true)]
+        [InlineData("true", false)]
+        [InlineData("[]", false)]
+        [InlineData("[{}]", false)]
+        public static void ReadObjectFail(string json, bool failsOnObject)
+        {
+            Assert.Throws<JsonException>(() => JsonSerializer.Parse<SimpleTestClass>(json));
+
+            if (failsOnObject)
+            {
+                Assert.Throws<JsonException>(() => JsonSerializer.Parse<object>(json));
+            }
+            else
+            {
+                Assert.IsType<JsonElement>(JsonSerializer.Parse<object>(json));
+            }
         }
 
         [Fact]
@@ -181,14 +193,6 @@ namespace System.Text.Json.Serialization.Tests
         }
 
         [Fact]
-        public static void ParseUntyped()
-        {
-            // Not supported until we are able to deserialize into JsonElement.
-            Assert.Throws<JsonException>(() => JsonSerializer.Parse<SimpleTestClass>("[]"));
-            Assert.Throws<JsonException>(() => JsonSerializer.Parse<object>("[]"));
-        }
-
-        [Fact]
         public static void ReadClassWithStringToPrimitiveDictionary()
         {
             TestClassWithStringToPrimitiveDictionary obj = JsonSerializer.Parse<TestClassWithStringToPrimitiveDictionary>(TestClassWithStringToPrimitiveDictionary.s_data);
index 7b35350..e057061 100644 (file)
@@ -64,6 +64,10 @@ namespace System.Text.Json.Serialization.Tests
 
             obj = JsonSerializer.Parse<object>(@"null");
             Assert.Null(obj);
+
+            obj = JsonSerializer.Parse<object>(@"[]");
+            element = (JsonElement)obj;
+            Assert.Equal(JsonValueType.Array, element.Type);
         }
 
         [Fact]