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.
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 &&
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;
Type implementedPropertyType,
PropertyInfo propertyInfo,
Type parentClassType,
+ JsonConverter converter,
JsonSerializerOptions options)
{
bool hasIgnoreAttribute = (JsonPropertyInfo.GetAttribute<JsonIgnoreAttribute>(propertyInfo) != null);
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(
}
else
{
- converter = options.DetermineConverterForProperty(parentClassType, runtimePropertyType, propertyInfo);
+ if (converter == null)
+ {
+ converter = options.DetermineConverterForProperty(parentClassType, runtimePropertyType, propertyInfo);
+ }
+
Type typeToConvert = converter?.TypeToConvert;
if (typeToConvert == null)
{
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;
--- /dev/null
+// 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();
+ }
+ }
+}