diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs index fad254dcda40..e11e8c8ee5a6 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs @@ -28,16 +28,17 @@ private JsonPropertyInfo AddProperty(Type propertyType, PropertyInfo propertyInf 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 @@ private JsonPropertyInfo AddProperty(Type propertyType, PropertyInfo propertyInf 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 @@ private JsonPropertyInfo AddProperty(Type propertyType, PropertyInfo propertyInf 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 @@ internal static JsonPropertyInfo CreateProperty( Type implementedPropertyType, PropertyInfo propertyInfo, Type parentClassType, + JsonConverter converter, JsonSerializerOptions options) { bool hasIgnoreAttribute = (JsonPropertyInfo.GetAttribute(propertyInfo) != null); @@ -106,15 +108,17 @@ internal static JsonPropertyInfo CreateProperty( break; } - JsonConverter converter; - // Create the JsonPropertyInfo 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 @@ internal static JsonPropertyInfo CreateProperty( } 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 @@ internal static JsonPropertyInfo CreateProperty( 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; diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.Helpers.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.Helpers.cs index 4367a1ad280a..355a97e58e41 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.Helpers.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.Helpers.cs @@ -6,6 +6,7 @@ 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 @@ internal partial class JsonClassInfo 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); @@ -136,6 +142,14 @@ public static Type GetImplementedCollectionType(Type queryType) queryType.IsInterface || 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; } diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs index ca007bc654c3..4d45329caa0d 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs @@ -435,7 +435,7 @@ public static ulong GetKey(ReadOnlySpan propertyName) 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 @@ public static ClassType GetClassType(Type type, JsonSerializerOptions options) 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<>)) { diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 124b431a166c..e2fabcb460da 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -355,7 +355,7 @@ internal JsonPropertyInfo GetJsonPropertyInfoFromClassInfo(Type objectType, 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/System.Text.Json/tests/Serialization/CustomConverterTests.DerivedTypes.cs b/src/System.Text.Json/tests/Serialization/CustomConverterTests.DerivedTypes.cs new file mode 100644 index 000000000000..6f8b17186b32 --- /dev/null +++ b/src/System.Text.Json/tests/Serialization/CustomConverterTests.DerivedTypes.cs @@ -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(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(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(() => JsonSerializer.Deserialize(json)); + Assert.Throws(() => 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(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(() => JsonSerializer.Deserialize(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(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 { } + + public class DictionaryWrapper : Dictionary { } + + public class UnsupportedDictionaryWrapper : Dictionary { } + + public class DerivedTypesWrapper + { + public ListWrapper ListWrapper { get; set; } + public List List { get; set; } + public DictionaryWrapper DictionaryWrapper { get; set; } + public Dictionary 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 + { + 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 + { + 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 + { + 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 + { + 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(); + } + } +} diff --git a/src/System.Text.Json/tests/Serialization/CustomConverterTests.Dictionary.cs b/src/System.Text.Json/tests/Serialization/CustomConverterTests.Dictionary.cs index 3cc3dcf9f272..a5904d1baeff 100644 --- a/src/System.Text.Json/tests/Serialization/CustomConverterTests.Dictionary.cs +++ b/src/System.Text.Json/tests/Serialization/CustomConverterTests.Dictionary.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.Collections; using System.Collections.Generic; using Xunit; diff --git a/src/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/System.Text.Json/tests/System.Text.Json.Tests.csproj index 3f6fe6d737c3..c07242aec5ac 100644 --- a/src/System.Text.Json/tests/System.Text.Json.Tests.csproj +++ b/src/System.Text.Json/tests/System.Text.Json.Tests.csproj @@ -40,6 +40,7 @@ +