From 69a07a54eeb661f96b42122bb7f3fcf3b7cb98b1 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Mon, 19 Aug 2019 09:43:31 -0400 Subject: [PATCH 1/3] Honor converters for derived types --- .../JsonClassInfo.AddProperty.cs | 5 +- .../Serialization/JsonClassInfo.Helpers.cs | 9 +- .../Text/Json/Serialization/JsonClassInfo.cs | 4 +- .../CustomConverterTests.DerivedTypes.cs | 187 ++++++++++++++++++ .../CustomConverterTests.Dictionary.cs | 1 - .../tests/System.Text.Json.Tests.csproj | 1 + 6 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 src/System.Text.Json/tests/Serialization/CustomConverterTests.DerivedTypes.cs 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..6bb460d9b4c8 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,8 +28,9 @@ 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, options); if (implementedType != propertyType) { 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..c58bf5525990 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 @@ -126,7 +126,11 @@ internal partial class JsonClassInfo SortedListTypeName, }; - public static Type GetImplementedCollectionType(Type queryType) + public static Type GetImplementedCollectionType( + Type parentClassType, + Type queryType, + PropertyInfo propertyInfo, + JsonSerializerOptions options) { Debug.Assert(queryType != null); @@ -135,7 +139,8 @@ public static Type GetImplementedCollectionType(Type queryType) queryType.IsAbstract || queryType.IsInterface || queryType.IsArray || - IsNativelySupportedCollection(queryType)) + IsNativelySupportedCollection(queryType) || + options.DetermineConverterForProperty(parentClassType, queryType, propertyInfo) != 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..86da1f524e80 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, 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, options); if (implementedType.IsGenericType && implementedType.GetGenericTypeDefinition() == typeof(Nullable<>)) { 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..4e93987b73cc --- /dev/null +++ b/src/System.Text.Json/tests/Serialization/CustomConverterTests.DerivedTypes.cs @@ -0,0 +1,187 @@ +// 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"]); + + 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 CustomUnsupportedDerivedTypeConverter() + { + string json = @"{""DictionaryWrapper"": {""1"": 1}}"; + + UnsupportedDerivedTypesWrapper wrapper = new UnsupportedDerivedTypesWrapper + { + 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 customWrapper = JsonSerializer.Deserialize(json, options); + Assert.Null(customWrapper.DictionaryWrapper); + + Assert.Contains(@"""DictionaryWrapper"":{}", 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 + { + public UnsupportedDictionaryWrapper DictionaryWrapper { 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 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 @@ + From 1ed8c18818d1baf52edf34b210e70b43b069cd40 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Mon, 19 Aug 2019 10:33:03 -0400 Subject: [PATCH 2/3] Cache derived-type converter if one is detected --- .../JsonClassInfo.AddProperty.cs | 35 +++++++++++-------- .../Serialization/JsonClassInfo.Helpers.cs | 13 +++++-- .../Text/Json/Serialization/JsonClassInfo.cs | 4 +-- .../Serialization/JsonSerializerOptions.cs | 2 +- 4 files changed, 35 insertions(+), 19 deletions(-) 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 6bb460d9b4c8..19ac82f5b205 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 @@ -30,15 +30,15 @@ private JsonPropertyInfo AddProperty(Type propertyType, PropertyInfo propertyInf // Get implemented type, if applicable. // 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, options); + 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. @@ -49,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 && @@ -67,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; @@ -88,6 +88,7 @@ internal static JsonPropertyInfo CreateProperty( Type implementedPropertyType, PropertyInfo propertyInfo, Type parentClassType, + JsonConverter converter, JsonSerializerOptions options) { bool hasIgnoreAttribute = (JsonPropertyInfo.GetAttribute(propertyInfo) != null); @@ -107,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( @@ -133,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) { @@ -183,12 +190,12 @@ internal static JsonPropertyInfo CreateProperty( internal JsonPropertyInfo CreateRootObject(JsonSerializerOptions options) { - return CreateProperty(Type, Type, Type, null, Type, options); + return CreateProperty(Type, Type, Type, null, Type, null, 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, Type, null, 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 c58bf5525990..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 @@ -130,6 +131,7 @@ public static Type GetImplementedCollectionType( Type parentClassType, Type queryType, PropertyInfo propertyInfo, + out JsonConverter converter, JsonSerializerOptions options) { Debug.Assert(queryType != null); @@ -139,8 +141,15 @@ public static Type GetImplementedCollectionType( queryType.IsAbstract || queryType.IsInterface || queryType.IsArray || - IsNativelySupportedCollection(queryType) || - options.DetermineConverterForProperty(parentClassType, queryType, propertyInfo) != null) + 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 86da1f524e80..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(parentType, propertyType, null, options); + 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(typeof(object), type, null, options); + 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; } From c592f2c71db1a14579c93090039c00de522180e6 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Mon, 19 Aug 2019 14:40:44 -0400 Subject: [PATCH 3/3] Address feedback: name parameters and add IEnumerable wrapper test --- .../JsonClassInfo.AddProperty.cs | 17 +++- .../CustomConverterTests.DerivedTypes.cs | 79 +++++++++++++++++-- 2 files changed, 88 insertions(+), 8 deletions(-) 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 19ac82f5b205..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 @@ -190,12 +190,25 @@ internal static JsonPropertyInfo CreateProperty( internal JsonPropertyInfo CreateRootObject(JsonSerializerOptions options) { - return CreateProperty(Type, Type, Type, null, Type, null, 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, null, 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/tests/Serialization/CustomConverterTests.DerivedTypes.cs b/src/System.Text.Json/tests/Serialization/CustomConverterTests.DerivedTypes.cs index 4e93987b73cc..6f8b17186b32 100644 --- a/src/System.Text.Json/tests/Serialization/CustomConverterTests.DerivedTypes.cs +++ b/src/System.Text.Json/tests/Serialization/CustomConverterTests.DerivedTypes.cs @@ -55,6 +55,11 @@ public static void CustomDerivedTypeConverter() 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); @@ -63,28 +68,59 @@ public static void CustomDerivedTypeConverter() } [Fact] - public static void CustomUnsupportedDerivedTypeConverter() + public static void CustomUnsupportedDictionaryConverter() { string json = @"{""DictionaryWrapper"": {""1"": 1}}"; - UnsupportedDerivedTypesWrapper wrapper = new UnsupportedDerivedTypesWrapper + 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.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 customWrapper = JsonSerializer.Deserialize(json, options); + UnsupportedDerivedTypesWrapper_Dictionary customWrapper = JsonSerializer.Deserialize(json, options); Assert.Null(customWrapper.DictionaryWrapper); - Assert.Contains(@"""DictionaryWrapper"":{}", JsonSerializer.Serialize(wrapper, options)); + // 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)); } } @@ -102,11 +138,16 @@ public class DerivedTypesWrapper public Dictionary Dictionary { get; set; } } - public class UnsupportedDerivedTypesWrapper + 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) @@ -133,6 +174,32 @@ public override void Write(Utf8JsonWriter writer, ListWrapper value, JsonSeriali } } + 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)