* Align dictionary key converter/metadata retrieval with pattern for collection elements
* Address review feedback
<value>The JSON value is not in a supported Boolean format.</value>
</data>
<data name="DictionaryKeyTypeNotSupported" xml:space="preserve">
- <value>The type '{0}' is not a supported Dictionary key type.</value>
+ <value>The type '{0}' is not a supported dictionary key using converter of type '{1}'.</value>
</data>
<data name="IgnoreConditionOnValueTypeInvalid" xml:space="preserve">
<value>The ignore condition 'JsonIgnoreCondition.WhenWritingNull' is not valid on value-type member '{0}' on type '{1}'. Consider using 'JsonIgnoreCondition.WhenWritingDefault'.</value>
internal override Type ElementType => typeof(TValue);
- protected static readonly Type KeyType = typeof(TKey);
+ internal override Type KeyType => typeof(TKey);
+
protected JsonConverter<TKey>? _keyConverter;
protected JsonConverter<TValue>? _valueConverter;
- protected static JsonConverter<TValue> GetValueConverter(JsonClassInfo elementClassInfo)
+ protected static JsonConverter<T> GetConverter<T>(JsonClassInfo classInfo)
{
- JsonConverter<TValue> converter = (JsonConverter<TValue>)elementClassInfo.PropertyInfoForClassInfo.ConverterBase;
+ JsonConverter<T> converter = (JsonConverter<T>)classInfo.PropertyInfoForClassInfo.ConverterBase;
Debug.Assert(converter != null); // It should not be possible to have a null converter at this point.
return converter;
}
- protected static JsonConverter<TKey> GetKeyConverter(Type keyType, JsonSerializerOptions options)
- => (JsonConverter<TKey>)options.GetDictionaryKeyConverter(keyType);
-
internal sealed override bool OnTryRead(
ref Utf8JsonReader reader,
Type typeToConvert,
CreateCollection(ref reader, ref state);
- JsonConverter<TValue> valueConverter = _valueConverter ??= GetValueConverter(elementClassInfo);
- if (valueConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null)
+ _valueConverter ??= GetConverter<TValue>(elementClassInfo);
+ if (_valueConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null)
{
// Process all elements.
while (true)
// Read the value and add.
reader.ReadWithVerify();
- TValue? element = valueConverter.Read(ref reader, ElementType, options);
+ TValue? element = _valueConverter.Read(ref reader, ElementType, options);
Add(key, element!, options, ref state);
}
}
reader.ReadWithVerify();
// Get the value from the converter and add it.
- valueConverter.TryRead(ref reader, ElementType, options, ref state, out TValue? element);
+ _valueConverter.TryRead(ref reader, ElementType, options, ref state, out TValue? element);
Add(key, element!, options, ref state);
}
}
}
// Process all elements.
- JsonConverter<TValue> elementConverter = _valueConverter ??= GetValueConverter(elementClassInfo);
+ _valueConverter ??= GetConverter<TValue>(elementClassInfo);
while (true)
{
if (state.Current.PropertyState == StackFramePropertyState.None)
{
state.Current.PropertyState = StackFramePropertyState.ReadValue;
- if (!SingleValueReadWithReadAhead(elementConverter.ClassType, ref reader, ref state))
+ if (!SingleValueReadWithReadAhead(_valueConverter.ClassType, ref reader, ref state))
{
state.Current.DictionaryKey = key;
value = default;
if (state.Current.PropertyState < StackFramePropertyState.TryRead)
{
// Get the value from the converter and add it.
- bool success = elementConverter.TryRead(ref reader, typeof(TValue), options, ref state, out TValue? element);
+ bool success = _valueConverter.TryRead(ref reader, typeof(TValue), options, ref state, out TValue? element);
if (!success)
{
state.Current.DictionaryKey = key;
}
else
{
- JsonConverter<TKey> keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options);
- key = keyConverter.ReadWithQuotes(ref reader);
+ _keyConverter ??= GetConverter<TKey>(state.Current.JsonClassInfo.KeyClassInfo!);
+ key = _keyConverter.ReadWithQuotes(ref reader);
unescapedPropertyNameAsString = reader.GetString()!;
}
enumerator = (Dictionary<TKey, TValue>.Enumerator)state.Current.CollectionEnumerator;
}
- JsonClassInfo elementClassInfo = state.Current.JsonClassInfo.ElementClassInfo!;
+ JsonClassInfo classInfo = state.Current.JsonClassInfo;
+ _keyConverter ??= GetConverter<TKey>(classInfo.KeyClassInfo!);
+ _valueConverter ??= GetConverter<TValue>(classInfo.ElementClassInfo!);
- JsonConverter<TKey> keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options);
- JsonConverter<TValue> valueConverter = _valueConverter ??= GetValueConverter(elementClassInfo);
-
- if (!state.SupportContinuation && valueConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null)
+ if (!state.SupportContinuation && _valueConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null)
{
// Fast path that avoids validation and extra indirection.
do
{
TKey key = enumerator.Current.Key;
- keyConverter.WriteWithQuotes(writer, key, options, ref state);
- valueConverter.Write(writer, enumerator.Current.Value, options);
+ _keyConverter.WriteWithQuotes(writer, key, options, ref state);
+ _valueConverter.Write(writer, enumerator.Current.Value, options);
} while (enumerator.MoveNext());
}
else
state.Current.PropertyState = StackFramePropertyState.Name;
TKey key = enumerator.Current.Key;
- keyConverter.WriteWithQuotes(writer, key, options, ref state);
+ _keyConverter.WriteWithQuotes(writer, key, options, ref state);
}
TValue element = enumerator.Current.Value;
- if (!valueConverter.TryWrite(writer, element, options, ref state))
+ if (!_valueConverter.TryWrite(writer, element, options, ref state))
{
state.Current.CollectionEnumerator = enumerator;
return false;
};
}
- private JsonConverter<object>? _objectConverter;
-
- private static JsonConverter<object> GetObjectKeyConverter(JsonSerializerOptions options)
- => (JsonConverter<object>)options.GetDictionaryKeyConverter(typeof(object));
-
protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state)
{
JsonClassInfo classInfo = state.Current.JsonClassInfo;
enumerator = (IDictionaryEnumerator)state.Current.CollectionEnumerator;
}
- JsonConverter<object?> valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo.ElementClassInfo!);
+ JsonClassInfo classInfo = state.Current.JsonClassInfo;
+ _valueConverter ??= GetConverter<object?>(classInfo.ElementClassInfo!);
+
do
{
if (ShouldFlush(writer, ref state))
// Optimize for string since that's the hot path.
if (key is string keyString)
{
- JsonConverter<string> stringKeyConverter = _keyConverter ??= GetKeyConverter(KeyType, options);
- stringKeyConverter.WriteWithQuotes(writer, keyString, options, ref state);
+ _keyConverter ??= GetConverter<string>(classInfo.KeyClassInfo!);
+ _keyConverter.WriteWithQuotes(writer, keyString, options, ref state);
}
else
{
// IDictionary is a special case since it has polymorphic object semantics on serialization
// but needs to use JsonConverter<string> on deserialization.
- JsonConverter<object> objectKeyConverter = _objectConverter ??= GetObjectKeyConverter(options);
- objectKeyConverter.WriteWithQuotes(writer, key, options, ref state);
+ _valueConverter.WriteWithQuotes(writer, key, options, ref state);
}
}
object? element = enumerator.Value;
- if (!valueConverter.TryWrite(writer, element, options, ref state))
+ if (!_valueConverter.TryWrite(writer, element, options, ref state))
{
state.Current.CollectionEnumerator = enumerator;
return false;
enumerator = (IEnumerator<KeyValuePair<TKey, TValue>>)state.Current.CollectionEnumerator;
}
- JsonConverter<TKey> keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options);
- JsonConverter<TValue> valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo.ElementClassInfo!);
+ JsonClassInfo classInfo = state.Current.JsonClassInfo;
+ _keyConverter ??= GetConverter<TKey>(classInfo.KeyClassInfo!);
+ _valueConverter ??= GetConverter<TValue>(classInfo.ElementClassInfo!);
+
do
{
if (ShouldFlush(writer, ref state))
{
state.Current.PropertyState = StackFramePropertyState.Name;
TKey key = enumerator.Current.Key;
- keyConverter.WriteWithQuotes(writer, key, options, ref state);
+ _keyConverter.WriteWithQuotes(writer, key, options, ref state);
}
TValue element = enumerator.Current.Value;
- if (!valueConverter.TryWrite(writer, element, options, ref state))
+ if (!_valueConverter.TryWrite(writer, element, options, ref state))
{
state.Current.CollectionEnumerator = enumerator;
return false;
enumerator = (Dictionary<TKey, TValue>.Enumerator)state.Current.CollectionEnumerator;
}
- JsonConverter<TKey> keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options);
- JsonConverter<TValue> valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo.ElementClassInfo!);
+ JsonClassInfo classInfo = state.Current.JsonClassInfo;
+ _keyConverter ??= GetConverter<TKey>(classInfo.KeyClassInfo!);
+ _valueConverter ??= GetConverter<TValue>(classInfo.ElementClassInfo!);
+
do
{
if (ShouldFlush(writer, ref state))
state.Current.PropertyState = StackFramePropertyState.Name;
TKey key = enumerator.Current.Key;
- keyConverter.WriteWithQuotes(writer, key, options, ref state);
+ _keyConverter.WriteWithQuotes(writer, key, options, ref state);
}
TValue element = enumerator.Current.Value;
- if (!valueConverter.TryWrite(writer, element, options, ref state))
+ if (!_valueConverter.TryWrite(writer, element, options, ref state))
{
state.Current.CollectionEnumerator = enumerator;
return false;
enumerator = (IEnumerator<KeyValuePair<TKey, TValue>>)state.Current.CollectionEnumerator;
}
- JsonConverter<TKey> keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options);
- JsonConverter<TValue> valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo.ElementClassInfo!);
+ JsonClassInfo classInfo = state.Current.JsonClassInfo;
+ _keyConverter ??= GetConverter<TKey>(classInfo.KeyClassInfo!);
+ _valueConverter ??= GetConverter<TValue>(classInfo.ElementClassInfo!);
+
do
{
if (ShouldFlush(writer, ref state))
state.Current.PropertyState = StackFramePropertyState.Name;
TKey key = enumerator.Current.Key;
- keyConverter.WriteWithQuotes(writer, key, options, ref state);
+ _keyConverter.WriteWithQuotes(writer, key, options, ref state);
}
TValue element = enumerator.Current.Value;
- if (!valueConverter.TryWrite(writer, element, options, ref state))
+ if (!_valueConverter.TryWrite(writer, element, options, ref state))
{
state.Current.CollectionEnumerator = enumerator;
return false;
internal abstract class JsonDictionaryConverter<T> : JsonResumableConverter<T>
{
internal sealed override ClassType ClassType => ClassType.Dictionary;
+
protected internal abstract bool OnWriteResume(Utf8JsonWriter writer, T dictionary, JsonSerializerOptions options, ref WriteStack state);
}
}
internal override object ReadWithQuotes(ref Utf8JsonReader reader)
{
- ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(TypeToConvert);
+ ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(TypeToConvert, this);
return null!;
}
internal override void WriteWithQuotes(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state)
{
- JsonConverter runtimeConverter = GetRuntimeConverter(value.GetType(), options);
- runtimeConverter.WriteWithQuotesAsObject(writer, value, options, ref state);
- }
-
- private JsonConverter GetRuntimeConverter(Type runtimeType, JsonSerializerOptions options)
- {
- JsonConverter runtimeConverter = options.GetDictionaryKeyConverter(runtimeType);
+ Type runtimeType = value.GetType();
+ JsonConverter runtimeConverter = options.GetConverter(runtimeType);
if (runtimeConverter == this)
{
- ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(runtimeType);
+ ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(runtimeType, this);
}
- return runtimeConverter;
+ runtimeConverter.WriteWithQuotesAsObject(writer, value, options, ref state);
}
internal override object ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling)
public JsonPropertyInfo? DataExtensionProperty { get; private set; }
- // If enumerable, the JsonClassInfo for the element type.
+ // If enumerable or dictionary, the JsonClassInfo for the element type.
private JsonClassInfo? _elementClassInfo;
/// <summary>
public Type? ElementType { get; set; }
+ // If dictionary, the JsonClassInfo for the key type.
+ private JsonClassInfo? _keyClassInfo;
+
+ /// <summary>
+ /// Return the JsonClassInfo for the key type, or null if the type is not a dictionary.
+ /// </summary>
+ /// <remarks>
+ /// This should not be called during warm-up (initial creation of JsonClassInfos) to avoid recursive behavior
+ /// which could result in a StackOverflowException.
+ /// </remarks>
+ public JsonClassInfo? KeyClassInfo
+ {
+ get
+ {
+ if (_keyClassInfo == null && KeyType != null)
+ {
+ Debug.Assert(ClassType == ClassType.Dictionary);
+
+ _keyClassInfo = Options.GetOrAddClass(KeyType);
+ }
+
+ return _keyClassInfo;
+ }
+ }
+
+ public Type? KeyType { get; set; }
+
public JsonSerializerOptions Options { get; private set; }
public Type Type { get; private set; }
}
break;
case ClassType.Enumerable:
+ {
+ ElementType = converter.ElementType;
+ CreateObject = Options.MemberAccessorStrategy.CreateConstructor(runtimeType);
+ }
+ break;
case ClassType.Dictionary:
{
+ KeyType = converter.KeyType;
ElementType = converter.ElementType;
CreateObject = Options.MemberAccessorStrategy.CreateConstructor(runtimeType);
}
internal abstract Type? ElementType { get; }
+ internal abstract Type? KeyType { get; }
+
/// <summary>
/// Cached value of TypeToConvert.IsValueType, which is an expensive call.
/// </summary>
throw new InvalidOperationException();
}
+ internal sealed override Type? KeyType => null;
+
internal sealed override Type? ElementType => null;
internal JsonConverter GetConverterInternal(Type typeToConvert, JsonSerializerOptions options)
return new JsonParameterInfo<T>();
}
+ internal override Type? KeyType => null;
+
internal override Type? ElementType => null;
/// <summary>
public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options);
internal virtual T ReadWithQuotes(ref Utf8JsonReader reader)
- => throw new InvalidOperationException();
+ {
+ ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(TypeToConvert, this);
+ return default;
+ }
internal virtual void WriteWithQuotes(Utf8JsonWriter writer, [DisallowNull] T value, JsonSerializerOptions options, ref WriteStack state)
- => throw new InvalidOperationException();
+ => ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(TypeToConvert, this);
internal sealed override void WriteWithQuotesAsObject(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state)
=> WriteWithQuotes(writer, (T)value, options, ref state);
converters.Add(converter.TypeToConvert, converter);
}
- internal JsonConverter GetDictionaryKeyConverter(Type keyType)
- {
- _dictionaryKeyConverters ??= GetDictionaryKeyConverters();
-
- if (!_dictionaryKeyConverters.TryGetValue(keyType, out JsonConverter? converter))
- {
- if (keyType.IsEnum)
- {
- converter = GetEnumConverter();
- _dictionaryKeyConverters[keyType] = converter;
- }
- else
- {
- ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(keyType);
- }
- }
-
- return converter!;
-
- // Use factory pattern to generate an EnumConverter with AllowStrings and AllowNumbers options for dictionary keys.
- // There will be one converter created for each enum type.
- JsonConverter GetEnumConverter()
- => EnumConverterFactory.Create(keyType, EnumConverterOptions.AllowStrings | EnumConverterOptions.AllowNumbers, this);
- }
-
- private ConcurrentDictionary<Type, JsonConverter>? _dictionaryKeyConverters;
-
- private static ConcurrentDictionary<Type, JsonConverter> GetDictionaryKeyConverters()
- {
- const int NumberOfConverters = 18;
- var converters = new ConcurrentDictionary<Type, JsonConverter>(Environment.ProcessorCount, NumberOfConverters);
-
- // When adding to this, update NumberOfConverters above.
- Add(s_defaultSimpleConverters[typeof(bool)]);
- Add(s_defaultSimpleConverters[typeof(byte)]);
- Add(s_defaultSimpleConverters[typeof(char)]);
- Add(s_defaultSimpleConverters[typeof(DateTime)]);
- Add(s_defaultSimpleConverters[typeof(DateTimeOffset)]);
- Add(s_defaultSimpleConverters[typeof(double)]);
- Add(s_defaultSimpleConverters[typeof(decimal)]);
- Add(s_defaultSimpleConverters[typeof(Guid)]);
- Add(s_defaultSimpleConverters[typeof(short)]);
- Add(s_defaultSimpleConverters[typeof(int)]);
- Add(s_defaultSimpleConverters[typeof(long)]);
- Add(s_defaultSimpleConverters[typeof(object)]);
- Add(s_defaultSimpleConverters[typeof(sbyte)]);
- Add(s_defaultSimpleConverters[typeof(float)]);
- Add(s_defaultSimpleConverters[typeof(string)]);
- Add(s_defaultSimpleConverters[typeof(ushort)]);
- Add(s_defaultSimpleConverters[typeof(uint)]);
- Add(s_defaultSimpleConverters[typeof(ulong)]);
-
- Debug.Assert(NumberOfConverters == converters.Count);
-
- return converters;
-
- void Add(JsonConverter converter) =>
- converters[converter.TypeToConvert] = converter;
- }
-
/// <summary>
/// The list of custom converters.
/// </summary>
ThrowHelper.ThrowInvalidOperationException_SerializationDuplicateAttribute(attributeType, classType, memberInfo);
return default;
}
-
}
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
- public static void ThrowNotSupportedException_DictionaryKeyTypeNotSupported(Type keyType)
+ public static void ThrowNotSupportedException_DictionaryKeyTypeNotSupported(Type keyType, JsonConverter converter)
{
- throw new NotSupportedException(SR.Format(SR.DictionaryKeyTypeNotSupported, keyType));
+ throw new NotSupportedException(SR.Format(SR.DictionaryKeyTypeNotSupported, keyType, converter.GetType()));
}
[DoesNotReturn]
Assert.Equal(Expected, JsonSerializer.Serialize(dictionary));
}
}
+
+ [Fact]
+ public static void KeyWithCustomConverter()
+ {
+ // TODO: update these tests after https://github.com/dotnet/runtime/issues/50071 is implemented.
+
+ JsonSerializerOptions options = new()
+ {
+ Converters = { new ConverterForInt32(), new ComplexKeyConverter() }
+ };
+
+ // Primitive key
+ string json = @"{
+ ""PrimitiveKey"":{
+ ""1"":""1""
+ }
+}
+";
+ ClassWithNonStringDictKeys obj = new()
+ {
+ PrimitiveKey = new Dictionary<int, string> { [1] = "1" },
+ };
+ RunTest(obj, json, typeof(int).ToString(), typeof(ConverterForInt32).ToString());
+
+ // Complex key
+ json = @"{
+ ""ComplexKey"":{
+ ""SomeStringRepresentation"":""1""
+ }
+}
+";
+ obj = new()
+ {
+ ComplexKey = new Dictionary<ClassWithIDictionary, string> { [new ClassWithIDictionary()] = "1" },
+ };
+ RunTest(obj, json, typeof(ClassWithIDictionary).ToString(), typeof(ComplexKeyConverter).ToString());
+
+ void RunTest(ClassWithNonStringDictKeys obj, string payload, string keyTypeAsStr, string converterTypeAsStr)
+ {
+ NotSupportedException ex = Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(obj, options));
+ string exAsStr = ex.ToString();
+ Assert.Contains(keyTypeAsStr, exAsStr);
+ Assert.Contains(converterTypeAsStr, exAsStr);
+
+ ex = Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<ClassWithNonStringDictKeys>(payload, options));
+ exAsStr = ex.ToString();
+ Assert.Contains(keyTypeAsStr, exAsStr);
+ Assert.Contains(converterTypeAsStr, exAsStr);
+ }
+ }
+
+ private class ClassWithNonStringDictKeys
+ {
+ public Dictionary<int, string> PrimitiveKey { get; set; }
+ public Dictionary<ClassWithIDictionary, string> ComplexKey { get; set; }
+ }
+
+ private class ComplexKeyConverter : JsonConverter<ClassWithIDictionary>
+ {
+ public override ClassWithIDictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ => throw new NotImplementedException();
+ public override void Write(Utf8JsonWriter writer, ClassWithIDictionary value, JsonSerializerOptions options)
+ => throw new NotImplementedException();
+ }
}
}