Honor converters for implementing types (dotnet/corefx#40411)
authorLayomi Akinrinade <laakinri@microsoft.com>
Tue, 20 Aug 2019 00:06:47 +0000 (20:06 -0400)
committerGitHub <noreply@github.com>
Tue, 20 Aug 2019 00:06:47 +0000 (20:06 -0400)
* Honor converters for implementing types

* Cache implementing-type converter if one is detected

* Address feedback: name parameters and add IEnumerable wrapper test

Commit migrated from https://github.com/dotnet/corefx/commit/ee880076f3bcacde1a32e88ea4ebd701a609e52d

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.Helpers.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs
src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.DerivedTypes.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.Dictionary.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj

index fad254d..e11e8c8 100644 (file)
@@ -28,16 +28,17 @@ namespace System.Text.Json
             JsonPropertyInfo jsonInfo;
 
             // Get implemented type, if applicable.
-            // Will return the propertyType itself if it's a non-enumerable, string, or natively supported collection.
-            Type implementedType = GetImplementedCollectionType(propertyType);
+            // Will return the propertyType itself if it's a non-enumerable, string, natively supported collection,
+            // or if a custom converter has been provided for the type.
+            Type implementedType = GetImplementedCollectionType(classType, propertyType, propertyInfo, out JsonConverter converter, options);
 
             if (implementedType != propertyType)
             {
-                jsonInfo = CreateProperty(implementedType, implementedType, implementedType, propertyInfo, typeof(object), options);
+                jsonInfo = CreateProperty(implementedType, implementedType, implementedType, propertyInfo, typeof(object), converter, options);
             }
             else
             {
-                jsonInfo = CreateProperty(propertyType, propertyType, propertyType, propertyInfo, classType, options);
+                jsonInfo = CreateProperty(propertyType, propertyType, propertyType, propertyInfo, classType, converter, options);
             }
 
             // Convert non-immutable dictionary interfaces to concrete types.
@@ -48,11 +49,11 @@ namespace System.Text.Json
                 Type newPropertyType = elementPropertyInfo.GetDictionaryConcreteType();
                 if (implementedType != newPropertyType)
                 {
-                    jsonInfo = CreateProperty(propertyType, newPropertyType, implementedType, propertyInfo, classType, options);
+                    jsonInfo = CreateProperty(propertyType, newPropertyType, implementedType, propertyInfo, classType, converter, options);
                 }
                 else
                 {
-                    jsonInfo = CreateProperty(propertyType, implementedType, implementedType, propertyInfo, classType, options);
+                    jsonInfo = CreateProperty(propertyType, implementedType, implementedType, propertyInfo, classType, converter, options);
                 }
             }
             else if (jsonInfo.ClassType == ClassType.Enumerable &&
@@ -66,16 +67,16 @@ namespace System.Text.Json
                 Type newPropertyType = elementPropertyInfo.GetConcreteType(implementedType);
                 if ((implementedType != newPropertyType) && implementedType.IsAssignableFrom(newPropertyType))
                 {
-                    jsonInfo = CreateProperty(propertyType, newPropertyType, implementedType, propertyInfo, classType, options);
+                    jsonInfo = CreateProperty(propertyType, newPropertyType, implementedType, propertyInfo, classType, converter, options);
                 }
                 else
                 {
-                    jsonInfo = CreateProperty(propertyType, implementedType, implementedType, propertyInfo, classType, options);
+                    jsonInfo = CreateProperty(propertyType, implementedType, implementedType, propertyInfo, classType, converter, options);
                 }
             }
             else if (propertyType != implementedType)
             {
-                jsonInfo = CreateProperty(propertyType, implementedType, implementedType, propertyInfo, classType, options);
+                jsonInfo = CreateProperty(propertyType, implementedType, implementedType, propertyInfo, classType, converter, options);
             }
 
             return jsonInfo;
@@ -87,6 +88,7 @@ namespace System.Text.Json
             Type implementedPropertyType,
             PropertyInfo propertyInfo,
             Type parentClassType,
