Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix InvalidCastException when deserializing some dictionary types #42835

Merged
merged 1 commit into from
Sep 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStac
ThrowHelper.ThrowNotSupportedException_CannotPopulateCollection(TypeToConvert, ref reader, ref state);
}

// Strings are intentionally used as keys when deserializing non-generic dictionaries.
state.Current.ReturnValue = new Dictionary<string, object>();
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
namespace System.Text.Json.Serialization.Converters
{
/// <summary>
/// Converter for <cref>System.Collections.Generic.IDictionary{string, TValue}</cref> that
/// Converter for <cref>System.Collections.Generic.IDictionary{TKey, TValue}</cref> that
/// (de)serializes as a JSON object with properties representing the dictionary element key and value.
/// </summary>
internal sealed class IDictionaryOfTKeyTValueConverter<TCollection, TKey, TValue>
Expand Down Expand Up @@ -35,7 +35,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStac
ThrowHelper.ThrowNotSupportedException_CannotPopulateCollection(TypeToConvert, ref reader, ref state);
}

state.Current.ReturnValue = new Dictionary<string, TValue>();
state.Current.ReturnValue = new Dictionary<TKey, TValue>();
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ public static MethodInfo GetImmutableEnumerableCreateRangeMethod(this Type type,

[DynamicDependency(CreateRangeMethodNameForDictionary, ImmutableDictionaryTypeName, ImmutableCollectionsAssembly)]
[DynamicDependency(CreateRangeMethodNameForDictionary, ImmutableSortedDictionaryTypeName, ImmutableCollectionsAssembly)]
public static MethodInfo GetImmutableDictionaryCreateRangeMethod(this Type type, Type elementType)
public static MethodInfo GetImmutableDictionaryCreateRangeMethod(this Type type, Type keyType, Type valueType)
{
Type? constructingType = GetImmutableDictionaryConstructingType(type);
if (constructingType != null)
Expand All @@ -184,7 +184,7 @@ public static MethodInfo GetImmutableDictionaryCreateRangeMethod(this Type type,
method.IsGenericMethod &&
method.GetGenericArguments().Length == 2)
{
return method.MakeGenericMethod(typeof(string), elementType);
return method.MakeGenericMethod(keyType, valueType);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStac
ThrowHelper.ThrowNotSupportedException_CannotPopulateCollection(TypeToConvert, ref reader, ref state);
}

state.Current.ReturnValue = new Dictionary<string, TValue>();
state.Current.ReturnValue = new Dictionary<TKey, TValue>();
}

protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,21 @@ protected override void Add(TKey key, in TValue value, JsonSerializerOptions opt

protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state)
{
state.Current.ReturnValue = new Dictionary<string, TValue>();
state.Current.ReturnValue = new Dictionary<TKey, TValue>();
}

protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options)
{
JsonClassInfo classInfo = state.Current.JsonClassInfo;

Func<IEnumerable<KeyValuePair<string, TValue>>, TCollection>? creator = (Func<IEnumerable<KeyValuePair<string, TValue>>, TCollection>?)classInfo.CreateObjectWithArgs;
Func<IEnumerable<KeyValuePair<TKey, TValue>>, TCollection>? creator = (Func<IEnumerable<KeyValuePair<TKey, TValue>>, TCollection>?)classInfo.CreateObjectWithArgs;
if (creator == null)
{
creator = options.MemberAccessorStrategy.CreateImmutableDictionaryCreateRangeDelegate<TValue, TCollection>();
creator = options.MemberAccessorStrategy.CreateImmutableDictionaryCreateRangeDelegate<TCollection, TKey, TValue>();
classInfo.CreateObjectWithArgs = creator;
}

state.Current.ReturnValue = creator((Dictionary<string, TValue>)state.Current.ReturnValue!);
state.Current.ReturnValue = creator((Dictionary<TKey, TValue>)state.Current.ReturnValue!);
}

protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ protected override void ConvertCollection(ref ReadStack state, JsonSerializerOpt
Func<IEnumerable<TElement>, TCollection>? creator = (Func<IEnumerable<TElement>, TCollection>?)classInfo.CreateObjectWithArgs;
if (creator == null)
{
creator = options.MemberAccessorStrategy.CreateImmutableEnumerableCreateRangeDelegate<TElement, TCollection>();
creator = options.MemberAccessorStrategy.CreateImmutableEnumerableCreateRangeDelegate<TCollection, TElement>();
classInfo.CreateObjectWithArgs = creator;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOp
}

internal override object ReadWithQuotes(ref Utf8JsonReader reader)
=> throw new NotSupportedException();
{
ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(TypeToConvert);
return null!;
}

internal override void WriteWithQuotes(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ public abstract JsonClassInfo.ParameterizedConstructorDelegate<T, TArg0, TArg1,

public abstract Action<TCollection, object?> CreateAddMethodDelegate<TCollection>();

public abstract Func<IEnumerable<TElement>, TCollection> CreateImmutableEnumerableCreateRangeDelegate<TElement, TCollection>();
public abstract Func<IEnumerable<TElement>, TCollection> CreateImmutableEnumerableCreateRangeDelegate<TCollection, TElement>();

public abstract Func<IEnumerable<KeyValuePair<string, TElement>>, TCollection> CreateImmutableDictionaryCreateRangeDelegate<TElement, TCollection>();
public abstract Func<IEnumerable<KeyValuePair<TKey, TValue>>, TCollection> CreateImmutableDictionaryCreateRangeDelegate<TCollection, TKey, TValue>();

public abstract Func<object, TProperty> CreatePropertyGetter<TProperty>(PropertyInfo propertyInfo);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ private static DynamicMethod CreateAddMethodDelegate(Type collectionType)
return dynamicMethod;
}

public override Func<IEnumerable<TElement>, TCollection> CreateImmutableEnumerableCreateRangeDelegate<TElement, TCollection>() =>
public override Func<IEnumerable<TElement>, TCollection> CreateImmutableEnumerableCreateRangeDelegate<TCollection, TElement>() =>
CreateDelegate<Func<IEnumerable<TElement>, TCollection>>(
CreateImmutableEnumerableCreateRangeDelegate(typeof(TCollection), typeof(TElement), typeof(IEnumerable<TElement>)));

Expand All @@ -195,13 +195,13 @@ private static DynamicMethod CreateImmutableEnumerableCreateRangeDelegate(Type c
return dynamicMethod;
}

public override Func<IEnumerable<KeyValuePair<string, TElement>>, TCollection> CreateImmutableDictionaryCreateRangeDelegate<TElement, TCollection>() =>
CreateDelegate<Func<IEnumerable<KeyValuePair<string, TElement>>, TCollection>>(
CreateImmutableDictionaryCreateRangeDelegate(typeof(TCollection), typeof(TElement), typeof(IEnumerable<KeyValuePair<string, TElement>>)));
public override Func<IEnumerable<KeyValuePair<TKey, TValue>>, TCollection> CreateImmutableDictionaryCreateRangeDelegate<TCollection, TKey, TValue>() =>
CreateDelegate<Func<IEnumerable<KeyValuePair<TKey, TValue>>, TCollection>>(
CreateImmutableDictionaryCreateRangeDelegate(typeof(TCollection), typeof(TKey), typeof(TValue), typeof(IEnumerable<KeyValuePair<TKey, TValue>>)));

private static DynamicMethod CreateImmutableDictionaryCreateRangeDelegate(Type collectionType, Type elementType, Type enumerableType)
private static DynamicMethod CreateImmutableDictionaryCreateRangeDelegate(Type collectionType, Type keyType, Type valueType, Type enumerableType)
{
MethodInfo realMethod = collectionType.GetImmutableDictionaryCreateRangeMethod(elementType);
MethodInfo realMethod = collectionType.GetImmutableDictionaryCreateRangeMethod(keyType, valueType);

var dynamicMethod = new DynamicMethod(
realMethod.Name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,18 +123,18 @@ public override JsonClassInfo.ParameterizedConstructorDelegate<T, TArg0, TArg1,
};
}

public override Func<IEnumerable<TElement>, TCollection> CreateImmutableEnumerableCreateRangeDelegate<TElement, TCollection>()
public override Func<IEnumerable<TElement>, TCollection> CreateImmutableEnumerableCreateRangeDelegate<TCollection, TElement>()
{
MethodInfo createRange = typeof(TCollection).GetImmutableEnumerableCreateRangeMethod(typeof(TElement));
return (Func<IEnumerable<TElement>, TCollection>)createRange.CreateDelegate(
typeof(Func<IEnumerable<TElement>, TCollection>));
}

public override Func<IEnumerable<KeyValuePair<string, TElement>>, TCollection> CreateImmutableDictionaryCreateRangeDelegate<TElement, TCollection>()
public override Func<IEnumerable<KeyValuePair<TKey, TValue>>, TCollection> CreateImmutableDictionaryCreateRangeDelegate<TCollection, TKey, TValue>()
{
MethodInfo createRange = typeof(TCollection).GetImmutableDictionaryCreateRangeMethod(typeof(TElement));
return (Func<IEnumerable<KeyValuePair<string, TElement>>, TCollection>)createRange.CreateDelegate(
typeof(Func<IEnumerable<KeyValuePair<string, TElement>>, TCollection>));
MethodInfo createRange = typeof(TCollection).GetImmutableDictionaryCreateRangeMethod(typeof(TKey), typeof(TValue));
return (Func<IEnumerable<KeyValuePair<TKey, TValue>>, TCollection>)createRange.CreateDelegate(
typeof(Func<IEnumerable<KeyValuePair<TKey, TValue>>, TCollection>));
}

public override Func<object, TProperty> CreatePropertyGetter<TProperty>(PropertyInfo propertyInfo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.Specialized;
using System.IO;
using System.Threading.Tasks;
Expand Down Expand Up @@ -574,5 +575,49 @@ public class SuffixNamingPolicy : JsonNamingPolicy
public const string Suffix = "_Suffix";
public override string ConvertName(string name) => name + Suffix;
}

[Fact]
public static void RoundtripAllDictionaryConverters()
{
const string Expected = @"{""1"":1}";

foreach (Type type in CollectionTestTypes.DeserializableDictionaryTypes<int, int>())
{
object dict = JsonSerializer.Deserialize(Expected, type);
Assert.Equal(Expected, JsonSerializer.Serialize(dict, type));
}
}

[Theory]
[InlineData(typeof(IDictionary))]
[InlineData(typeof(Hashtable))]
public static void IDictionary_Keys_ShouldBe_String_WhenDeserializing(Type type)
{
const string Expected = @"{""1998-02-14"":1}";

IDictionary dict = (IDictionary)JsonSerializer.Deserialize(Expected, type);
Assert.Equal(1, dict.Count);
JsonElement element = Assert.IsType<JsonElement>(dict["1998-02-14"]);
Assert.Equal(1, element.GetInt32());

Assert.Equal(Expected, JsonSerializer.Serialize(dict, type));
}

[Fact]
public static void GenericDictionary_WithObjectKeys_Throw_WhenDeserializing()
{
const string Expected = @"{""1998-02-14"":1}";

var dict = new Dictionary<object, int> { ["1998-02-14"] = 1 };
RunTest<IDictionary<object, int>>(dict);
RunTest<Dictionary<object, int>>(dict);
RunTest<ImmutableDictionary<object, int>>(ImmutableDictionary.CreateRange(dict));

void RunTest<T>(T dictionary)
{
Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<T>(Expected));
Assert.Equal(Expected, JsonSerializer.Serialize(dictionary));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -626,13 +626,13 @@ private static void RunAllDictionariessRoundTripTest<T>(List<T> numbers)

string jsonNumbersAsStrings = jsonBuilder_NumbersAsStrings.ToString();

foreach (Type type in CollectionTestTypes.DeserializableDictionaryTypes<T>())
foreach (Type type in CollectionTestTypes.DeserializableDictionaryTypes<string, T>())
{
object obj = JsonSerializer.Deserialize(jsonNumbersAsStrings, type, s_optionReadAndWriteFromStr);
JsonTestHelper.AssertJsonEqual(jsonNumbersAsStrings, JsonSerializer.Serialize(obj, s_optionReadAndWriteFromStr));
}

foreach (Type type in CollectionTestTypes.DeserializableNonDictionaryTypes<T>())
foreach (Type type in CollectionTestTypes.DeserializableNonGenericDictionaryTypes())
{
Dictionary<T, T> dict = JsonSerializer.Deserialize<Dictionary<T, T>>(jsonNumbersAsStrings, s_optionReadAndWriteFromStr);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1939,17 +1939,19 @@ public static IEnumerable<Type> DictionaryTypes<TElement>()
yield return typeof(GenericIReadOnlyDictionaryWrapper<string, TElement>); // IReadOnlyDictionaryOfStringTValueConverter
}

public static IEnumerable<Type> DeserializableDictionaryTypes<TElement>()
public static IEnumerable<Type> DeserializableDictionaryTypes<TKey, TValue>()
{
yield return typeof(Dictionary<string, TElement>); // DictionaryOfStringTValueConverter
yield return typeof(Dictionary<TKey, TValue>); // DictionaryOfStringTValueConverter
yield return typeof(Hashtable); // IDictionaryConverter
yield return typeof(ConcurrentDictionary<string, TElement>); // IDictionaryOfStringTValueConverter
yield return typeof(GenericIDictionaryWrapper<string, TElement>); // IDictionaryOfStringTValueConverter
yield return typeof(ImmutableDictionary<string, TElement>); // ImmutableDictionaryOfStringTValueConverter
yield return typeof(IReadOnlyDictionary<string, TElement>); // IReadOnlyDictionaryOfStringTValueConverter
yield return typeof(IDictionary); // IDictionaryConverter
yield return typeof(ConcurrentDictionary<TKey, TValue>); // IDictionaryOfStringTValueConverter
yield return typeof(IDictionary<TKey, TValue>); // IDictionaryOfStringTValueConverter
yield return typeof(GenericIDictionaryWrapper<TKey, TValue>); // IDictionaryOfStringTValueConverter
yield return typeof(ImmutableDictionary<TKey, TValue>); // ImmutableDictionaryOfStringTValueConverter
yield return typeof(IReadOnlyDictionary<TKey, TValue>); // IReadOnlyDictionaryOfStringTValueConverter
}

public static IEnumerable<Type> DeserializableNonDictionaryTypes<TElement>()
public static IEnumerable<Type> DeserializableNonGenericDictionaryTypes()
{
yield return typeof(Hashtable); // IDictionaryConverter
yield return typeof(SortedList); // IDictionaryConverter
Expand Down