From c9a36da4f2d8b38dfcae7749a9d1de0a634eab1c Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 26 Feb 2020 19:53:04 -0800 Subject: [PATCH 1/2] Add support for non-string Tkey on Dictionary using custom KeyConverter. (#2) --- .../src/System.Text.Json.csproj | 10 +- .../Reader/JsonReaderHelper.Unescaping.cs | 27 ++ .../Text/Json/Reader/Utf8JsonReader.TryGet.cs | 10 + .../Collection/DictionaryDefaultConverter.cs | 33 +- ...Converter.cs => DictionaryOfTKeyTValue.cs} | 41 +- .../Collection/IDictionaryConverter.cs | 4 +- .../IDictionaryOfStringTValueConverter.cs | 4 +- .../Collection/IEnumerableConverterFactory.cs | 28 +- ...adOnlyDictionaryOfStringTValueConverter.cs | 4 +- ...utableDictionaryOfStringTValueConverter.cs | 4 +- .../KeyConverters/EnumKeyConverter.cs | 46 ++ .../KeyConverters/GuidKeyConverter.cs | 39 ++ .../KeyConverters/Int32KeyConverter.cs | 39 ++ .../KeyConverters/KeyConverterOfTKey.cs | 86 ++++ .../KeyConverters/ObjectKeyConverter.cs | 39 ++ .../KeyConverters/StringKeyConverter.cs | 22 + .../Text/Json/Serialization/JsonClassInfo.cs | 24 +- .../Text/Json/Serialization/JsonConverter.cs | 2 + .../Json/Serialization/JsonConverterOfT.cs | 1 + .../JsonPropertyInfoOfTTypeToConvert.cs | 1 - .../JsonSerializerOptions.Converters.cs | 69 +++ .../Text/Json/Serialization/ReadStackFrame.cs | 1 + .../Utf8JsonWriter.WriteProperties.Guid.cs | 10 + ...JsonWriter.WriteProperties.SignedNumber.cs | 10 + .../Utf8JsonWriter.WriteProperties.String.cs | 2 + .../CustomConverterTests.DerivedTypes.cs | 4 +- .../DictionaryTests.KeyConverter.cs | 431 ++++++++++++++++++ .../tests/Serialization/DictionaryTests.cs | 8 +- .../tests/Serialization/ExtensionDataTests.cs | 3 +- .../tests/System.Text.Json.Tests.csproj | 3 +- 30 files changed, 943 insertions(+), 62 deletions(-) rename src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/{DictionaryOfStringTValueConverter.cs => DictionaryOfTKeyTValue.cs} (63%) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/EnumKeyConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/GuidKeyConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/Int32KeyConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/KeyConverterOfTKey.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/ObjectKeyConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/StringKeyConverter.cs create mode 100644 src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.KeyConverter.cs diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 68bf3d616528ba..65dc9c5dbf165e 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -1,4 +1,4 @@ - + true $(OutputPath)$(MSBuildProjectName).xml @@ -59,7 +59,7 @@ - + @@ -78,6 +78,12 @@ + + + + + + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs index d44a0b9316c09b..f4084cec3fd135 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs @@ -65,6 +65,33 @@ public static string GetUnescapedString(ReadOnlySpan utf8Source, int idx) return utf8String; } + // TODO: Borrowing this from https://github.com/dotnet/runtime/pull/32669/files#diff-82b934371fff193b37c85635239bb6ebR70 + // Remove when that PR is in or remove from #32669 if this gets in first. + public static ReadOnlySpan GetUnescapedSpan(ReadOnlySpan utf8Source, int idx) + { + // The escaped name is always >= than the unescaped, so it is safe to use escaped name for the buffer length. + int length = utf8Source.Length; + byte[]? pooledName = null; + + Span utf8Unescaped = length <= JsonConstants.StackallocThreshold ? + stackalloc byte[length] : + (pooledName = ArrayPool.Shared.Rent(length)); + + Unescape(utf8Source, utf8Unescaped, idx, out int written); + Debug.Assert(written > 0); + + ReadOnlySpan propertyName = utf8Unescaped.Slice(0, written).ToArray(); + Debug.Assert(!propertyName.IsEmpty); + + if (pooledName != null) + { + new Span(pooledName, 0, written).Clear(); + ArrayPool.Shared.Return(pooledName); + } + + return propertyName; + } + public static bool UnescapeAndCompare(ReadOnlySpan utf8Source, ReadOnlySpan other) { Debug.Assert(utf8Source.Length >= other.Length && utf8Source.Length / JsonConstants.MaxExpansionFactorWhileEscaping <= other.Length); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs index 642d4a6db5f1bf..e9aa1843862f8c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs @@ -583,6 +583,11 @@ public bool TryGetInt32(out int value) throw ThrowHelper.GetInvalidOperationException_ExpectedNumber(TokenType); } + return TryGetInt32AfterValidation(out value); + } + + internal bool TryGetInt32AfterValidation(out int value) + { ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; if (Utf8Parser.TryParse(span, out int tmp, out int bytesConsumed) && span.Length == bytesConsumed) @@ -945,6 +950,11 @@ public bool TryGetGuid(out Guid value) throw ThrowHelper.GetInvalidOperationException_ExpectedString(TokenType); } + return TryGetGuidAfterValidation(out value); + } + + internal bool TryGetGuidAfterValidation(out Guid value) + { ReadOnlySpan span = stackalloc byte[0]; if (HasValueSequence) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs index cc26ad1edc65c0..f96e5e8ec10ad8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs @@ -2,6 +2,9 @@ // 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.Buffers.Text; +using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -10,13 +13,13 @@ namespace System.Text.Json.Serialization.Converters /// /// Default base class implementation of JsonDictionaryConverter{TCollection} . /// - internal abstract class DictionaryDefaultConverter - : JsonDictionaryConverter + internal abstract class DictionaryDefaultConverter + : JsonDictionaryConverter where TKey : notnull { /// /// When overridden, adds the value to the collection. /// - protected abstract void Add(TValue value, JsonSerializerOptions options, ref ReadStack state); + protected abstract void Add(TKey key, TValue value, JsonSerializerOptions options, ref ReadStack state); /// /// When overridden, converts the temporary collection held in state.Current.ReturnValue to the final collection. @@ -31,6 +34,8 @@ protected virtual void CreateCollection(ref ReadStack state) { } internal override Type ElementType => typeof(TValue); + internal override Type KeyType => typeof(TKey); + protected static JsonConverter GetElementConverter(ref ReadStack state) { JsonConverter converter = (JsonConverter)state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!.ConverterBase; @@ -62,6 +67,8 @@ protected static JsonConverter GetValueConverter(ref WriteStack state) return converter; } + protected static KeyConverter GetKeyConverter(JsonClassInfo classInfo) => (KeyConverter)classInfo.KeyConverter; + internal sealed override bool OnTryRead( ref Utf8JsonReader reader, Type typeToConvert, @@ -69,6 +76,10 @@ internal sealed override bool OnTryRead( ref ReadStack state, [MaybeNullWhen(false)] out TCollection value) { + // Get the key converter at the very beginning; will throw NSE if there is no converter for TKey. + // This is performed at the very beginning to avoid processing unsupported types. + KeyConverter keyConverter = GetKeyConverter(state.Current.JsonClassInfo); + bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences(); if (!state.SupportContinuation && !shouldReadPreservedReferences) @@ -102,11 +113,12 @@ internal sealed override bool OnTryRead( } state.Current.JsonPropertyNameAsString = reader.GetString(); + keyConverter.OnTryRead(ref reader, keyConverter.TypeToConvert, options, ref state, out TKey key); // Read the value and add. reader.ReadWithVerify(); TValue element = elementConverter.Read(ref reader, typeof(TValue), options); - Add(element, options, ref state); + Add(key, element, options, ref state); } } else @@ -127,13 +139,14 @@ internal sealed override bool OnTryRead( ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); } - state.Current.JsonPropertyNameAsString = reader.GetString(); + state.Current.JsonPropertyNameAsString = reader.GetString()!; + keyConverter.OnTryRead(ref reader, keyConverter.TypeToConvert, options, ref state, out TKey key); reader.ReadWithVerify(); // Get the value from the converter and add it. elementConverter.TryRead(ref reader, typeof(TValue), options, ref state, out TValue element); - Add(element, options, ref state); + Add(key, element, options, ref state); } } } @@ -219,16 +232,16 @@ internal sealed override bool OnTryRead( state.Current.PropertyState = StackFramePropertyState.Name; + ReadOnlySpan propertyName = reader.GetSpan(); // Verify property doesn't contain metadata. if (shouldReadPreservedReferences) { - ReadOnlySpan propertyName = reader.GetSpan(); if (propertyName.Length > 0 && propertyName[0] == '$') { ThrowHelper.ThrowUnexpectedMetadataException(propertyName, ref reader, ref state); } } - + state.Current.DictionaryKeyName = propertyName.ToArray(); state.Current.JsonPropertyNameAsString = reader.GetString(); } @@ -245,6 +258,8 @@ internal sealed override bool OnTryRead( if (state.Current.PropertyState < StackFramePropertyState.TryRead) { + keyConverter.OnTryRead(ref reader, keyConverter.TypeToConvert, options, ref state, out TKey key); + // Get the value from the converter and add it. bool success = elementConverter.TryRead(ref reader, typeof(TValue), options, ref state, out TValue element); if (!success) @@ -253,7 +268,7 @@ internal sealed override bool OnTryRead( return false; } - Add(element, options, ref state); + Add(key, element, options, ref state); state.Current.EndElement(); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfStringTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValue.cs similarity index 63% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfStringTValueConverter.cs rename to src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValue.cs index 19c781187e1055..f282243d76cf2d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfStringTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValue.cs @@ -3,23 +3,20 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { /// - /// Converter for Dictionary{string, TValue} that (de)serializes as a JSON object with properties + /// Converter for Dictionary{TKey, TValue} that (de)serializes as a JSON object with properties /// representing the dictionary element key and value. /// - internal sealed class DictionaryOfStringTValueConverter - : DictionaryDefaultConverter - where TCollection : Dictionary + internal sealed class DictionaryOfTKeyTValueConverter + : DictionaryDefaultConverter + where TCollection : Dictionary + where TKey : notnull { - protected override void Add(TValue value, JsonSerializerOptions options, ref ReadStack state) + protected override void Add(TKey key, TValue value, JsonSerializerOptions options, ref ReadStack state) { - Debug.Assert(state.Current.ReturnValue is TCollection); - - string key = state.Current.JsonPropertyNameAsString!; ((TCollection)state.Current.ReturnValue!)[key] = value; } @@ -39,7 +36,12 @@ protected internal override bool OnWriteResume( JsonSerializerOptions options, ref WriteStack state) { - Dictionary.Enumerator enumerator; + // Get the key converter at the very beginning; will throw NSE if there is no converter for TKey. + // This is performed at the very beginning to avoid processing unsupported types. + KeyConverter keyConverter = GetKeyConverter(state.Current.JsonClassInfo); + + Dictionary.Enumerator enumerator; + if (state.Current.CollectionEnumerator == null) { enumerator = value.GetEnumerator(); @@ -50,20 +52,19 @@ protected internal override bool OnWriteResume( } else { - Debug.Assert(state.Current.CollectionEnumerator is Dictionary.Enumerator); - enumerator = (Dictionary.Enumerator)state.Current.CollectionEnumerator; + enumerator = (Dictionary.Enumerator)state.Current.CollectionEnumerator; } - JsonConverter converter = GetValueConverter(ref state); - if (!state.SupportContinuation && converter.CanUseDirectReadOrWrite) + JsonConverter valueConverter = GetValueConverter(ref state); + + if (!state.SupportContinuation && valueConverter.CanUseDirectReadOrWrite) { // Fast path that avoids validation and extra indirection. do { - string key = GetKeyName(enumerator.Current.Key, ref state, options); - writer.WritePropertyName(key); + keyConverter.OnTryWrite(writer, enumerator.Current.Key, options, ref state); // TODO: https://github.com/dotnet/runtime/issues/32523 - converter.Write(writer, enumerator.Current.Value!, options); + valueConverter.Write(writer, enumerator.Current.Value!, options); } while (enumerator.MoveNext()); } else @@ -77,14 +78,14 @@ protected internal override bool OnWriteResume( } TValue element = enumerator.Current.Value; + if (state.Current.PropertyState < StackFramePropertyState.Name) { state.Current.PropertyState = StackFramePropertyState.Name; - string key = GetKeyName(enumerator.Current.Key, ref state, options); - writer.WritePropertyName(key); + keyConverter.OnTryWrite(writer, enumerator.Current.Key, options, ref state); } - if (!converter.TryWrite(writer, element, options, ref state)) + if (!valueConverter.TryWrite(writer, element, options, ref state)) { state.Current.CollectionEnumerator = enumerator; return false; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs index 5702bd2da362a7..bb5565bd9f61d1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs @@ -13,10 +13,10 @@ namespace System.Text.Json.Serialization.Converters /// representing the dictionary element key and value. /// internal sealed class IDictionaryConverter - : DictionaryDefaultConverter + : DictionaryDefaultConverter where TCollection : IDictionary { - protected override void Add(object? value, JsonSerializerOptions options, ref ReadStack state) + protected override void Add(string _, object? value, JsonSerializerOptions options, ref ReadStack state) { Debug.Assert(state.Current.ReturnValue is IDictionary); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfStringTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfStringTValueConverter.cs index fd912fa799b1d9..c4d887425bb688 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfStringTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfStringTValueConverter.cs @@ -12,10 +12,10 @@ namespace System.Text.Json.Serialization.Converters /// (de)serializes as a JSON object with properties representing the dictionary element key and value. /// internal sealed class IDictionaryOfStringTValueConverter - : DictionaryDefaultConverter + : DictionaryDefaultConverter where TCollection : IDictionary { - protected override void Add(TValue value, JsonSerializerOptions options, ref ReadStack state) + protected override void Add(string _, TValue value, JsonSerializerOptions options, ref ReadStack state) { Debug.Assert(state.Current.ReturnValue is TCollection); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverterFactory.cs index 3d1de1343519ae..71c2b389af81b2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverterFactory.cs @@ -30,6 +30,7 @@ public override bool CanConvert(Type typeToConvert) [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.ConcurrentStackOfTConverter`2")] [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.DefaultArrayConverter`2")] [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.DictionaryOfStringTValueConverter`2")] + [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.DictionaryOfTKeyTValueConverter`3")] [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.ICollectionOfTConverter`2")] [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.IDictionaryOfStringTValueConverter`2")] [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.IEnumerableOfTConverter`2")] @@ -49,6 +50,7 @@ public override bool CanConvert(Type typeToConvert) Type converterType; Type[] genericArgs; Type? elementType = null; + Type? dictionaryKeyType = null; Type? actualTypeToConvert; // Array @@ -69,19 +71,13 @@ public override bool CanConvert(Type typeToConvert) converterType = typeof(ListOfTConverter<,>); elementType = actualTypeToConvert.GetGenericArguments()[0]; } - // Dictionary or deriving from Dictionary + // Dictionary or deriving from Dictionary else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericBaseClass(typeof(Dictionary<,>))) != null) { genericArgs = actualTypeToConvert.GetGenericArguments(); - if (genericArgs[0] == typeof(string)) - { - converterType = typeof(DictionaryOfStringTValueConverter<,>); - elementType = genericArgs[1]; - } - else - { - return null; - } + converterType = typeof(DictionaryOfTKeyTValueConverter<,,>); + dictionaryKeyType = genericArgs[0]; + elementType = genericArgs[1]; } // Immutable dictionaries from System.Collections.Immutable, e.g. ImmutableDictionary else if (typeToConvert.IsImmutableDictionaryType()) @@ -216,14 +212,22 @@ public override bool CanConvert(Type typeToConvert) if (converterType != null) { Type genericType; - if (converterType.GetGenericArguments().Length == 1) + + int numberOfGenericArgs = converterType.GetGenericArguments().Length; + + if (numberOfGenericArgs == 1) { genericType = converterType.MakeGenericType(typeToConvert); } - else + else if (numberOfGenericArgs == 2) { genericType = converterType.MakeGenericType(typeToConvert, elementType!); } + else + { + Debug.Assert(numberOfGenericArgs == 3); + genericType = converterType.MakeGenericType(typeToConvert, dictionaryKeyType!, elementType!); + } converter = (JsonConverter)Activator.CreateInstance( genericType, diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfStringTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfStringTValueConverter.cs index 74b70802ea022c..ff1667b40cf67d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfStringTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfStringTValueConverter.cs @@ -8,10 +8,10 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class IReadOnlyDictionaryOfStringTValueConverter - : DictionaryDefaultConverter + : DictionaryDefaultConverter where TCollection : IReadOnlyDictionary { - protected override void Add(TValue value, JsonSerializerOptions options, ref ReadStack state) + protected override void Add(string _, TValue value, JsonSerializerOptions options, ref ReadStack state) { Debug.Assert(state.Current.ReturnValue is Dictionary); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfStringTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfStringTValueConverter.cs index 145b304eb2b5bc..57d77b7c45805e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfStringTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfStringTValueConverter.cs @@ -8,10 +8,10 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class ImmutableDictionaryOfStringTValueConverter - : DictionaryDefaultConverter + : DictionaryDefaultConverter where TCollection : IReadOnlyDictionary { - protected override void Add(TValue value, JsonSerializerOptions options, ref ReadStack state) + protected override void Add(string _, TValue value, JsonSerializerOptions options, ref ReadStack state) { Debug.Assert(state.Current.ReturnValue is Dictionary); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/EnumKeyConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/EnumKeyConverter.cs new file mode 100644 index 00000000000000..37947710a0fca8 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/EnumKeyConverter.cs @@ -0,0 +1,46 @@ +// 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.Globalization; +using System.Text.Unicode; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class EnumKeyConverter : KeyConverter where TEnum : struct, Enum + { + public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? enumValue = reader.GetString(); + if (!Enum.TryParse(enumValue, out TEnum value) + && !Enum.TryParse(enumValue, ignoreCase: true, out value)) + { + ThrowHelper.ThrowJsonException(); + } + + return value; + } + + public override TEnum ReadKeyFromBytes(ReadOnlySpan bytes) + { + int idx = bytes.IndexOf(JsonConstants.BackSlash); + // if no escaping, just parse the bytes to string using TranscodeHelper. + string unescapedKeyName = idx > -1 ? JsonReaderHelper.GetUnescapedString(bytes, idx) : JsonReaderHelper.TranscodeHelper(bytes); + + if (!Enum.TryParse(unescapedKeyName, out TEnum value) + && !Enum.TryParse(unescapedKeyName, ignoreCase: true, out value)) + { + ThrowHelper.ThrowJsonException(); + } + + return value; + } + + public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) + { + string keyName = value.ToString(); + // Unlike EnumConverter we don't do any validation here since PropertyName can only be string. + writer.WritePropertyName(keyName); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/GuidKeyConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/GuidKeyConverter.cs new file mode 100644 index 00000000000000..562ebc3da00269 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/GuidKeyConverter.cs @@ -0,0 +1,39 @@ +// 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.Buffers.Text; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class GuidKeyConverter : KeyConverter + { + public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (!reader.TryGetGuidAfterValidation(out Guid keyValue)) + { + throw ThrowHelper.GetFormatException(DataType.Guid); + } + + return keyValue; + } + + public override Guid ReadKeyFromBytes(ReadOnlySpan bytes) + { + int idx = bytes.IndexOf(JsonConstants.BackSlash); + ReadOnlySpan unescapedBytes = idx > -1 ? JsonReaderHelper.GetUnescapedSpan(bytes, idx) : bytes; + + if (Utf8Parser.TryParse(unescapedBytes, out Guid keyValue, out int bytesConsumed) && bytesConsumed == unescapedBytes.Length) + { + return keyValue; + } + + throw ThrowHelper.GetFormatException(DataType.Guid); + } + + public override void Write(Utf8JsonWriter writer, Guid key, JsonSerializerOptions options) + => writer.WritePropertyName(key); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/Int32KeyConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/Int32KeyConverter.cs new file mode 100644 index 00000000000000..181f04af4f62ad --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/Int32KeyConverter.cs @@ -0,0 +1,39 @@ +// 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.Buffers.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class Int32KeyConverter : KeyConverter + { + public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (!reader.TryGetInt32AfterValidation(out int keyValue)) + { + throw ThrowHelper.GetFormatException(NumericType.Int32); + } + + return keyValue; + } + + public override int ReadKeyFromBytes(ReadOnlySpan bytes) + { + int idx = bytes.IndexOf(JsonConstants.BackSlash); + ReadOnlySpan unescapedBytes = idx > -1 ? JsonReaderHelper.GetUnescapedSpan(bytes, idx) : bytes; + + if (Utf8Parser.TryParse(unescapedBytes, out int keyValue, out int bytesConsumed) && bytesConsumed == unescapedBytes.Length) + { + return keyValue; + } + + throw ThrowHelper.GetFormatException(NumericType.Int32); + } + + public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) + => writer.WritePropertyName(value); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/KeyConverterOfTKey.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/KeyConverterOfTKey.cs new file mode 100644 index 00000000000000..952c7a9e56de52 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/KeyConverterOfTKey.cs @@ -0,0 +1,86 @@ +// 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.Diagnostics.CodeAnalysis; + +namespace System.Text.Json.Serialization.Converters +{ + internal abstract class KeyConverter : JsonConverter where TKey : notnull + { + // This is less API friendly than just call Read and keep the resulting dictionary key boxed in state.Current. + // Maybe we can call this only for internal converters. + public abstract TKey ReadKeyFromBytes(ReadOnlySpan bytes); + + internal override bool OnTryWrite(Utf8JsonWriter writer, TKey value, JsonSerializerOptions options, ref WriteStack state) + { + if (CanBePolymorphic) + { + JsonConverter runtimeConverter = GetPolymorphicConverter(value, options, ref state); + // Redirect to the runtime-type key converter. + runtimeConverter.WriteKeyAsObject(writer, value, options, ref state); + } + // If we need to apply the policy, we are forced to get a string since that is the only type that ConvertName can take as argument. + else if (options.DictionaryKeyPolicy != null && !state.Current.IgnoreDictionaryKeyPolicy) + { + string keyAsString = value.ToString()!; + keyAsString = options.DictionaryKeyPolicy.ConvertName(keyAsString); + + if (keyAsString == null) + { + ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(options.DictionaryKeyPolicy.GetType()); + } + + writer.WritePropertyName(keyAsString); + } + else + { + Write(writer, value, options); + } + + return true; + } + + private JsonConverter GetPolymorphicConverter(object value, JsonSerializerOptions options, ref WriteStack state) + { + Type runtimeType = value.GetType(); + JsonConverter runtimeConverter = options.GetOrAddKeyConverter(runtimeType, state.Current.JsonClassInfo.Type); + + // We don't support object itself as TKey, only the other supported types when they are boxed. + if (runtimeConverter is JsonConverter) + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(state.Current.JsonClassInfo.Type); + } + + return runtimeConverter; + } + + internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out TKey value) + { + // We already called reader.GetString(), there is no need to do it again for string keys. + if (typeof(TKey) == typeof(string)) + { + value = (TKey)(object)state.Current.JsonPropertyNameAsString!; + } + else + { + // Fast path that does not support continuation. + if (!state.SupportContinuation && !options.ReferenceHandling.ShouldReadPreservedReferences()) + { + value = Read(ref reader, typeToConvert, options); + } + // Slow path where the reader is no longer in TokenType.PropertyName, is in the element; i.e. in TValue. + // We remember the PropertyName bytes in the state and parse the key from there. + else + { + value = ReadKeyFromBytes(state.Current.DictionaryKeyName); + } + } + + return true; + } + + internal override void WriteKeyAsObject(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state) + => OnTryWrite(writer, (TKey)value, options, ref state); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/ObjectKeyConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/ObjectKeyConverter.cs new file mode 100644 index 00000000000000..e9fdc6533a1022 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/ObjectKeyConverter.cs @@ -0,0 +1,39 @@ +// 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. + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class ObjectKeyConverter : KeyConverter + { + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Can't use ParseValue as ObjectConverter does since it does not parse the property name but the value token next to it. + return ReadKeyFromBytes(reader.GetSpan()); + } + + public override object ReadKeyFromBytes(ReadOnlySpan bytes) + { + int idx = bytes.IndexOf(JsonConstants.BackSlash); + ReadOnlySpan unescapedBytes = idx > -1 ? JsonReaderHelper.GetUnescapedSpan(bytes, idx) : bytes; + + // Always wrap property name in quotes since reader.GetSpan() removes it from property names. + // The side effect of this is that any boxed number will be a JsonElement of JsonValueKind.String. + byte[] propertyNameArray = new byte[unescapedBytes.Length + 2]; + Span span = propertyNameArray; + span[0] = (byte)'"'; + unescapedBytes.CopyTo(span.Slice(1)); + span[span.Length - 1] = (byte)'"'; + + using (JsonDocument document = JsonDocument.Parse(propertyNameArray)) + { + return document.RootElement.Clone(); + } + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + throw new InvalidOperationException(); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/StringKeyConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/StringKeyConverter.cs new file mode 100644 index 00000000000000..75fd3b356b8c9a --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/StringKeyConverter.cs @@ -0,0 +1,22 @@ +// 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. + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class StringKeyConverter : KeyConverter + { + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override string ReadKeyFromBytes(ReadOnlySpan bytes) + { + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, string key, JsonSerializerOptions options) + => writer.WritePropertyName(key); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs index a6f6961c523241..53876283893647 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs @@ -10,6 +10,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Converters; namespace System.Text.Json { @@ -68,7 +69,23 @@ public JsonClassInfo? ElementClassInfo } } + private JsonConverter? _keyConverter; + public JsonConverter KeyConverter + { + get + { + if (_keyConverter == null) + { + Debug.Assert(KeyType != null); + _keyConverter = Options.GetOrAddKeyConverter(KeyType, Type); + } + + return _keyConverter; + } + } + public Type? ElementType { get; set; } + public Type? KeyType { get; set; } public JsonSerializerOptions Options { get; private set; } @@ -193,8 +210,13 @@ public JsonClassInfo(Type type, JsonSerializerOptions options) PolicyProperty = CreatePolicyProperty(type, runtimeType, converter!, ClassType, options); } break; - case ClassType.Enumerable: case ClassType.Dictionary: + { + Debug.Assert(converter!.KeyType != null); + KeyType = converter.KeyType; + goto case ClassType.Enumerable; + } + case ClassType.Enumerable: { ElementType = elementType; PolicyProperty = CreatePolicyProperty(type, runtimeType, converter!, ClassType, options); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs index 1bc90ae29a17d6..4ea5bd6620513c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs @@ -46,6 +46,7 @@ internal virtual bool HandleNullValue internal abstract JsonPropertyInfo CreateJsonPropertyInfo(); internal abstract Type? ElementType { get; } + internal virtual Type? KeyType { get; } // For polymorphic cases, the concrete type to create. internal virtual Type RuntimeType => TypeToConvert; @@ -61,5 +62,6 @@ internal bool ShouldFlush(Utf8JsonWriter writer, ref WriteStack state) internal abstract bool TryReadAsObject(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out object? value); internal abstract bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state); + internal virtual void WriteKeyAsObject(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state) { } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs index 1e2c19119c93a9..a0fd64c685f206 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs @@ -319,6 +319,7 @@ internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, Json // Ignore the naming policy for extension data. state.Current.IgnoreDictionaryKeyPolicy = true; + state.Current.DeclaredJsonPropertyInfo = state.Current.JsonClassInfo.ElementClassInfo!.PolicyProperty!; success = dictionaryConverter.OnWriteResume(writer, value, options, ref state); if (success) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfTTypeToConvert.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfTTypeToConvert.cs index 15ec6a3df81da0..35b694d43d7d4a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfTTypeToConvert.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfTTypeToConvert.cs @@ -124,7 +124,6 @@ public override bool GetMemberAndWriteJsonExtensionData(object obj, ref WriteSta } else { - state.Current.PolymorphicJsonPropertyInfo = state.Current.DeclaredJsonPropertyInfo!.RuntimeClassInfo.ElementClassInfo!.PolicyProperty; success = Converter.TryWriteDataExtensionProperty(writer, value, Options, ref state); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs index c17282b56eabee..6e9f19a21f9179 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Converters; @@ -282,5 +283,73 @@ private static IEnumerable DefaultSimpleConverters yield return new UriConverter(); } } + + // Get converter from the cached key converters or add a new Enum converter. + internal JsonConverter GetOrAddKeyConverter(Type keyType, Type dictionaryType) + { + if (s_keyConverters.TryGetValue(keyType, out JsonConverter? converter)) + { + return converter; + } + // short-term solution, instead of using a factory pattern like JsonStringEnumConverter, + // we just create the enum converter here add we add it to s_keyConverters. + else if (keyType.IsEnum) + { + converter = CreateEnumKeyConverter(keyType); + // Ignore failure case here in multi-threaded cases since the cached item will be equivalent. + s_keyConverters.TryAdd(keyType, converter); + + return converter; + } + else + { + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(dictionaryType); + return null; + } + } + + private JsonConverter CreateEnumKeyConverter(Type type) + { + JsonConverter converter = (JsonConverter)Activator.CreateInstance( + typeof(EnumKeyConverter<>).MakeGenericType(type), + BindingFlags.Instance | BindingFlags.Public, + binder: null, + Array.Empty(), + culture: null)!; + + return converter; + } + + // The global list of built-in key converters. + private static readonly Dictionary s_keyConverters = GetSupportedKeyConverters(); + + private const int NumberOfKeyConverters = 4; + + private static Dictionary GetSupportedKeyConverters() + { + var converters = new Dictionary(NumberOfKeyConverters); + + // Use a dictionary for simple converters. + foreach (JsonConverter converter in KeyConverters) + { + converters.Add(converter.TypeToConvert, converter); + } + + Debug.Assert(NumberOfKeyConverters == converters.Count); + + return converters; + } + + private static IEnumerable KeyConverters + { + get + { + // When adding to this, update NumberOfKeyConverters above. + yield return new StringKeyConverter(); + yield return new Int32KeyConverter(); + yield return new GuidKeyConverter(); + yield return new ObjectKeyConverter(); + } + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs index dfdfd19376c587..cd161e068ec40a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs @@ -18,6 +18,7 @@ internal struct ReadStackFrame // Support JSON Path on exceptions. public byte[]? JsonPropertyName; // This is Utf8 since we don't want to convert to string until an exception is thown. public string? JsonPropertyNameAsString; // This is used for dictionary keys and re-entry cases that specify a property name. + internal byte[]? DictionaryKeyName; // This will contain the Utf8 Json property name that represents a dictionary key; used to defer parsing on async/re-entry. // Validation state. public int OriginalDepth; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Guid.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Guid.cs index b18a107c78b350..474452d3940506 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Guid.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Guid.cs @@ -378,5 +378,15 @@ private void WriteStringIndented(ReadOnlySpan escapedPropertyName, Guid va output[BytesPending++] = JsonConstants.Quote; } + + internal void WritePropertyName(Guid value) + { + Span utf8PropertyName = stackalloc byte[JsonConstants.MaximumFormatGuidLength]; + + bool result = Utf8Formatter.TryFormat(value, utf8PropertyName, out int bytesWritten); + Debug.Assert(result); + + WritePropertyName(utf8PropertyName); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.SignedNumber.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.SignedNumber.cs index 360bea1089d0c2..6a331ef56c2032 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.SignedNumber.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.SignedNumber.cs @@ -432,5 +432,15 @@ private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, long va Debug.Assert(result); BytesPending += bytesWritten; } + + internal void WritePropertyName(int value) + { + Span utf8PropertyName = stackalloc byte[JsonConstants.MaximumFormatInt64Length]; + + bool result = Utf8Formatter.TryFormat(value, utf8PropertyName, out int bytesWritten); + Debug.Assert(result); + + WritePropertyName(utf8PropertyName.Slice(0, bytesWritten)); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs index 74ebc60214ff91..20fcde83990966 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using System.Buffers; +using System.Buffers.Text; +using System.Collections.Generic; using System.Diagnostics; namespace System.Text.Json diff --git a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.DerivedTypes.cs b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.DerivedTypes.cs index 37b2765cb2b2e2..21817f7348a103 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.DerivedTypes.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.DerivedTypes.cs @@ -76,7 +76,7 @@ public static void CustomUnsupportedDictionaryConverter() { DictionaryWrapper = new UnsupportedDictionaryWrapper() }; - wrapper.DictionaryWrapper[1] = 1; + wrapper.DictionaryWrapper[new Uri("https://github.com/dotnet/runtime/pull/32676")] = 1; // Without converter, we throw. Assert.Throws(() => JsonSerializer.Deserialize(json)); @@ -128,7 +128,7 @@ public class ListWrapper : List { } public class DictionaryWrapper : Dictionary { } - public class UnsupportedDictionaryWrapper : Dictionary { } + public class UnsupportedDictionaryWrapper : Dictionary { } public class DerivedTypesWrapper { diff --git a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.KeyConverter.cs b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.KeyConverter.cs new file mode 100644 index 00000000000000..3db728f54bc84a --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.KeyConverter.cs @@ -0,0 +1,431 @@ +// 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; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public abstract class DictionaryKeyConverterTests + { + protected abstract TKey Key { get; } + protected abstract TValue Value { get; } + protected virtual string _expectedJson => $"{{\"{Key}\":{Value}}}"; + protected virtual string _expectedJsonHashedProperties => $"{{\"{HashingNamingPolicy.HashName(Key.ToString())}\":{Value}}}"; + + private static JsonSerializerOptions _policyOptions = new JsonSerializerOptions { DictionaryKeyPolicy = new HashingNamingPolicy() }; + + protected virtual void Validate(Dictionary dictionary) + { + bool success = dictionary.TryGetValue(Key, out TValue value); + Assert.True(success); + Assert.Equal(Value, value); + } + + protected virtual void ValidateHashed(Dictionary dictionary) + { + bool success = dictionary.TryGetValue(Key.ToString().GetHashCode(), out TValue value); + Assert.True(success); + Assert.Equal(Value, value); + } + + protected virtual Dictionary BuildDictionary() + { + var dictionary = new Dictionary(); + dictionary.Add(Key, Value); + + return dictionary; + } + + [Fact] + public void TestNonStringKeyDictinary() + { + Dictionary dictionary = BuildDictionary(); + + string json = JsonSerializer.Serialize(dictionary); + Assert.Equal(_expectedJson, json); + + Dictionary dictionaryCopy = JsonSerializer.Deserialize>(json); + Validate(dictionaryCopy); + } + + [Fact] + public async Task TestNonStringKeyDictionaryAsync() + { + Dictionary dictionary = BuildDictionary(); + + MemoryStream serializeStream = new MemoryStream(); + await JsonSerializer.SerializeAsync(serializeStream, dictionary); + string json = Encoding.UTF8.GetString(serializeStream.ToArray()); + Assert.Equal(_expectedJson, json); + + byte[] jsonBytes = Encoding.UTF8.GetBytes(json); + Stream deserializeStream = new MemoryStream(jsonBytes); + Dictionary dictionaryCopy = await JsonSerializer.DeserializeAsync>(deserializeStream); + Validate(dictionaryCopy); + } + + [Fact] + public void TestNonStringKeyDictinaryUsingPolicy() + { + Dictionary dictionary = BuildDictionary(); + + string json = JsonSerializer.Serialize(dictionary, _policyOptions); + Assert.Equal(_expectedJsonHashedProperties, json); + + Dictionary dictionaryCopy = JsonSerializer.Deserialize>(json); + ValidateHashed(dictionaryCopy); + } + + [Fact] + public async Task TestNonStringKeyDictionaryAsyncUsingPolicy() + { + Dictionary dictionary = BuildDictionary(); + + MemoryStream serializeStream = new MemoryStream(); + await JsonSerializer.SerializeAsync(serializeStream, dictionary, _policyOptions); + string json = Encoding.UTF8.GetString(serializeStream.ToArray()); + Assert.Equal(_expectedJsonHashedProperties, json); + + byte[] jsonBytes = Encoding.UTF8.GetBytes(json); + Stream deserializeStream = new MemoryStream(jsonBytes); + Dictionary dictionaryCopy = await JsonSerializer.DeserializeAsync>(deserializeStream); + ValidateHashed(dictionaryCopy); + } + } + + public class DictionaryIntKey : DictionaryKeyConverterTests + { + protected override int Key => 1; + protected override int Value => 1; + } + + public class DictionaryGuidKey : DictionaryKeyConverterTests + { + // Use singleton pattern here so the Guid key does not change everytime this is called. + protected override Guid Key { get; } = Guid.NewGuid(); + protected override int Value => 1; + } + + public enum MyEnum + { + Foo, + Bar + } + + public class DictionaryEnumKey : DictionaryKeyConverterTests + { + protected override MyEnum Key => MyEnum.Foo; + protected override int Value => 1; + } + + [Flags] + public enum MyEnumFlags + { + Foo, + Bar, + Baz + } + + public class DictionaryEnumFlagsKey : DictionaryKeyConverterTests + { + protected override MyEnumFlags Key => MyEnumFlags.Foo | MyEnumFlags.Bar; + protected override int Value => 1; + } + + public class DictionaryStringKey : DictionaryKeyConverterTests + { + protected override string Key => "KeyString"; + protected override int Value => 1; + } + + public class DictionaryObjectKey : DictionaryKeyConverterTests + { + protected override object Key => 1; + protected override int Value => 1; + protected override string _expectedJson => BuildExpectedJson(); + protected override string _expectedJsonHashedProperties => BuildExpectedJsonHashedProperties(); + private Dictionary _dictionary; + private Dictionary _dictionaryOfStringKeys; + private Dictionary _dictionaryOfHashedKeys; + + protected override Dictionary BuildDictionary() + { + var dictionary = new Dictionary(); + // Add a bunch of different types. + dictionary.Add(1, 1); + dictionary.Add(Guid.NewGuid(), 2); + dictionary.Add("Key1", 3); + dictionary.Add(MyEnum.Foo, 4); + dictionary.Add(MyEnumFlags.Foo | MyEnumFlags.Bar, 5); + + _dictionary = dictionary; + _dictionaryOfStringKeys = dictionary.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value); + _dictionaryOfHashedKeys = dictionary.ToDictionary(kvp => kvp.Key.ToString().GetHashCode(), kvp => kvp.Value); + + return dictionary; + } + + private string BuildExpectedJson() + { + using (var stream = new MemoryStream()) + { + using (var writer = new Utf8JsonWriter(stream)) + { + writer.WriteStartObject(); + foreach (KeyValuePair kvp in _dictionary) + { + writer.WriteNumber(kvp.Key.ToString(), kvp.Value); + } + writer.WriteEndObject(); + } + + return Encoding.UTF8.GetString(stream.ToArray()); + } + } + + private string BuildExpectedJsonHashedProperties() + { + using (var stream = new MemoryStream()) + { + using (var writer = new Utf8JsonWriter(stream)) + { + writer.WriteStartObject(); + foreach (KeyValuePair kvp in _dictionary) + { + string hashedKey = HashingNamingPolicy.HashName(kvp.Key.ToString()); + writer.WriteNumber(hashedKey, kvp.Value); + } + writer.WriteEndObject(); + } + + return Encoding.UTF8.GetString(stream.ToArray()); + } + } + + protected override void Validate(Dictionary dictionary) + { + foreach (KeyValuePair kvp in dictionary) + { + if (kvp.Key is JsonElement keyJsonElement) + { + Assert.Equal(JsonValueKind.String, keyJsonElement.ValueKind); + + string keyString = keyJsonElement.GetString(); + Assert.Equal(_dictionaryOfStringKeys[keyString], kvp.Value); + } + else + { + Assert.True(false, "Polymorphic key is not JsonElement"); + } + } + } + + protected override void ValidateHashed(Dictionary dictionary) + { + foreach (KeyValuePair kvp in dictionary) + { + bool success = _dictionaryOfHashedKeys.TryGetValue(kvp.Key, out int value); + Assert.True(success); + Assert.Equal(value, kvp.Value); + } + } + } + + public class HashingNamingPolicy : JsonNamingPolicy + { + public override string ConvertName(string name) + { + return HashName(name); + } + + public static string HashName(string name) + { + return name.GetHashCode().ToString(); + } + } + + public abstract class DictionaryUnsupportedKeyTests + { + private Dictionary _dictionary => BuildDictionary(); + private static JsonSerializerOptions _policyOptions = new JsonSerializerOptions { DictionaryKeyPolicy = new HashingNamingPolicy() }; + + private Dictionary BuildDictionary() + { + return new Dictionary(); + } + + [Fact] + public void ThrowUnsupportedSerialize() => Assert.Throws(() => JsonSerializer.Serialize(_dictionary)); + [Fact] + public void ThrowUnsupportedSerializeUsingPolicy() => Assert.Throws(() => JsonSerializer.Serialize(_dictionary, _policyOptions)); + [Fact] + public async Task ThrowUnsupportedSerializeAsync() => await Assert.ThrowsAsync(() => JsonSerializer.SerializeAsync(new MemoryStream(), _dictionary)); + [Fact] + public async Task ThrowUnsupportedSerializeAsyncUsingPolicyAsync() => await Assert.ThrowsAsync(() => JsonSerializer.SerializeAsync(new MemoryStream(), _dictionary, _policyOptions)); + [Fact] + public void ThrowUnsupportedDeserialize() => Assert.Throws(() => JsonSerializer.Deserialize>("{}")); + [Fact] + public async Task ThrowUnsupportedDeserializeAsync() => await Assert.ThrowsAsync(async () => await JsonSerializer.DeserializeAsync>(new MemoryStream(Encoding.UTF8.GetBytes("{}")))); + } + + public class DictionaryUriKeyUnsupported : DictionaryUnsupportedKeyTests{ } + public class MyClass { } + public class DictionaryMyClassKeyUnsupported : DictionaryUnsupportedKeyTests{ } + public struct MyStruct { } + public class DictionaryMyStructKeyUnsupported : DictionaryUnsupportedKeyTests { } + + public class DictionaryNonStringKeyTests + { + private static JsonSerializerOptions _policyOptions = new JsonSerializerOptions { DictionaryKeyPolicy = new HashingNamingPolicy() }; + + [Fact] + public void ThrowOnUnsupportedRuntimeType() + { + Dictionary dictionary = new Dictionary(); + dictionary.Add(new Uri("http://github.com/Jozkee"), 1); + + Assert.Throws(() => JsonSerializer.Serialize(dictionary)); + + Assert.Throws(() => JsonSerializer.Serialize(dictionary, _policyOptions)); + } + + [Fact] + public async Task ThrowOnUnsupportedRuntimeTypeAsync() + { + Dictionary dictionary = new Dictionary(); + dictionary.Add(new Uri("http://github.com/Jozkee"), 1); + + await Assert.ThrowsAsync(() => JsonSerializer.SerializeAsync(new MemoryStream(), dictionary)); + + await Assert.ThrowsAsync(() => JsonSerializer.SerializeAsync(new MemoryStream(), dictionary, _policyOptions)); + } + + [Theory] // Extend this test when support for more types is added. + [InlineData(@"{""1.1"":1}", typeof(Dictionary))] + [InlineData(@"{""{00000000-0000-0000-0000-000000000000}"":1}", typeof(Dictionary))] + public void ThrowOnInvalidFormat(string json, Type typeToConvert) + { + JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize(json, typeToConvert)); + Assert.Contains(typeToConvert.ToString(), ex.Message); + } + + [Theory] // Extend this test when support for more types is added. + [InlineData(@"{""1.1"":1}", typeof(Dictionary))] + [InlineData(@"{""{00000000-0000-0000-0000-000000000000}"":1}", typeof(Dictionary))] + public async Task ThrowOnInvalidFormatAsync(string json, Type typeToConvert) + { + byte[] jsonBytes = Encoding.UTF8.GetBytes(json); + Stream stream = new MemoryStream(jsonBytes); + + JsonException ex = await Assert.ThrowsAsync(async () => await JsonSerializer.DeserializeAsync(stream, typeToConvert)); + Assert.Contains(typeToConvert.ToString(), ex.Message); + } + + [Theory] + [InlineData(@"{""\u0039"":1}", typeof(Dictionary), 9)] + [InlineData(@"{""\u0041"":1}", typeof(Dictionary), "A")] + [InlineData(@"{""\u0066\u006f\u006f"":1}", typeof(Dictionary), MyEnum.Foo)] + public static async Task TestUnescapedKeysAsync(string json, Type typeToConvert, object keyAsType) + { + byte[] utf8Json = Encoding.UTF8.GetBytes(json); + MemoryStream stream = new MemoryStream(utf8Json); + + object result = await JsonSerializer.DeserializeAsync(stream, typeToConvert); + + IDictionary dictionary = (IDictionary)result; + Assert.True(dictionary.Contains(keyAsType)); + } + + [Fact] + public static async Task TestUnescapedKeysAsync() + { + // Test Guid which cannot be passed as parameter on above method. + byte[] utf8Json = Encoding.UTF8.GetBytes(@"{""\u0036bb67e4e-9780-4895-851b-75f72ac34c5a"":1}"); + MemoryStream stream = new MemoryStream(utf8Json); + + Dictionary result = await JsonSerializer.DeserializeAsync>(stream); + + Guid myGuid = new Guid("6bb67e4e-9780-4895-851b-75f72ac34c5a"); + Assert.Equal(1, result[myGuid]); + + // Test object. + utf8Json = Encoding.UTF8.GetBytes(@"{""\u0038"":1}"); + stream = new MemoryStream(utf8Json); + + Dictionary result2 = await JsonSerializer.DeserializeAsync>(stream); + + Dictionary.Enumerator enumerator = result2.GetEnumerator(); + enumerator.MoveNext(); + + Assert.Equal(1, enumerator.Current.Value); + + JsonElement dictionaryKey = (JsonElement)enumerator.Current.Key; + JsonElement myElement = JsonDocument.Parse("\"8\"").RootElement; + + Assert.Equal(dictionaryKey.ValueKind, myElement.ValueKind); + Assert.Equal(dictionaryKey.GetString(), myElement.GetString()); + } + + private class UnsupportedDictionaryWrapper + { + public Dictionary Dictionary { get; set; } + } + + private class UnsupportedDictionaryWrapper_Wrapper + { + public UnsupportedDictionaryWrapper Wrapper { get; set; } + } + + [Fact] + public static void TestNotSuportedExceptionIsThrown() + { + // Dictionary> + // Does not throw NSE. + Assert.Null(JsonSerializer.Deserialize>("null")); + + // Throws NSE. + Assert.Throws(() => JsonSerializer.Deserialize>("\"\"")); + Assert.Throws(() => JsonSerializer.Deserialize>("{}")); + + // UnsupportedDictionaryWrapper + // Does not throw NSE. + Assert.Throws(() => JsonSerializer.Deserialize("\"\"")); + Assert.NotNull(JsonSerializer.Deserialize("{}")); + Assert.Null(JsonSerializer.Deserialize("null")); + Assert.NotNull(JsonSerializer.Deserialize(@"{""Dictionary"":null}")); + + // Throws NSE. + Assert.Throws(() => JsonSerializer.Deserialize(@"{""Dictionary"":{}}")); + + // UnsupportedDictionaryWrapper_Wrapper + // Does not throw NSE. + Assert.Throws(() => JsonSerializer.Deserialize("\"\"")); + Assert.NotNull(JsonSerializer.Deserialize("{}")); + Assert.Null(JsonSerializer.Deserialize("null")); + Assert.NotNull(JsonSerializer.Deserialize(@"{""Wrapper"":{}}")); + Assert.NotNull(JsonSerializer.Deserialize(@"{""Wrapper"":null}")); + Assert.NotNull(JsonSerializer.Deserialize(@"{""Wrapper"":{""Dictionary"":null}}")); + + // Throws NSE. + Assert.Throws(() => JsonSerializer.Deserialize(@"{""Wrapper"":{""Dictionary"":{}}")); + + + // List> + // Does not throw NSE. + Assert.Throws(() => JsonSerializer.Deserialize>>("\"\"")); + Assert.Null(JsonSerializer.Deserialize>>("null")); + Assert.NotNull(JsonSerializer.Deserialize>>("[]")); + Assert.NotNull(JsonSerializer.Deserialize>>("[null]")); + + // Throws NSE. + Assert.Throws(() => JsonSerializer.Deserialize>>("[\"\"]")); + Assert.Throws(() => JsonSerializer.Deserialize>>("[{}]")); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs index ba9729e8e9d3dd..c69f1035033116 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs @@ -644,7 +644,6 @@ public class Poco [Fact] public static void FirstGenericArgNotStringFail() { - Assert.Throws(() => JsonSerializer.Deserialize>(@"{1:1}")); Assert.Throws(() => JsonSerializer.Deserialize>(@"{1:1}")); } @@ -1623,9 +1622,10 @@ public static void DictionaryNotSupported() } catch (NotSupportedException e) { + // TODO: Borrow logic from https://github.com/dotnet/runtime/pull/32669 to keep appending the parent type to the NSE message. // The exception should contain className.propertyName and the invalid type. - Assert.Contains("ClassWithNotSupportedDictionary.MyDictionary", e.Message); - Assert.Contains("Dictionary`2[System.Int32,System.Int32]", e.Message); + //Assert.Contains("ClassWithNotSupportedDictionary.MyDictionary", e.Message); + Assert.Contains("Dictionary`2[System.Uri,System.Int32]", e.Message); } } @@ -1846,7 +1846,7 @@ public static void NullDictionaryValuesShouldDeserializeAsNull() public class ClassWithNotSupportedDictionary { - public Dictionary MyDictionary { get; set; } + public Dictionary MyDictionary { get; set; } } public class ClassWithNotSupportedDictionaryButIgnored diff --git a/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs b/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs index b23ef614c13071..1416ef849b1bae 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs @@ -655,9 +655,8 @@ public static void ExtensionProperty_InvalidDictionary() ClassWithInvalidExtensionPropertyStringString obj1 = new ClassWithInvalidExtensionPropertyStringString(); Assert.Throws(() => JsonSerializer.Serialize(obj1)); - // This fails with NotSupportedException since all Dictionaries currently need to have a string TKey. ClassWithInvalidExtensionPropertyObjectString obj2 = new ClassWithInvalidExtensionPropertyObjectString(); - Assert.Throws(() => JsonSerializer.Serialize(obj2)); + Assert.Throws(() => JsonSerializer.Serialize(obj2)); } private class ClassWithExtensionPropertyAlreadyInstantiated diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj index d101e9fe03f8b7..7ac47d199bca9e 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);$(NetFrameworkCurrent) $(DefineConstants);BUILDING_INBOX_LIBRARY @@ -58,6 +58,7 @@ + From 53bfb9636b04b157b945ca59a1a6e958a791e00b Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 27 Feb 2020 14:11:35 -0800 Subject: [PATCH 2/2] * Add ConcurrentDictionary to cache the key converters * Throw InvalidOperationException on StringKeyConverter --- .../KeyConverters/StringKeyConverter.cs | 4 ++-- .../JsonSerializerOptions.Converters.cs | 21 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/StringKeyConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/StringKeyConverter.cs index 75fd3b356b8c9a..5b283d5f125e8b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/StringKeyConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/KeyConverters/StringKeyConverter.cs @@ -8,12 +8,12 @@ internal sealed class StringKeyConverter : KeyConverter { public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); + throw new InvalidOperationException(); } public override string ReadKeyFromBytes(ReadOnlySpan bytes) { - throw new NotImplementedException(); + throw new InvalidOperationException(); } public override void Write(Utf8JsonWriter writer, string key, JsonSerializerOptions options) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs index 6e9f19a21f9179..12546320510889 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs @@ -287,19 +287,25 @@ private static IEnumerable DefaultSimpleConverters // Get converter from the cached key converters or add a new Enum converter. internal JsonConverter GetOrAddKeyConverter(Type keyType, Type dictionaryType) { - if (s_keyConverters.TryGetValue(keyType, out JsonConverter? converter)) + // Search on the cached key converters. + if (_keyConverters.TryGetValue(keyType, out JsonConverter? keyConverter)) { - return converter; + return keyConverter; + } + // Search the built-in key converters. + if (s_keyConverters.TryGetValue(keyType, out keyConverter)) + { + return keyConverter; } // short-term solution, instead of using a factory pattern like JsonStringEnumConverter, - // we just create the enum converter here add we add it to s_keyConverters. + // we just create the enum converter here add we add it to the cached _keyConverters. else if (keyType.IsEnum) { - converter = CreateEnumKeyConverter(keyType); + keyConverter = CreateEnumKeyConverter(keyType); // Ignore failure case here in multi-threaded cases since the cached item will be equivalent. - s_keyConverters.TryAdd(keyType, converter); + _keyConverters.TryAdd(keyType, keyConverter); - return converter; + return keyConverter; } else { @@ -320,6 +326,9 @@ private JsonConverter CreateEnumKeyConverter(Type type) return converter; } + // The cached key converters. + private readonly ConcurrentDictionary _keyConverters = new ConcurrentDictionary(); + // The global list of built-in key converters. private static readonly Dictionary s_keyConverters = GetSupportedKeyConverters();