private JsonClassInfo _runtimeClassInfo;
private JsonClassInfo _declaredTypeClassInfo;
+ private JsonPropertyInfo _dictionaryValuePropertyPolicy;
+
public bool CanBeNull { get; private set; }
public ClassType ClassType;
}
}
+ /// <summary>
+ /// Return the JsonPropertyInfo for the TValue in IDictionary{string, TValue} when deserializing.
+ /// </summary>
+ /// <remarks>
+ /// This should not be called during warm-up (initial creation of JsonPropertyInfos) to avoid recursive behavior
+ /// which could result in a StackOverflowException.
+ /// </remarks>
+ public JsonPropertyInfo DictionaryValuePropertyPolicy
+ {
+ get
+ {
+ if (_dictionaryValuePropertyPolicy == null)
+ {
+ Debug.Assert(ClassType == ClassType.Dictionary || ClassType == ClassType.IDictionaryConstructible);
+
+ Type dictionaryValueType = ElementType;
+ Debug.Assert(ElementType != null);
+
+ _dictionaryValuePropertyPolicy = JsonClassInfo.CreateRootProperty(dictionaryValueType, Options);
+ }
+
+ return _dictionaryValuePropertyPolicy;
+ }
+ }
+
/// <summary>
/// Return the JsonClassInfo for the element type, or null if the property is not an enumerable or dictionary.
/// </summary>
return (TAttribute)propertyInfo?.GetCustomAttribute(typeof(TAttribute), inherit: false);
}
+ public abstract Type GetConcreteType(Type type);
+
public abstract Type GetDictionaryConcreteType();
- public abstract Type GetConcreteType(Type type);
+ public void GetDictionaryKeyAndValue(ref WriteStackFrame writeStackFrame, out string key, out object value)
+ {
+ Debug.Assert(ClassType == ClassType.Dictionary ||
+ ClassType == ClassType.IDictionaryConstructible);
+
+ if (writeStackFrame.CollectionEnumerator is IDictionaryEnumerator iDictionaryEnumerator)
+ {
+ // Since IDictionaryEnumerator is not based on generics we can obtain the value directly.
+ key = (string)iDictionaryEnumerator.Key;
+ value = iDictionaryEnumerator.Value;
+ }
+ else
+ {
+ // Forward to the generic dictionary.
+ DictionaryValuePropertyPolicy.GetDictionaryKeyAndValueFromGenericDictionary(ref writeStackFrame, out key, out value);
+ }
+ }
+
+ public abstract void GetDictionaryKeyAndValueFromGenericDictionary(ref WriteStackFrame writeStackFrame, out string key, out object value);
public virtual void GetPolicies()
{
return true;
}
- if (enumerable is IDictionary dictionary)
- {
- state.Current.CollectionEnumerator = dictionary.GetEnumerator();
- }
- else
- {
- state.Current.CollectionEnumerator = enumerable.GetEnumerator();
- }
+ // Let the dictionary return the default IEnumerator from its IEnumerable.GetEnumerator().
+ // For IDictionary-derived classes this is normally be IDictionaryEnumerator.
+ // For IDictionary<TKey, TVale>-derived classes this is normally IDictionaryEnumerator as well
+ // but may be IEnumerable<KeyValuePair<TKey, TValue>> if the dictionary only supports generics.
+ state.Current.CollectionEnumerator = enumerable.GetEnumerator();
if (state.Current.ExtensionDataStatus != ExtensionDataWriteStatus.Writing)
{
if (state.Current.CollectionEnumerator.MoveNext())
{
+ // A dictionary should not have a null KeyValuePair.
+ Debug.Assert(state.Current.CollectionEnumerator.Current != null);
+
+ bool obtainedValues = false;
+ string key = default;
+ object value = default;
+
// Check for polymorphism.
if (elementClassInfo.ClassType == ClassType.Unknown)
{
- object currentValue = ((IDictionaryEnumerator)state.Current.CollectionEnumerator).Entry.Value;
- GetRuntimeClassInfo(currentValue, ref elementClassInfo, options);
+ jsonPropertyInfo.GetDictionaryKeyAndValue(ref state.Current, out key, out value);
+ GetRuntimeClassInfo(value, ref elementClassInfo, options);
+ obtainedValues = true;
}
if (elementClassInfo.ClassType == ClassType.Value)
{
elementClassInfo.PolicyProperty.WriteDictionary(ref state, writer);
}
- else if (state.Current.CollectionEnumerator.Current == null)
- {
- writer.WriteNull(jsonPropertyInfo.Name);
- }
else
{
+ if (!obtainedValues)
+ {
+ jsonPropertyInfo.GetDictionaryKeyAndValue(ref state.Current, out key, out value);
+ }
+
// An object or another enumerator requires a new stack frame.
- var enumerator = (IDictionaryEnumerator)state.Current.CollectionEnumerator;
- object value = enumerator.Value;
state.Push(elementClassInfo, value);
- state.Current.KeyName = (string)enumerator.Key;
+ state.Current.KeyName = key;
}
return false;
key = polymorphicEnumerator.Current.Key;
value = (TProperty)polymorphicEnumerator.Current.Value;
}
- else if (current.IsIDictionaryConstructible || current.IsIDictionaryConstructibleProperty)
+ else if (current.CollectionEnumerator is IDictionaryEnumerator iDictionaryEnumerator)
{
- key = (string)((DictionaryEntry)current.CollectionEnumerator.Current).Key;
- value = (TProperty)((DictionaryEntry)current.CollectionEnumerator.Current).Value;
+ key = (string)iDictionaryEnumerator.Key;
+ value = (TProperty)iDictionaryEnumerator.Value;
}
else
{
- // Todo: support non-generic Dictionary here (IDictionaryEnumerator)
- // https://github.com/dotnet/corefx/issues/41034
throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(
current.JsonPropertyInfo.DeclaredPropertyType,
current.JsonPropertyInfo.ParentClassType,
}
[Fact]
- public static void HashtableFail()
+ public static void Hashtable()
{
- {
- IDictionary ht = new Hashtable();
- ht.Add("Key", "Value");
- Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(ht));
- }
+ const string Json = @"{""Key"":""Value""}";
+
+ IDictionary ht = new Hashtable();
+ ht.Add("Key", "Value");
+ string json = JsonSerializer.Serialize(ht);
+
+ Assert.Equal(Json, json);
+
+ ht = JsonSerializer.Deserialize<IDictionary>(json);
+ Assert.IsType<JsonElement>(ht["Key"]);
+ Assert.Equal("Value", ((JsonElement)ht["Key"]).GetString());
+
+ // Verify round-tripped JSON.
+ json = JsonSerializer.Serialize(ht);
+ Assert.Equal(Json, json);
}
[Fact]
public Dictionary<string, Dictionary<string, object>> ObjectDictVals { get; set; }
public Dictionary<string, SimpleClassWithDictionaries> ClassVals { get; set; }
}
+
+ public class DictionaryThatOnlyImplementsIDictionaryOfStringTValue<TValue> : IDictionary<string, TValue>
+ {
+ IDictionary<string, TValue> _inner = new Dictionary<string, TValue>();
+
+ public TValue this[string key]
+ {
+ get
+ {
+ return _inner[key];
+ }
+ set
+ {
+ _inner[key] = value;
+ }
+ }
+
+ public ICollection<string> Keys => _inner.Keys;
+
+ public ICollection<TValue> Values => _inner.Values;
+
+ public int Count => _inner.Count;
+
+ public bool IsReadOnly => _inner.IsReadOnly;
+
+ public void Add(string key, TValue value)
+ {
+ _inner.Add(key, value);
+ }
+
+ public void Add(KeyValuePair<string, TValue> item)
+ {
+ _inner.Add(item);
+ }
+
+ public void Clear()
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool Contains(KeyValuePair<string, TValue> item)
+ {
+ return _inner.Contains(item);
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return _inner.ContainsKey(key);
+ }
+
+ public void CopyTo(KeyValuePair<string, TValue>[] array, int arrayIndex)
+ {
+ // CopyTo should not be called.
+ throw new NotImplementedException();
+ }
+
+ public IEnumerator<KeyValuePair<string, TValue>> GetEnumerator()
+ {
+ // Don't return results directly from _inner since that will return an enumerator that returns
+ // IDictionaryEnumerator which should not require.
+ foreach (KeyValuePair<string, TValue> keyValuePair in _inner)
+ {
+ yield return keyValuePair;
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public bool Remove(string key)
+ {
+ // Remove should not be called.
+ throw new NotImplementedException();
+ }
+
+ public bool Remove(KeyValuePair<string, TValue> item)
+ {
+ // Remove should not be called.
+ throw new NotImplementedException();
+ }
+
+ public bool TryGetValue(string key, out TValue value)
+ {
+ return _inner.TryGetValue(key, out value);
+ }
+ }
+
+ [Fact]
+ public static void DictionaryOfTOnlyWithStringTValueAsInt()
+ {
+ const string Json = @"{""One"":1,""Two"":2}";
+
+ DictionaryThatOnlyImplementsIDictionaryOfStringTValue<int> dictionary;
+
+ dictionary = JsonSerializer.Deserialize<DictionaryThatOnlyImplementsIDictionaryOfStringTValue<int>>(Json);
+ Assert.Equal(1, dictionary["One"]);
+ Assert.Equal(2, dictionary["Two"]);
+
+ string json = JsonSerializer.Serialize(dictionary);
+ Assert.Equal(Json, json);
+ }
+
+ [Fact]
+ public static void DictionaryOfTOnlyWithStringTValueAsPoco()
+ {
+ const string Json = @"{""One"":{""Id"":1},""Two"":{""Id"":2}}";
+
+ DictionaryThatOnlyImplementsIDictionaryOfStringTValue<Poco> dictionary;
+
+ dictionary = JsonSerializer.Deserialize<DictionaryThatOnlyImplementsIDictionaryOfStringTValue<Poco>>(Json);
+ Assert.Equal(1, dictionary["One"].Id);
+ Assert.Equal(2, dictionary["Two"].Id);
+
+ string json = JsonSerializer.Serialize(dictionary);
+ Assert.Equal(Json, json);
+ }
+
+ public class DictionaryThatOnlyImplementsIDictionaryOfStringPoco : DictionaryThatOnlyImplementsIDictionaryOfStringTValue<Poco>
+ {
+ }
+
+ [Fact]
+ public static void DictionaryOfTOnlyWithStringPoco()
+ {
+ const string Json = @"{""One"":{""Id"":1},""Two"":{""Id"":2}}";
+
+ DictionaryThatOnlyImplementsIDictionaryOfStringPoco dictionary;
+
+ dictionary = JsonSerializer.Deserialize<DictionaryThatOnlyImplementsIDictionaryOfStringPoco>(Json);
+ Assert.Equal(1, dictionary["One"].Id);
+ Assert.Equal(2, dictionary["Two"].Id);
+
+ string json = JsonSerializer.Serialize(dictionary);
+ Assert.Equal(Json, json);
+ }
+
+ public class DictionaryThatHasIncomatibleEnumerator<TValue> : IDictionary<string, TValue>
+ {
+ IDictionary<string, TValue> _inner = new Dictionary<string, TValue>();
+
+ public TValue this[string key]
+ {
+ get
+ {
+ return _inner[key];
+ }
+ set
+ {
+ _inner[key] = value;
+ }
+ }
+
+ public ICollection<string> Keys => _inner.Keys;
+
+ public ICollection<TValue> Values => _inner.Values;
+
+ public int Count => _inner.Count;
+
+ public bool IsReadOnly => _inner.IsReadOnly;
+
+ public void Add(string key, TValue value)
+ {
+ _inner.Add(key, value);
+ }
+
+ public void Add(KeyValuePair<string, TValue> item)
+ {
+ _inner.Add(item);
+ }
+
+ public void Clear()
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool Contains(KeyValuePair<string, TValue> item)
+ {
+ return _inner.Contains(item);
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return _inner.ContainsKey(key);
+ }
+
+ public void CopyTo(KeyValuePair<string, TValue>[] array, int arrayIndex)
+ {
+ // CopyTo should not be called.
+ throw new NotImplementedException();
+ }
+
+ public IEnumerator<KeyValuePair<string, TValue>> GetEnumerator()
+ {
+ // The generic GetEnumerator() should not be called for this test.
+ throw new NotImplementedException();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ // Create an incompatible converter.
+ return new int[] {100,200 }.GetEnumerator();
+ }
+
+ public bool Remove(string key)
+ {
+ // Remove should not be called.
+ throw new NotImplementedException();
+ }
+
+ public bool Remove(KeyValuePair<string, TValue> item)
+ {
+ // Remove should not be called.
+ throw new NotImplementedException();
+ }
+
+ public bool TryGetValue(string key, out TValue value)
+ {
+ return _inner.TryGetValue(key, out value);
+ }
+ }
+
+ [Fact]
+ public static void VerifyDictionaryThatHasIncomatibleEnumeratorWithInt()
+ {
+ const string Json = @"{""One"":1,""Two"":2}";
+
+ DictionaryThatHasIncomatibleEnumerator<int> dictionary;
+ dictionary = JsonSerializer.Deserialize<DictionaryThatHasIncomatibleEnumerator<int>>(Json);
+ Assert.Equal(1, dictionary["One"]);
+ Assert.Equal(2, dictionary["Two"]);
+ Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(dictionary));
+ }
+
+
+ [Fact]
+ public static void VerifyDictionaryThatHasIncomatibleEnumeratorWithPoco()
+ {
+ const string Json = @"{""One"":{""Id"":1},""Two"":{""Id"":2}}";
+
+ DictionaryThatHasIncomatibleEnumerator<Poco> dictionary;
+ dictionary = JsonSerializer.Deserialize<DictionaryThatHasIncomatibleEnumerator<Poco>>(Json);
+ Assert.Equal(1, dictionary["One"].Id);
+ Assert.Equal(2, dictionary["Two"].Id);
+ Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(dictionary));
+ }
}
}