+            JsonConverter converter,
             JsonSerializerOptions options)
         {
             bool hasIgnoreAttribute = (JsonPropertyInfo.GetAttribute<JsonIgnoreAttribute>(propertyInfo) != null);
@@ -106,15 +108,17 @@ namespace System.Text.Json
                     break;
             }
 
-            JsonConverter converter;
-
             // Create the JsonPropertyInfo<TType, TProperty>
             Type propertyInfoClassType;
             if (runtimePropertyType.IsGenericType && runtimePropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
             {
                 // First try to find a converter for the Nullable, then if not found use the underlying type.
                 // This supports custom converters that want to (de)serialize as null when the value is not null.
-                converter = options.DetermineConverterForProperty(parentClassType, runtimePropertyType, propertyInfo);
+                if (converter == null)
+                {
+                    converter = options.DetermineConverterForProperty(parentClassType, runtimePropertyType, propertyInfo);
+                }
+
                 if (converter != null)
                 {
                     propertyInfoClassType = typeof(JsonPropertyInfoNotNullable<,,,>).MakeGenericType(
@@ -132,7 +136,11 @@ namespace System.Text.Json
             }
             else
             {
-                converter = options.DetermineConverterForProperty(parentClassType, runtimePropertyType, propertyInfo);
+                if (converter == null)
+                {
+                    converter = options.DetermineConverterForProperty(parentClassType, runtimePropertyType, propertyInfo);
+                }
+
                 Type typeToConvert = converter?.TypeToConvert;
                 if (typeToConvert == null)
                 {
@@ -182,12 +190,25 @@ namespace System.Text.Json
 
         internal JsonPropertyInfo CreateRootObject(JsonSerializerOptions options)
         {
-            return CreateProperty(Type, Type, Type, null, Type, options);
+            return CreateProperty(
+                declaredPropertyType: Type,
+                runtimePropertyType: Type,
+                implementedPropertyType: Type,
+                propertyInfo: null,
+                parentClassType: Type,
+                converter: null,
+                options: options);
         }
 
         internal JsonPropertyInfo CreatePolymorphicProperty(JsonPropertyInfo property, Type runtimePropertyType, JsonSerializerOptions options)
         {
-            JsonPropertyInfo runtimeProperty = CreateProperty(property.DeclaredPropertyType, runtimePropertyType, property.ImplementedPropertyType, property.PropertyInfo, Type, options);
+            JsonPropertyInfo runtimeProperty = CreateProperty(
+                property.DeclaredPropertyType, runtimePropertyType,
+                property.ImplementedPropertyType,
+                property.PropertyInfo,
+                parentClassType: Type,
+                converter: null,
+                options: options);
             property.CopyRuntimeSettingsTo(runtimeProperty);
 
             return runtimeProperty;
index 4367a1a..355a97e 100644 (file)
@@ -6,6 +6,7 @@ using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Reflection;
+using System.Text.Json.Serialization;
 using System.Text.Json.Serialization.Converters;
 
 namespace System.Text.Json
@@ -126,7 +127,12 @@ namespace System.Text.Json
             SortedListTypeName,
         };
 
-        public static Type GetImplementedCollectionType(Type queryType)
+        public static Type GetImplementedCollectionType(
+            Type parentClassType,
+            Type queryType,
+            PropertyInfo propertyInfo,
+            out JsonConverter converter,
+            JsonSerializerOptions options)
         {
             Debug.Assert(queryType != null);
 
@@ -137,6 +143,14 @@ namespace System.Text.Json
                 queryType.IsArray ||
                 IsNativelySupportedCollection(queryType))
             {
+                converter = null;
+                return queryType;
+            }
+
+            // If a converter was provided, we should not detect implemented types and instead use the converter later.
+            converter = options.DetermineConverterForProperty(parentClassType, queryType, propertyInfo);
+            if (converter != null)
+            {
                 return queryType;
             }
 
index ca007bc..4d45329 100644 (file)
@@ -435,7 +435,7 @@ namespace System.Text.Json
         public static Type GetElementType(Type propertyType, Type parentType, MemberInfo memberInfo, JsonSerializerOptions options)
         {
             // We want to handle as the implemented collection type, if applicable.
-            Type implementedType = GetImplementedCollectionType(propertyType);
+            Type implementedType = GetImplementedCollectionType(parentType, propertyType, null, out _, options);
 
             if (!typeof(IEnumerable).IsAssignableFrom(implementedType))
             {
@@ -484,7 +484,7 @@ namespace System.Text.Json
             Debug.Assert(type != null);
 
             // We want to handle as the implemented collection type, if applicable.
-            Type implementedType = GetImplementedCollectionType(type);
+            Type implementedType = GetImplementedCollectionType(typeof(object), type, null, out _, options);
 
             if (implementedType.IsGenericType && implementedType.GetGenericTypeDefinition() == typeof(Nullable<>))
             {
index 124b431..e2fabcb 100644 (file)
@@ -355,7 +355,7 @@ namespace System.Text.Json
         {
             if (!_objectJsonProperties.TryGetValue(objectType, out JsonPropertyInfo propertyInfo))
             {
-                propertyInfo = JsonClassInfo.CreateProperty(objectType, objectType, objectType, null, typeof(object), options);
+                propertyInfo = JsonClassInfo.CreateProperty(objectType, objectType, objectType, null, typeof(object), null, options);
                 _objectJsonProperties[objectType] = propertyInfo;
             }
 
diff --git a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.DerivedTypes.cs b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.DerivedTypes.cs
new file mode 100644 (file)
index 0000000..6f8b171
--- /dev/null
@@ -0,0 +1,254 @@
+// 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 Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+    public static partial class CustomConverterTests
+    {
+        [Fact]
+        public static void CustomDerivedTypeConverter()
+        {
+            string json =
+                @"{
+                    ""ListWrapper"": [1, 2, 3],
+                    ""List"": [4, 5, 6],
+                    ""DictionaryWrapper"": {""key"": 1},
+                    ""Dictionary"": {""key"": 2}
+                }";
+
+            // Without converters, we deserialize as is.
+            DerivedTypesWrapper wrapper = JsonSerializer.Deserialize<DerivedTypesWrapper>(json);
+            int expected = 1;
+            foreach (int value in wrapper.ListWrapper)
+            {
+                Assert.Equal(expected++, value);
+            }
+            foreach (int value in wrapper.List)
+            {
+                Assert.Equal(expected++, value);
+            }
+            Assert.Equal(1, wrapper.DictionaryWrapper["key"]);
+            Assert.Equal(2, wrapper.Dictionary["key"]);
+
+            string serialized = JsonSerializer.Serialize(wrapper);
+            Assert.Contains(@"""ListWrapper"":[1,2,3]", serialized);
+            Assert.Contains(@"""List"":[4,5,6]", serialized);
+            Assert.Contains(@"""DictionaryWrapper"":{""key"":1}", serialized);
+            Assert.Contains(@"""Dictionary"":{""key"":2}", serialized);
+
+            // With converters, we expect no values in the wrappers per converters' implementation.
+            JsonSerializerOptions options = new JsonSerializerOptions();
+            options.Converters.Add(new ListWrapperConverter());
+            options.Converters.Add(new DictionaryWrapperConverter());
+
+            DerivedTypesWrapper customWrapper = JsonSerializer.Deserialize<DerivedTypesWrapper>(json, options);
+            Assert.Null(customWrapper.ListWrapper);
+            expected = 4;
+            foreach (int value in customWrapper.List)
+            {
+                Assert.Equal(expected++, value);
+            }
+            Assert.Null(customWrapper.DictionaryWrapper);
+            Assert.Equal(2, customWrapper.Dictionary["key"]);
+
+            // Clear metadata for serialize.
+            options = new JsonSerializerOptions();
+            options.Converters.Add(new ListWrapperConverter());
+            options.Converters.Add(new DictionaryWrapperConverter());
+
+            serialized = JsonSerializer.Serialize(wrapper, options);
+            Assert.Contains(@"""ListWrapper"":[]", serialized);
+            Assert.Contains(@"""List"":[4,5,6]", serialized);
+            Assert.Contains(@"""DictionaryWrapper"":{}", serialized);
+            Assert.Contains(@"""Dictionary"":{""key"":2}", serialized);
+        }
+
+        [Fact]
+        public static void CustomUnsupportedDictionaryConverter()
+        {
+            string json = @"{""DictionaryWrapper"": {""1"": 1}}";
+
+            UnsupportedDerivedTypesWrapper_Dictionary wrapper = new UnsupportedDerivedTypesWrapper_Dictionary
+            {
+                DictionaryWrapper = new UnsupportedDictionaryWrapper()
+            };
+            wrapper.DictionaryWrapper[1] = 1;
+
+            // Without converter, we throw.
+            Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<UnsupportedDerivedTypesWrapper_Dictionary>(json));
+            Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(wrapper));
+
+            // With converter, we expect no values in the wrapper per converter's implementation.
+            JsonSerializerOptions options = new JsonSerializerOptions();
+            options.Converters.Add(new UnsupportedDictionaryWrapperConverter());
+
+            UnsupportedDerivedTypesWrapper_Dictionary customWrapper = JsonSerializer.Deserialize<UnsupportedDerivedTypesWrapper_Dictionary>(json, options);
+            Assert.Null(customWrapper.DictionaryWrapper);
+
+            // Clear metadata for serialize.
+            options = new JsonSerializerOptions();
+            options.Converters.Add(new UnsupportedDictionaryWrapperConverter());
+            Assert.Equal(@"{""DictionaryWrapper"":{}}", JsonSerializer.Serialize(wrapper, options));
+        }
+
+        [Fact]
+        public static void CustomUnsupportedIEnumerableConverter()
+        {
+            string json = @"{""IEnumerableWrapper"": [""1"", ""2"", ""3""]}";
+
+            UnsupportedDerivedTypesWrapper_IEnumerable wrapper = new UnsupportedDerivedTypesWrapper_IEnumerable
+            {
+                IEnumerableWrapper = new StringIEnumerableWrapper() { "1", "2", "3" },
+            };
+
+            // Without converter, we throw on deserialize.
+            Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<UnsupportedDerivedTypesWrapper_IEnumerable>(json));
+            // Without converter, we serialize as is.
+            Assert.Equal(@"{""IEnumerableWrapper"":[""1"",""2"",""3""]}", JsonSerializer.Serialize(wrapper));
+
+            // With converter, we expect no values in the wrapper per converter's implementation.
+            JsonSerializerOptions options = new JsonSerializerOptions();
+            options.Converters.Add(new UnsupportedIEnumerableWrapperConverter());
+
+            UnsupportedDerivedTypesWrapper_IEnumerable customWrapper = JsonSerializer.Deserialize<UnsupportedDerivedTypesWrapper_IEnumerable>(json, options);
+            Assert.Null(customWrapper.IEnumerableWrapper);
+
+            // Clear metadata for serialize.
+            options = new JsonSerializerOptions();
+            options.Converters.Add(new UnsupportedIEnumerableWrapperConverter());
+            Assert.Equal(@"{""IEnumerableWrapper"":[]}", JsonSerializer.Serialize(wrapper, options));
+        }
+    }
+
+    public class ListWrapper : List<int> { }
+
+    public class DictionaryWrapper : Dictionary<string, int> { }
+
+    public class UnsupportedDictionaryWrapper : Dictionary<int, int> { }
+
+    public class DerivedTypesWrapper
+    {
+        public ListWrapper ListWrapper { get; set; }
+        public List<int> List { get; set; }
+        public DictionaryWrapper DictionaryWrapper { get; set; }
+        public Dictionary<string, int> Dictionary { get; set; }
+    }
+
+    public class UnsupportedDerivedTypesWrapper_Dictionary
+    {
+        public UnsupportedDictionaryWrapper DictionaryWrapper { get; set; }
+    }
+
+    public class UnsupportedDerivedTypesWrapper_IEnumerable
+    {
+        public StringIEnumerableWrapper IEnumerableWrapper { get; set; }
+    }
+
+    public class ListWrapperConverter : JsonConverter<ListWrapper>
+    {
+        public override ListWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            if (reader.TokenType != JsonTokenType.StartArray)
+            {
+                throw new JsonException();
+            }
+
+            while (reader.Read())
+            {
+                if (reader.TokenType == JsonTokenType.EndArray)
+                {
+                    return null;
+                }
+            }
+
+            throw new JsonException();
+        }
+        public override void Write(Utf8JsonWriter writer, ListWrapper value, JsonSerializerOptions options)
+        {
+            writer.WriteStartArray();
+            writer.WriteEndArray();
+        }
+    }
+
+    public class UnsupportedIEnumerableWrapperConverter : JsonConverter<StringIEnumerableWrapper>
+    {
+        public override StringIEnumerableWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            if (reader.TokenType != JsonTokenType.StartArray)
+            {
+                throw new JsonException();
+            }
+
+            while (reader.Read())
+            {
+                if (reader.TokenType == JsonTokenType.EndArray)
+                {
+                    return null;
+                }
+            }
+
+            throw new JsonException();
+        }
+        public override void Write(Utf8JsonWriter writer, StringIEnumerableWrapper value, JsonSerializerOptions options)
+        {
+            writer.WriteStartArray();
+            writer.WriteEndArray();
+        }
+    }
+
+    public class DictionaryWrapperConverter : JsonConverter<DictionaryWrapper>
+    {
+        public override DictionaryWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            if (reader.TokenType != JsonTokenType.StartObject)
+            {
+                throw new JsonException();
+            }
+
+            while (reader.Read())
+            {
+                if (reader.TokenType == JsonTokenType.EndObject)
+                {
+                    return null;
+                }
+            }
+
+            throw new JsonException();
+        }
+        public override void Write(Utf8JsonWriter writer, DictionaryWrapper value, JsonSerializerOptions options)
+        {
+            writer.WriteStartObject();
+            writer.WriteEndObject();
+        }
+    }
+
+    public class UnsupportedDictionaryWrapperConverter : JsonConverter<UnsupportedDictionaryWrapper>
+    {
+        public override UnsupportedDictionaryWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            if (reader.TokenType != JsonTokenType.StartObject)
+            {
+                throw new JsonException();
+            }
+
+            while (reader.Read())
+            {
+                if (reader.TokenType == JsonTokenType.EndObject)
+                {
+                    return null;
+                }
+            }
+
+            throw new JsonException();
+        }
+        public override void Write(Utf8JsonWriter writer, UnsupportedDictionaryWrapper value, JsonSerializerOptions options)
+        {
+            writer.WriteStartObject();
+            writer.WriteEndObject();
+        }
+    }
+}
index 3cc3dcf..a5904d1 100644 (file)
@@ -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.Collections;
 using System.Collections.Generic;
 using Xunit;
 
index 3f6fe6d..c07242a 100644 (file)
@@ -40,6 +40,7 @@
     <Compile Include="Serialization\CustomConverterTests.Attribute.cs" />
     <Compile Include="Serialization\CustomConverterTests.BadConverters.cs" />
     <Compile Include="Serialization\CustomConverterTests.cs" />
+    <Compile Include="Serialization\CustomConverterTests.DerivedTypes.cs" />
     <Compile Include="Serialization\CustomConverterTests.Dictionary.cs" />
     <Compile Include="Serialization\CustomConverterTests.Enum.cs" />
     <Compile Include="Serialization\CustomConverterTests.Exceptions.cs" />