diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 1a029bf64b235..7309f577181ed 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -527,4 +527,10 @@ The value cannot be 'JsonIgnoreCondition.Always'. + + The JSON value is not in a supported Boolean format. + + + The type '{0}' is not a supported Dictionary key type. + 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 f09a442249bba..5d8e706a2aa4b 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -66,10 +66,10 @@ - + - + @@ -78,9 +78,9 @@ - + - + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs index 1d13e73cdda8d..0e695e0435372 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs @@ -66,6 +66,7 @@ internal static class JsonConstants public const int MaxBase64ValueTokenSize = (MaxEscapedTokenSize >> 2) * 3 / MaxExpansionFactorWhileEscaping; // 125_000_000 bytes public const int MaxCharacterTokenSize = MaxEscapedTokenSize / MaxExpansionFactorWhileEscaping; // 166_666_666 characters + public const int MaximumFormatBooleanLength = 5; public const int MaximumFormatInt64Length = 20; // 19 + sign (i.e. -9223372036854775808) public const int MaximumFormatUInt64Length = 20; // i.e. 18446744073709551615 public const int MaximumFormatDoubleLength = 128; // default (i.e. 'G'), using 128 (rather than say 32) to be future-proof. 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 642d4a6db5f1b..a81a971a24234 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 @@ -6,6 +6,7 @@ using System.Buffers.Text; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace System.Text.Json { @@ -138,6 +139,16 @@ public byte GetByte() return value; } + internal byte GetByteWithQuotes() + { + ReadOnlySpan span = GetUnescapedSpan(); + if (!TryGetByteCore(out byte value, span)) + { + throw ThrowHelper.GetFormatException(NumericType.Byte); + } + return value; + } + /// /// Parses the current JSON token value from the source as an . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to an @@ -163,6 +174,16 @@ public sbyte GetSByte() return value; } + internal sbyte GetSByteWithQuotes() + { + ReadOnlySpan span = GetUnescapedSpan(); + if (!TryGetSByteCore(out sbyte value, span)) + { + throw ThrowHelper.GetFormatException(NumericType.SByte); + } + return value; + } + /// /// Parses the current JSON token value from the source as a . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a @@ -187,6 +208,16 @@ public short GetInt16() return value; } + internal short GetInt16WithQuotes() + { + ReadOnlySpan span = GetUnescapedSpan(); + if (!TryGetInt16Core(out short value, span)) + { + throw ThrowHelper.GetFormatException(NumericType.Int16); + } + return value; + } + /// /// Parses the current JSON token value from the source as an . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to an @@ -211,6 +242,16 @@ public int GetInt32() return value; } + internal int GetInt32WithQuotes() + { + ReadOnlySpan span = GetUnescapedSpan(); + if (!TryGetInt32Core(out int value, span)) + { + throw ThrowHelper.GetFormatException(NumericType.Int32); + } + return value; + } + /// /// Parses the current JSON token value from the source as a . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a @@ -235,6 +276,16 @@ public long GetInt64() return value; } + internal long GetInt64WithQuotes() + { + ReadOnlySpan span = GetUnescapedSpan(); + if (!TryGetInt64Core(out long value, span)) + { + throw ThrowHelper.GetFormatException(NumericType.Int64); + } + return value; + } + /// /// Parses the current JSON token value from the source as a . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a @@ -260,6 +311,16 @@ public ushort GetUInt16() return value; } + internal ushort GetUInt16WithQuotes() + { + ReadOnlySpan span = GetUnescapedSpan(); + if (!TryGetUInt16Core(out ushort value, span)) + { + throw ThrowHelper.GetFormatException(NumericType.UInt16); + } + return value; + } + /// /// Parses the current JSON token value from the source as a . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a @@ -285,6 +346,16 @@ public uint GetUInt32() return value; } + internal uint GetUInt32WithQuotes() + { + ReadOnlySpan span = GetUnescapedSpan(); + if (!TryGetUInt32Core(out uint value, span)) + { + throw ThrowHelper.GetFormatException(NumericType.UInt32); + } + return value; + } + /// /// Parses the current JSON token value from the source as a . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a @@ -310,6 +381,16 @@ public ulong GetUInt64() return value; } + internal ulong GetUInt64WithQuotes() + { + ReadOnlySpan span = GetUnescapedSpan(); + if (!TryGetUInt64Core(out ulong value, span)) + { + throw ThrowHelper.GetFormatException(NumericType.UInt64); + } + return value; + } + /// /// Parses the current JSON token value from the source as a . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a @@ -333,6 +414,16 @@ public float GetSingle() return value; } + internal float GetSingleWithQuotes() + { + ReadOnlySpan span = GetUnescapedSpan(); + if (!TryGetSingleCore(out float value, span)) + { + throw ThrowHelper.GetFormatException(NumericType.Single); + } + return value; + } + /// /// Parses the current JSON token value from the source as a . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a @@ -356,6 +447,16 @@ public double GetDouble() return value; } + internal double GetDoubleWithQuotes() + { + ReadOnlySpan span = GetUnescapedSpan(); + if (!TryGetDoubleCore(out double value, span)) + { + throw ThrowHelper.GetFormatException(NumericType.Double); + } + return value; + } + /// /// Parses the current JSON token value from the source as a . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a @@ -379,6 +480,16 @@ public decimal GetDecimal() return value; } + internal decimal GetDecimalWithQuotes() + { + ReadOnlySpan span = GetUnescapedSpan(); + if (!TryGetDecimalCore(out decimal value, span)) + { + throw ThrowHelper.GetFormatException(NumericType.Decimal); + } + return value; + } + /// /// Parses the current JSON token value from the source as a . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a @@ -402,6 +513,16 @@ public DateTime GetDateTime() return value; } + internal DateTime GetDateTimeNoValidation() + { + if (!TryGetDateTimeCore(out DateTime value)) + { + throw ThrowHelper.GetFormatException(DataType.DateTime); + } + + return value; + } + /// /// Parses the current JSON token value from the source as a . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a @@ -425,6 +546,16 @@ public DateTimeOffset GetDateTimeOffset() return value; } + internal DateTimeOffset GetDateTimeOffsetNoValidation() + { + if (!TryGetDateTimeOffsetCore(out DateTimeOffset value)) + { + throw ThrowHelper.GetFormatException(DataType.DateTimeOffset); + } + + return value; + } + /// /// Parses the current JSON token value from the source as a . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a @@ -448,6 +579,16 @@ public Guid GetGuid() return value; } + internal Guid GetGuidNoValidation() + { + if (!TryGetGuidCore(out Guid value)) + { + throw ThrowHelper.GetFormatException(DataType.Guid); + } + + return value; + } + /// /// Parses the current JSON token value from the source and decodes the Base64 encoded JSON string as bytes. /// Returns if the entire token value is encoded as valid Base64 text and can be successfully @@ -496,6 +637,12 @@ public bool TryGetByte(out byte value) } ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; + return TryGetByteCore(out value, span); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryGetByteCore(out byte value, ReadOnlySpan span) + { if (Utf8Parser.TryParse(span, out byte tmp, out int bytesConsumed) && span.Length == bytesConsumed) { @@ -526,6 +673,12 @@ public bool TryGetSByte(out sbyte value) } ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; + return TryGetSByteCore(out value, span); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryGetSByteCore(out sbyte value, ReadOnlySpan span) + { if (Utf8Parser.TryParse(span, out sbyte tmp, out int bytesConsumed) && span.Length == bytesConsumed) { @@ -555,6 +708,12 @@ public bool TryGetInt16(out short value) } ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; + return TryGetInt16Core(out value, span); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryGetInt16Core(out short value, ReadOnlySpan span) + { if (Utf8Parser.TryParse(span, out short tmp, out int bytesConsumed) && span.Length == bytesConsumed) { @@ -584,6 +743,12 @@ public bool TryGetInt32(out int value) } ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; + return TryGetInt32Core(out value, span); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryGetInt32Core(out int value, ReadOnlySpan span) + { if (Utf8Parser.TryParse(span, out int tmp, out int bytesConsumed) && span.Length == bytesConsumed) { @@ -613,6 +778,12 @@ public bool TryGetInt64(out long value) } ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; + return TryGetInt64Core(out value, span); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryGetInt64Core(out long value, ReadOnlySpan span) + { if (Utf8Parser.TryParse(span, out long tmp, out int bytesConsumed) && span.Length == bytesConsumed) { @@ -643,6 +814,12 @@ public bool TryGetUInt16(out ushort value) } ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; + return TryGetUInt16Core(out value, span); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryGetUInt16Core(out ushort value, ReadOnlySpan span) + { if (Utf8Parser.TryParse(span, out ushort tmp, out int bytesConsumed) && span.Length == bytesConsumed) { @@ -673,6 +850,12 @@ public bool TryGetUInt32(out uint value) } ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; + return TryGetUInt32Core(out value, span); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryGetUInt32Core(out uint value, ReadOnlySpan span) + { if (Utf8Parser.TryParse(span, out uint tmp, out int bytesConsumed) && span.Length == bytesConsumed) { @@ -703,6 +886,12 @@ public bool TryGetUInt64(out ulong value) } ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; + return TryGetUInt64Core(out value, span); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryGetUInt64Core(out ulong value, ReadOnlySpan span) + { if (Utf8Parser.TryParse(span, out ulong tmp, out int bytesConsumed) && span.Length == bytesConsumed) { @@ -732,6 +921,12 @@ public bool TryGetSingle(out float value) } ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; + return TryGetSingleCore(out value, span); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryGetSingleCore(out float value, ReadOnlySpan span) + { if (Utf8Parser.TryParse(span, out float tmp, out int bytesConsumed, _numberFormat) && span.Length == bytesConsumed) { @@ -761,6 +956,12 @@ public bool TryGetDouble(out double value) } ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; + return TryGetDoubleCore(out value, span); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryGetDoubleCore(out double value, ReadOnlySpan span) + { if (Utf8Parser.TryParse(span, out double tmp, out int bytesConsumed, _numberFormat) && span.Length == bytesConsumed) { @@ -790,6 +991,12 @@ public bool TryGetDecimal(out decimal value) } ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; + return TryGetDecimalCore(out value, span); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryGetDecimalCore(out decimal value, ReadOnlySpan span) + { if (Utf8Parser.TryParse(span, out decimal tmp, out int bytesConsumed, _numberFormat) && span.Length == bytesConsumed) { @@ -818,6 +1025,11 @@ public bool TryGetDateTime(out DateTime value) throw ThrowHelper.GetInvalidOperationException_ExpectedString(TokenType); } + return TryGetDateTimeCore(out value); + } + + internal bool TryGetDateTimeCore(out DateTime value) + { ReadOnlySpan span = stackalloc byte[0]; if (HasValueSequence) @@ -881,6 +1093,11 @@ public bool TryGetDateTimeOffset(out DateTimeOffset value) throw ThrowHelper.GetInvalidOperationException_ExpectedString(TokenType); } + return TryGetDateTimeOffsetCore(out value); + } + + internal bool TryGetDateTimeOffsetCore(out DateTimeOffset value) + { ReadOnlySpan span = stackalloc byte[0]; if (HasValueSequence) @@ -945,6 +1162,11 @@ public bool TryGetGuid(out Guid value) throw ThrowHelper.GetInvalidOperationException_ExpectedString(TokenType); } + return TryGetGuidCore(out value); + } + + internal bool TryGetGuidCore(out Guid value) + { ReadOnlySpan span = stackalloc byte[0]; if (HasValueSequence) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs index cb43f7b936049..b9d2723495137 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs @@ -2552,5 +2552,18 @@ private string DebugTokenType JsonTokenType.True => nameof(JsonTokenType.True), _ => ((byte)TokenType).ToString() }; + + private ReadOnlySpan GetUnescapedSpan() + { + ReadOnlySpan span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan; + if (_stringHasEscaping) + { + int idx = span.IndexOf(JsonConstants.BackSlash); + Debug.Assert(idx != -1); + span = JsonReaderHelper.GetUnescapedSpan(span, idx); + } + + return span; + } } } 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 1c4b0fae4ff12..afd541ccd7d0e 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 @@ -10,13 +10,14 @@ namespace System.Text.Json.Serialization.Converters /// /// Default base class implementation of JsonDictionaryConverter{TCollection} . /// - internal abstract class DictionaryDefaultConverter + internal abstract class DictionaryDefaultConverter : JsonDictionaryConverter + where TKey : notnull { /// /// When overridden, adds the value to the collection. /// - protected abstract void Add(in TValue value, JsonSerializerOptions options, ref ReadStack state); + protected abstract void Add(TKey key, in TValue value, JsonSerializerOptions options, ref ReadStack state); /// /// When overridden, converts the temporary collection held in state.Current.ReturnValue to the final collection. @@ -31,37 +32,25 @@ protected virtual void CreateCollection(ref Utf8JsonReader reader, ref ReadStack internal override Type ElementType => typeof(TValue); - protected static JsonConverter GetElementConverter(ref ReadStack state) - { - JsonConverter converter = (JsonConverter)state.Current.JsonClassInfo.ElementClassInfo!.PropertyInfoForClassInfo.ConverterBase; - Debug.Assert(converter != null); // It should not be possible to have a null converter at this point. - - return converter; - } - - protected string GetKeyName(string key, ref WriteStack state, JsonSerializerOptions options) - { - if (options.DictionaryKeyPolicy != null && !state.Current.IgnoreDictionaryKeyPolicy) - { - key = options.DictionaryKeyPolicy.ConvertName(key); - - if (key == null) - { - ThrowHelper.ThrowInvalidOperationException_NamingPolicyReturnNull(options.DictionaryKeyPolicy); - } - } + protected Type KeyType = typeof(TKey); + // For string keys we don't use a key converter + // in order to avoid performance regression on already supported types. + protected bool IsStringKey = typeof(TKey) == typeof(string); - return key; - } + protected JsonConverter? _keyConverter; + protected JsonConverter? _valueConverter; - protected static JsonConverter GetValueConverter(ref WriteStack state) + protected static JsonConverter GetValueConverter(JsonClassInfo classInfo) { - JsonConverter converter = (JsonConverter)state.Current.DeclaredJsonPropertyInfo!.ConverterBase; + JsonConverter converter = (JsonConverter)classInfo.ElementClassInfo!.PropertyInfoForClassInfo.ConverterBase; Debug.Assert(converter != null); // It should not be possible to have a null converter at this point. return converter; } + protected static JsonConverter GetKeyConverter(Type keyType, JsonSerializerOptions options) + => (JsonConverter)options.GetDictionaryKeyConverter(keyType); + internal sealed override bool OnTryRead( ref Utf8JsonReader reader, Type typeToConvert, @@ -80,8 +69,8 @@ internal sealed override bool OnTryRead( CreateCollection(ref reader, ref state); - JsonConverter elementConverter = GetElementConverter(ref state); - if (elementConverter.CanUseDirectReadOrWrite) + JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo); + if (valueConverter.CanUseDirectReadOrWrite) { // Process all elements. while (true) @@ -97,12 +86,12 @@ internal sealed override bool OnTryRead( // Read method would have thrown if otherwise. Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); - state.Current.JsonPropertyNameAsString = reader.GetString(); + TKey key = ReadDictionaryKey(ref reader, ref state); // Read the value and add. reader.ReadWithVerify(); - TValue element = elementConverter.Read(ref reader, typeof(TValue), options); - Add(element!, options, ref state); + TValue element = valueConverter.Read(ref reader, typeof(TValue), options); + Add(key, element!, options, ref state); } } else @@ -121,13 +110,13 @@ internal sealed override bool OnTryRead( // Read method would have thrown if otherwise. Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); - state.Current.JsonPropertyNameAsString = reader.GetString(); + TKey key = ReadDictionaryKey(ref reader, ref state); 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); + valueConverter.TryRead(ref reader, typeof(TValue), options, ref state, out TValue element); + Add(key, element!, options, ref state); } } } @@ -184,7 +173,7 @@ internal sealed override bool OnTryRead( } // Process all elements. - JsonConverter elementConverter = GetElementConverter(ref state); + JsonConverter elementConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo); while (true) { if (state.Current.PropertyState == StackFramePropertyState.None) @@ -212,7 +201,6 @@ internal sealed override bool OnTryRead( state.Current.PropertyState = StackFramePropertyState.Name; - // Verify property doesn't contain metadata. if (preserveReferences) { ReadOnlySpan propertyName = reader.GetSpan(); @@ -222,7 +210,7 @@ internal sealed override bool OnTryRead( } } - state.Current.JsonPropertyNameAsString = reader.GetString(); + state.Current.DictionaryKey = ReadDictionaryKey(ref reader, ref state); } if (state.Current.PropertyState < StackFramePropertyState.ReadValue) @@ -246,7 +234,8 @@ internal sealed override bool OnTryRead( return false; } - Add(element!, options, ref state); + TKey key = (TKey)state.Current.DictionaryKey!; + Add(key, element!, options, ref state); state.Current.EndElement(); } } @@ -255,6 +244,29 @@ internal sealed override bool OnTryRead( ConvertCollection(ref state, options); value = (TCollection)state.Current.ReturnValue!; return true; + + TKey ReadDictionaryKey(ref Utf8JsonReader reader, ref ReadStack state) + { + TKey key; + string unescapedPropertyNameAsString; + + // Special case string to avoid calling GetString twice and save one allocation. + if (IsStringKey) + { + unescapedPropertyNameAsString = reader.GetString()!; + key = (TKey)(object)unescapedPropertyNameAsString; + } + else + { + JsonConverter keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options); + key = keyConverter.ReadWithQuotes(ref reader); + unescapedPropertyNameAsString = reader.GetString()!; + } + + // Copy key name for JSON Path support in case of error. + state.Current.JsonPropertyNameAsString = unescapedPropertyNameAsString; + return key; + } } internal sealed override bool OnTryWrite( 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/DictionaryOfTKeyTValueConverter.cs similarity index 66% 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/DictionaryOfTKeyTValueConverter.cs index 13bcea6c7d10b..b34294f9e002f 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/DictionaryOfTKeyTValueConverter.cs @@ -10,13 +10,13 @@ namespace System.Text.Json.Serialization.Converters /// Converter for Dictionary{string, 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(in TValue value, JsonSerializerOptions options, ref ReadStack state) + protected override void Add(TKey key, in TValue value, JsonSerializerOptions options, ref ReadStack state) { - string key = state.Current.JsonPropertyNameAsString!; ((TCollection)state.Current.ReturnValue!)[key] = value; } @@ -36,7 +36,7 @@ protected internal override bool OnWriteResume( JsonSerializerOptions options, ref WriteStack state) { - Dictionary.Enumerator enumerator; + Dictionary.Enumerator enumerator; if (state.Current.CollectionEnumerator == null) { enumerator = value.GetEnumerator(); @@ -47,18 +47,20 @@ protected internal override bool OnWriteResume( } else { - enumerator = (Dictionary.Enumerator)state.Current.CollectionEnumerator; + enumerator = (Dictionary.Enumerator)state.Current.CollectionEnumerator; } - JsonConverter converter = GetValueConverter(ref state); - if (!state.SupportContinuation && converter.CanUseDirectReadOrWrite) + JsonConverter keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options); + JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo); + 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); - converter.Write(writer, enumerator.Current.Value, options); + TKey key = enumerator.Current.Key; + keyConverter.WriteWithQuotes(writer, key, options, ref state); + + valueConverter.Write(writer, enumerator.Current.Value, options); } while (enumerator.MoveNext()); } else @@ -74,12 +76,13 @@ protected internal override bool OnWriteResume( if (state.Current.PropertyState < StackFramePropertyState.Name) { state.Current.PropertyState = StackFramePropertyState.Name; - string key = GetKeyName(enumerator.Current.Key, ref state, options); - writer.WritePropertyName(key); + + TKey key = enumerator.Current.Key; + keyConverter.WriteWithQuotes(writer, key, options, ref state); } TValue element = enumerator.Current.Value; - 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 4927187b0af77..1e586c093fa1b 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 @@ -12,15 +12,19 @@ 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(in object? value, JsonSerializerOptions options, ref ReadStack state) + protected override void Add(string key, in object? value, JsonSerializerOptions options, ref ReadStack state) { - string key = state.Current.JsonPropertyNameAsString!; ((IDictionary)state.Current.ReturnValue!)[key] = value; } + private JsonConverter? _objectConverter; + + private static JsonConverter GetObjectKeyConverter(JsonSerializerOptions options) + => (JsonConverter)options.GetDictionaryKeyConverter(typeof(object)); + protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state) { JsonClassInfo classInfo = state.Current.JsonClassInfo; @@ -68,7 +72,7 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio enumerator = (IDictionaryEnumerator)state.Current.CollectionEnumerator; } - JsonConverter converter = GetValueConverter(ref state); + JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo); do { if (ShouldFlush(writer, ref state)) @@ -80,20 +84,24 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio if (state.Current.PropertyState < StackFramePropertyState.Name) { state.Current.PropertyState = StackFramePropertyState.Name; - - if (enumerator.Key is string key) + object key = enumerator.Key; + // Optimize for string since that's the hot path. + if (key is string keyString) { - key = GetKeyName(key, ref state, options); - writer.WritePropertyName(key); + JsonConverter stringKeyConverter = _keyConverter ??= GetKeyConverter(KeyType, options); + stringKeyConverter.WriteWithQuotes(writer, keyString, options, ref state); } else { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.DeclaredJsonPropertyInfo!.RuntimePropertyType!); + // IDictionary is a special case since it has polymorphic object semantics on serialization + // but needs to use JsonConverter on deserialization. + JsonConverter objectKeyConverter = _objectConverter ??= GetObjectKeyConverter(options); + objectKeyConverter.WriteWithQuotes(writer, key, options, ref state); } } object? element = enumerator.Value; - 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/IDictionaryOfStringTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfTKeyTValueConverter.cs similarity index 76% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfStringTValueConverter.cs rename to src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfTKeyTValueConverter.cs index 72a12ade8ab00..158feccb0270c 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/IDictionaryOfTKeyTValueConverter.cs @@ -10,13 +10,13 @@ namespace System.Text.Json.Serialization.Converters /// Converter for System.Collections.Generic.IDictionary{string, TValue} that /// (de)serializes as a JSON object with properties representing the dictionary element key and value. /// - internal sealed class IDictionaryOfStringTValueConverter - : DictionaryDefaultConverter - where TCollection : IDictionary + internal sealed class IDictionaryOfTKeyTValueConverter + : DictionaryDefaultConverter + where TCollection : IDictionary + where TKey : notnull { - protected override void Add(in TValue value, JsonSerializerOptions options, ref ReadStack state) + protected override void Add(TKey key, in TValue value, JsonSerializerOptions options, ref ReadStack state) { - string key = state.Current.JsonPropertyNameAsString!; ((TCollection)state.Current.ReturnValue!)[key] = value; } @@ -57,7 +57,7 @@ protected internal override bool OnWriteResume( JsonSerializerOptions options, ref WriteStack state) { - IEnumerator> enumerator; + IEnumerator> enumerator; if (state.Current.CollectionEnumerator == null) { enumerator = value.GetEnumerator(); @@ -68,10 +68,11 @@ protected internal override bool OnWriteResume( } else { - enumerator = (IEnumerator>)state.Current.CollectionEnumerator; + enumerator = (IEnumerator>)state.Current.CollectionEnumerator; } - JsonConverter converter = GetValueConverter(ref state); + JsonConverter keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options); + JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo); do { if (ShouldFlush(writer, ref state)) @@ -83,12 +84,12 @@ protected internal override bool OnWriteResume( if (state.Current.PropertyState < StackFramePropertyState.Name) { state.Current.PropertyState = StackFramePropertyState.Name; - string key = GetKeyName(enumerator.Current.Key, ref state, options); - writer.WritePropertyName(key); + TKey key = enumerator.Current.Key; + keyConverter.WriteWithQuotes(writer, key, options, ref state); } TValue element = enumerator.Current.Value; - if (!converter.TryWrite(writer, element, options, ref state)) + if (!valueConverter.TryWrite(writer, element, options, ref state)) { state.Current.CollectionEnumerator = enumerator; return false; @@ -106,7 +107,7 @@ internal override Type RuntimeType { if (TypeToConvert.IsAbstract || TypeToConvert.IsInterface) { - return typeof(Dictionary); + return typeof(Dictionary); } return TypeToConvert; 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 56012689bc5e6..7b1cdd130f50d 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 @@ -28,25 +28,26 @@ public override bool CanConvert(Type typeToConvert) [DynamicDependency("#ctor", typeof(ArrayConverter<,>))] [DynamicDependency("#ctor", typeof(ConcurrentQueueOfTConverter<,>))] [DynamicDependency("#ctor", typeof(ConcurrentStackOfTConverter<,>))] - [DynamicDependency("#ctor", typeof(DictionaryOfStringTValueConverter<,>))] + [DynamicDependency("#ctor", typeof(DictionaryOfTKeyTValueConverter<,,>))] [DynamicDependency("#ctor", typeof(ICollectionOfTConverter<,>))] - [DynamicDependency("#ctor", typeof(IDictionaryOfStringTValueConverter<,>))] + [DynamicDependency("#ctor", typeof(IDictionaryOfTKeyTValueConverter<,,>))] [DynamicDependency("#ctor", typeof(IEnumerableOfTConverter<,>))] [DynamicDependency("#ctor", typeof(IEnumerableWithAddMethodConverter<>))] [DynamicDependency("#ctor", typeof(IListConverter<>))] [DynamicDependency("#ctor", typeof(IListOfTConverter<,>))] - [DynamicDependency("#ctor", typeof(ImmutableDictionaryOfStringTValueConverter<,>))] + [DynamicDependency("#ctor", typeof(ImmutableDictionaryOfTKeyTValueConverter<,,>))] [DynamicDependency("#ctor", typeof(ImmutableEnumerableOfTConverter<,>))] - [DynamicDependency("#ctor", typeof(IReadOnlyDictionaryOfStringTValueConverter<,>))] + [DynamicDependency("#ctor", typeof(IReadOnlyDictionaryOfTKeyTValueConverter<,,>))] [DynamicDependency("#ctor", typeof(ISetOfTConverter<,>))] [DynamicDependency("#ctor", typeof(ListOfTConverter<,>))] [DynamicDependency("#ctor", typeof(QueueOfTConverter<,>))] [DynamicDependency("#ctor", typeof(StackOfTConverter<,>))] public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - Type converterType = null!; + Type converterType; Type[] genericArgs; Type? elementType = null; + Type? dictionaryKeyType = null; Type? actualTypeToConvert; // Array @@ -67,61 +68,37 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer 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 - { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(typeToConvert); - } + converterType = typeof(DictionaryOfTKeyTValueConverter<,,>); + dictionaryKeyType = genericArgs[0]; + elementType = genericArgs[1]; } - // Immutable dictionaries from System.Collections.Immutable, e.g. ImmutableDictionary + // Immutable dictionaries from System.Collections.Immutable, e.g. ImmutableDictionary else if (typeToConvert.IsImmutableDictionaryType()) { genericArgs = typeToConvert.GetGenericArguments(); - if (genericArgs[0] == typeof(string)) - { - converterType = typeof(ImmutableDictionaryOfStringTValueConverter<,>); - elementType = genericArgs[1]; - } - else - { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(typeToConvert); - } + converterType = typeof(ImmutableDictionaryOfTKeyTValueConverter<,,>); + dictionaryKeyType = genericArgs[0]; + elementType = genericArgs[1]; } - // IDictionary or deriving from IDictionary + // IDictionary or deriving from IDictionary else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IDictionary<,>))) != null) { genericArgs = actualTypeToConvert.GetGenericArguments(); - if (genericArgs[0] == typeof(string)) - { - converterType = typeof(IDictionaryOfStringTValueConverter<,>); - elementType = genericArgs[1]; - } - else - { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(typeToConvert); - } + converterType = typeof(IDictionaryOfTKeyTValueConverter<,,>); + dictionaryKeyType = genericArgs[0]; + elementType = genericArgs[1]; } - // IReadOnlyDictionary or deriving from IReadOnlyDictionary + // IReadOnlyDictionary or deriving from IReadOnlyDictionary else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IReadOnlyDictionary<,>))) != null) { genericArgs = actualTypeToConvert.GetGenericArguments(); - if (genericArgs[0] == typeof(string)) - { - converterType = typeof(IReadOnlyDictionaryOfStringTValueConverter<,>); - elementType = genericArgs[1]; - } - else - { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(typeToConvert); - } + converterType = typeof(IReadOnlyDictionaryOfTKeyTValueConverter<,,>); + dictionaryKeyType = genericArgs[0]; + elementType = genericArgs[1]; } // Immutable non-dictionaries from System.Collections.Immutable, e.g. ImmutableStack else if (typeToConvert.IsImmutableEnumerableType()) @@ -211,17 +188,21 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer converterType = typeof(IEnumerableConverter<>); } - Debug.Assert(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!); + } JsonConverter 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/IReadOnlyDictionaryOfTKeyTValueConverter.cs similarity index 65% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfStringTValueConverter.cs rename to src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfTKeyTValueConverter.cs index 0d05ff3a1869d..b4cc76d8bd6b8 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/IReadOnlyDictionaryOfTKeyTValueConverter.cs @@ -6,14 +6,14 @@ namespace System.Text.Json.Serialization.Converters { - internal sealed class IReadOnlyDictionaryOfStringTValueConverter - : DictionaryDefaultConverter - where TCollection : IReadOnlyDictionary + internal sealed class IReadOnlyDictionaryOfTKeyTValueConverter + : DictionaryDefaultConverter + where TCollection : IReadOnlyDictionary + where TKey : notnull { - protected override void Add(in TValue value, JsonSerializerOptions options, ref ReadStack state) + protected override void Add(TKey key, in TValue value, JsonSerializerOptions options, ref ReadStack state) { - string key = state.Current.JsonPropertyNameAsString!; - ((Dictionary)state.Current.ReturnValue!)[key] = value; + ((Dictionary)state.Current.ReturnValue!)[key] = value; } protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state) @@ -28,7 +28,7 @@ protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStac protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) { - IEnumerator> enumerator; + IEnumerator> enumerator; if (state.Current.CollectionEnumerator == null) { enumerator = value.GetEnumerator(); @@ -39,10 +39,11 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio } else { - enumerator = (Dictionary.Enumerator)state.Current.CollectionEnumerator; + enumerator = (Dictionary.Enumerator)state.Current.CollectionEnumerator; } - JsonConverter converter = GetValueConverter(ref state); + JsonConverter keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options); + JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo); do { if (ShouldFlush(writer, ref state)) @@ -54,12 +55,13 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio if (state.Current.PropertyState < StackFramePropertyState.Name) { state.Current.PropertyState = StackFramePropertyState.Name; - string key = GetKeyName(enumerator.Current.Key, ref state, options); - writer.WritePropertyName(key); + + TKey key = enumerator.Current.Key; + keyConverter.WriteWithQuotes(writer, key, options, ref state); } TValue element = enumerator.Current.Value; - if (!converter.TryWrite(writer, element, options, ref state)) + if (!valueConverter.TryWrite(writer, element, options, ref state)) { state.Current.CollectionEnumerator = enumerator; return false; @@ -71,6 +73,6 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio return true; } - internal override Type RuntimeType => typeof(Dictionary); + internal override Type RuntimeType => typeof(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/ImmutableDictionaryOfTKeyTValueConverter.cs similarity index 70% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfStringTValueConverter.cs rename to src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfTKeyTValueConverter.cs index 4e6e1caa818f3..7d5b429d287d9 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/ImmutableDictionaryOfTKeyTValueConverter.cs @@ -6,14 +6,14 @@ namespace System.Text.Json.Serialization.Converters { - internal sealed class ImmutableDictionaryOfStringTValueConverter - : DictionaryDefaultConverter - where TCollection : IReadOnlyDictionary + internal sealed class ImmutableDictionaryOfTKeyTValueConverter + : DictionaryDefaultConverter + where TCollection : IReadOnlyDictionary + where TKey : notnull { - protected override void Add(in TValue value, JsonSerializerOptions options, ref ReadStack state) + protected override void Add(TKey key, in TValue value, JsonSerializerOptions options, ref ReadStack state) { - string key = state.Current.JsonPropertyNameAsString!; - ((Dictionary)state.Current.ReturnValue!)[key] = value; + ((Dictionary)state.Current.ReturnValue!)[key] = value; } internal override bool CanHaveIdMetadata => false; @@ -39,7 +39,7 @@ protected override void ConvertCollection(ref ReadStack state, JsonSerializerOpt protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state) { - IEnumerator> enumerator; + IEnumerator> enumerator; if (state.Current.CollectionEnumerator == null) { enumerator = value.GetEnumerator(); @@ -50,10 +50,11 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio } else { - enumerator = (IEnumerator>)state.Current.CollectionEnumerator; + enumerator = (IEnumerator>)state.Current.CollectionEnumerator; } - JsonConverter converter = GetValueConverter(ref state); + JsonConverter keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options); + JsonConverter valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo); do { if (ShouldFlush(writer, ref state)) @@ -65,12 +66,13 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollectio if (state.Current.PropertyState < StackFramePropertyState.Name) { state.Current.PropertyState = StackFramePropertyState.Name; - string key = GetKeyName(enumerator.Current.Key, ref state, options); - writer.WritePropertyName(key); + + TKey key = enumerator.Current.Key; + keyConverter.WriteWithQuotes(writer, key, options, ref state); } TValue element = enumerator.Current.Value; - 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/Value/BooleanConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/BooleanConverter.cs index 04da6ed163fc5..32aba9afcf3ff 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/BooleanConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/BooleanConverter.cs @@ -2,6 +2,8 @@ // 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; + namespace System.Text.Json.Serialization.Converters { internal sealed class BooleanConverter : JsonConverter @@ -15,5 +17,22 @@ public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOpti { writer.WriteBooleanValue(value); } + + internal override bool ReadWithQuotes(ref Utf8JsonReader reader) + { + ReadOnlySpan propertyName = reader.GetSpan(); + if (Utf8Parser.TryParse(propertyName, out bool value, out int bytesConsumed) + && propertyName.Length == bytesConsumed) + { + return value; + } + + throw ThrowHelper.GetFormatException(DataType.Boolean); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, bool value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs index 38b8211ee3619..1280d2abf0fc5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs @@ -15,5 +15,15 @@ public override void Write(Utf8JsonWriter writer, byte value, JsonSerializerOpti { writer.WriteNumberValue(value); } + + internal override byte ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetByteWithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, byte value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/CharConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/CharConverter.cs index 5b259892c4d9c..41901e39560c3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/CharConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/CharConverter.cs @@ -25,6 +25,20 @@ public override void Write(Utf8JsonWriter writer, char value, JsonSerializerOpti MemoryMarshal.CreateSpan(ref value, 1) #else value.ToString() +#endif + ); + } + + internal override char ReadWithQuotes(ref Utf8JsonReader reader) + => Read(ref reader, default!, default!); + + internal override void WriteWithQuotes(Utf8JsonWriter writer, char value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName( +#if BUILDING_INBOX_LIBRARY + MemoryMarshal.CreateSpan(ref value, 1) +#else + value.ToString() #endif ); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeConverter.cs index e5dbbd588271f..00754b5b8ed82 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeConverter.cs @@ -15,5 +15,15 @@ public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializer { writer.WriteStringValue(value); } + + internal override DateTime ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetDateTimeNoValidation(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeOffsetConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeOffsetConverter.cs index 927911cb7bd54..e6eada9de0462 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeOffsetConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeOffsetConverter.cs @@ -15,5 +15,15 @@ public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSeri { writer.WriteStringValue(value); } + + internal override DateTimeOffset ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetDateTimeOffsetNoValidation(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs index 326df60822c75..0f2350fa34175 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs @@ -15,5 +15,15 @@ public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerO { writer.WriteNumberValue(value); } + + internal override decimal ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetDecimalWithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs index 4a2acd60865c9..5814f89c293a6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs @@ -15,5 +15,15 @@ public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOp { writer.WriteNumberValue(value); } + + internal override double ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetDoubleWithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, double value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs index 41f7302f7db55..c4b4dd75468f8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs @@ -83,15 +83,7 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial return default; } - // Try parsing case sensitive first - string? enumString = reader.GetString(); - if (!Enum.TryParse(enumString, out T value) - && !Enum.TryParse(enumString, ignoreCase: true, out value)) - { - ThrowHelper.ThrowJsonException(); - return default; - } - return value; + return ReadWithQuotes(ref reader); } if (token != JsonTokenType.Number || !_converterOptions.HasFlag(EnumConverterOptions.AllowNumbers)) @@ -317,5 +309,91 @@ private string FormatEnumValueToString(string value, JavaScriptEncoder? encoder) return converted; } + + internal override T ReadWithQuotes(ref Utf8JsonReader reader) + { + string? enumString = reader.GetString(); + + // Try parsing case sensitive first + if (!Enum.TryParse(enumString, out T value) + && !Enum.TryParse(enumString, ignoreCase: true, out value)) + { + ThrowHelper.ThrowJsonException(); + } + + return value; + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state) + { + // An EnumConverter that invokes this method + // can only be created by JsonSerializerOptions.GetDictionaryKeyConverter + // hence no naming policy is expected. + Debug.Assert(_namingPolicy == null); + + ulong key = ConvertToUInt64(value); + + if (_nameCache.TryGetValue(key, out JsonEncodedText formatted)) + { + writer.WritePropertyName(formatted); + return; + } + + string original = value.ToString(); + if (IsValidIdentifier(original)) + { + // We are dealing with a combination of flag constants since + // all constant values were cached during warm-up. + JavaScriptEncoder? encoder = options.Encoder; + + if (_nameCache.Count < NameCacheSizeSoftLimit) + { + formatted = JsonEncodedText.Encode(original, encoder); + + writer.WritePropertyName(formatted); + + _nameCache.TryAdd(key, formatted); + } + else + { + // We also do not create a JsonEncodedText instance here because passing the string + // directly to the writer is cheaper than creating one and not caching it for reuse. + writer.WritePropertyName(original); + } + + return; + } + + switch (s_enumTypeCode) + { + case TypeCode.Int32: + writer.WritePropertyName(Unsafe.As(ref value)); + break; + case TypeCode.UInt32: + writer.WritePropertyName(Unsafe.As(ref value)); + break; + case TypeCode.UInt64: + writer.WritePropertyName(Unsafe.As(ref value)); + break; + case TypeCode.Int64: + writer.WritePropertyName(Unsafe.As(ref value)); + break; + case TypeCode.Int16: + writer.WritePropertyName(Unsafe.As(ref value)); + break; + case TypeCode.UInt16: + writer.WritePropertyName(Unsafe.As(ref value)); + break; + case TypeCode.Byte: + writer.WritePropertyName(Unsafe.As(ref value)); + break; + case TypeCode.SByte: + writer.WritePropertyName(Unsafe.As(ref value)); + break; + default: + ThrowHelper.ThrowJsonException(); + break; + } + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/GuidConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/GuidConverter.cs index 3c7c010098ffd..6328144fc6bf7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/GuidConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/GuidConverter.cs @@ -15,5 +15,15 @@ public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOpti { writer.WriteStringValue(value); } + + internal override Guid ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetGuidNoValidation(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs index f68e23459af7e..8104a987625b2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs @@ -2,6 +2,8 @@ // 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 sealed class Int16Converter : JsonConverter @@ -15,5 +17,15 @@ public override void Write(Utf8JsonWriter writer, short value, JsonSerializerOpt { writer.WriteNumberValue(value); } + + internal override short ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetInt16WithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, short value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs index abd9089c65873..f6f6712c4dff7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs @@ -15,5 +15,15 @@ public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptio { writer.WriteNumberValue(value); } + + internal override int ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetInt32WithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, int value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs index 0ea30c22a5644..a8817af4c924c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs @@ -15,5 +15,15 @@ public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOpti { writer.WriteNumberValue(value); } + + internal override long ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetInt64WithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, long value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs index acbae39e3edc3..fe680f6cde506 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs @@ -18,5 +18,25 @@ public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOp { throw new InvalidOperationException(); } + + internal override object ReadWithQuotes(ref Utf8JsonReader reader) + => throw new NotSupportedException(); + + 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); + if (runtimeConverter == this) + { + ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(runtimeType); + } + + return runtimeConverter; + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs index bbc004a29c07a..f4a2339e4831e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs @@ -15,5 +15,15 @@ public override void Write(Utf8JsonWriter writer, sbyte value, JsonSerializerOpt { writer.WriteNumberValue(value); } + + internal override sbyte ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetSByteWithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, sbyte value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs index c7c8b180ff5a8..1653a65e7e481 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs @@ -15,5 +15,15 @@ public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOpt { writer.WriteNumberValue(value); } + + internal override float ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetSingleWithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, float value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/StringConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/StringConverter.cs index bd420a35e719c..d0d958ab7a44f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/StringConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/StringConverter.cs @@ -2,9 +2,11 @@ // 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 sealed class StringConverter : JsonConverter + internal sealed class StringConverter : JsonConverter { public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -15,5 +17,25 @@ public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerO { writer.WriteStringValue(value); } + + internal override string ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetString()!; + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, string value, JsonSerializerOptions options, ref WriteStack state) + { + if (options.DictionaryKeyPolicy != null && !state.Current.IgnoreDictionaryKeyPolicy) + { + value = options.DictionaryKeyPolicy.ConvertName(value); + + if (value == null) + { + ThrowHelper.ThrowInvalidOperationException_NamingPolicyReturnNull(options.DictionaryKeyPolicy); + } + } + + writer.WritePropertyName(value); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs index 56f24b06a5345..44f20a21e5d9c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs @@ -15,5 +15,15 @@ public override void Write(Utf8JsonWriter writer, ushort value, JsonSerializerOp { writer.WriteNumberValue(value); } + + internal override ushort ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetUInt16WithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, ushort value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs index 9dfc006ac6836..cea8390da69fc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs @@ -15,5 +15,15 @@ public override void Write(Utf8JsonWriter writer, uint value, JsonSerializerOpti { writer.WriteNumberValue(value); } + + internal override uint ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetUInt32WithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, uint value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs index af26a458c86c9..3f07e3623a22f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs @@ -15,5 +15,15 @@ public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOpt { writer.WriteNumberValue(value); } + + internal override ulong ReadWithQuotes(ref Utf8JsonReader reader) + { + return reader.GetUInt64WithQuotes(); + } + + internal override void WriteWithQuotes(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options, ref WriteStack state) + { + writer.WritePropertyName(value); + } } } 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 cc73208798ab4..25b43c861342a 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 @@ -71,6 +71,11 @@ internal bool ShouldFlush(Utf8JsonWriter writer, ref WriteStack state) /// internal abstract bool WriteCoreAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state); + /// + /// Loosely-typed WriteWithQuotes() that forwards to strongly-typed WriteWithQuotes(). + /// + internal abstract void WriteWithQuotesAsObject(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state); + // Whether a type (ClassType.Object) is deserialized using a parameterized constructor. internal virtual bool ConstructorIsParameterized => false; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs index 5186002fe4077..0250731debbfc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs @@ -111,5 +111,15 @@ internal sealed override bool WriteCoreAsObject( throw new InvalidOperationException(); } + + internal sealed override void WriteWithQuotesAsObject( + Utf8JsonWriter writer, object value, + JsonSerializerOptions options, + ref WriteStack state) + { + Debug.Fail("We should never get here."); + + throw new InvalidOperationException(); + } } } 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 5689440414765..bfdcaaf40b9d8 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 @@ -335,8 +335,6 @@ internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, Json Debug.Assert(this is JsonDictionaryConverter); - state.Current.PolymorphicJsonPropertyInfo = state.Current.DeclaredJsonPropertyInfo!.RuntimeClassInfo.ElementClassInfo!.PropertyInfoForClassInfo; - if (writer.CurrentDepth >= options.EffectiveMaxDepth) { ThrowHelper.ThrowJsonException_SerializerCycleDetected(options.EffectiveMaxDepth); @@ -433,5 +431,14 @@ internal void VerifyWrite(int originalDepth, Utf8JsonWriter writer) /// The value to convert. /// The being used. public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options); + + internal virtual T ReadWithQuotes(ref Utf8JsonReader reader) + => throw new InvalidOperationException(); + + internal virtual void WriteWithQuotes(Utf8JsonWriter writer, [DisallowNull] T value, JsonSerializerOptions options, ref WriteStack state) + => throw new InvalidOperationException(); + + internal sealed override void WriteWithQuotesAsObject(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state) + => WriteWithQuotes(writer, (T)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 bc1f7981c30fd..fc63f11fb7ca0 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 @@ -70,7 +70,72 @@ private static Dictionary GetDefaultSimpleConverters() return converters; void Add(JsonConverter converter) => - converters.Add(converter.TypeToConvert!, converter); + 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() + => (JsonConverter)Activator.CreateInstance( + typeof(EnumConverter<>).MakeGenericType(keyType), + BindingFlags.Instance | BindingFlags.Public, + binder: null, + new object[] { EnumConverterOptions.AllowStrings | EnumConverterOptions.AllowNumbers, this }, + culture: null)!; + } + + private ConcurrentDictionary? _dictionaryKeyConverters; + + private static ConcurrentDictionary GetDictionaryKeyConverters() + { + const int NumberOfConverters = 18; + var converters = new ConcurrentDictionary(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; } /// 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 d9a59477faa2b..74ff1bca6710d 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 @@ -15,9 +15,14 @@ internal struct ReadStackFrame public StackFramePropertyState PropertyState; public bool UseExtensionProperty; - // 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. + // Support JSON Path on exceptions and non-string Dictionary keys. + // This is Utf8 since we don't want to convert to string until an exception is thown. + // For dictionary keys we don't want to convert to TKey until we have both key and value when parsing the dictionary elements on stream cases. + public byte[]? JsonPropertyName; + public string? JsonPropertyNameAsString; // This is used for string dictionary keys and re-entry cases that specify a property name. + + // Stores the non-string dictionary keys for continuation. + public object? DictionaryKey; // Validation state. public int OriginalDepth; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index 71d3f2ada0f49..a27c198c97e01 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -40,6 +40,13 @@ public static void ThrowNotSupportedException_ConstructorMaxOf64Parameters(Const throw new NotSupportedException(SR.Format(SR.ConstructorMaxOf64Parameters, constructorInfo, type)); } + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowNotSupportedException_DictionaryKeyTypeNotSupported(Type keyType) + { + throw new NotSupportedException(SR.Format(SR.DictionaryKeyTypeNotSupported, keyType)); + } + [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs index 9dffc288b1a55..16b2471587091 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs @@ -612,6 +612,9 @@ public static FormatException GetFormatException(DataType dateType) switch (dateType) { + case DataType.Boolean: + message = SR.FormatBoolean; + break; case DataType.DateTime: message = SR.FormatDateTime; break; @@ -702,6 +705,7 @@ internal enum NumericType internal enum DataType { + Boolean, DateTime, DateTimeOffset, Base64String, diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTime.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTime.cs index da5219b98771d..0ed995b641d7b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTime.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTime.cs @@ -375,5 +375,12 @@ private void WriteStringIndented(ReadOnlySpan escapedPropertyName, DateTim output[BytesPending++] = JsonConstants.Quote; } + + internal void WritePropertyName(DateTime value) + { + Span buffer = stackalloc byte[JsonConstants.MaximumFormatDateTimeOffsetLength]; + JsonWriterHelper.WriteDateTimeTrimmed(buffer, value, out int bytesWritten); + WritePropertyNameUnescaped(buffer.Slice(0, bytesWritten)); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTimeOffset.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTimeOffset.cs index 37cd479a2b735..62ba78f9e0df5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTimeOffset.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTimeOffset.cs @@ -374,5 +374,12 @@ private void WriteStringIndented(ReadOnlySpan escapedPropertyName, DateTim output[BytesPending++] = JsonConstants.Quote; } + + internal void WritePropertyName(DateTimeOffset value) + { + Span buffer = stackalloc byte[JsonConstants.MaximumFormatDateTimeOffsetLength]; + JsonWriterHelper.WriteDateTimeOffsetTrimmed(buffer, value, out int bytesWritten); + WritePropertyNameUnescaped(buffer.Slice(0, bytesWritten)); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Decimal.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Decimal.cs index e4b05e79b30fd..626098736f94e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Decimal.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Decimal.cs @@ -362,5 +362,13 @@ private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, decimal Debug.Assert(result); BytesPending += bytesWritten; } + + internal void WritePropertyName(decimal value) + { + Span utf8PropertyName = stackalloc byte[JsonConstants.MaximumFormatDecimalLength]; + bool result = Utf8Formatter.TryFormat(value, utf8PropertyName, out int bytesWritten); + Debug.Assert(result); + WritePropertyNameUnescaped(utf8PropertyName.Slice(0, bytesWritten)); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Double.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Double.cs index f8ae05cc04c20..63047537ab372 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Double.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Double.cs @@ -366,5 +366,14 @@ private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, double Debug.Assert(result); BytesPending += bytesWritten; } + + internal void WritePropertyName(double value) + { + JsonWriterHelper.ValidateDouble(value); + Span utf8PropertyName = stackalloc byte[JsonConstants.MaximumFormatDoubleLength]; + bool result = TryFormatDouble(value, utf8PropertyName, out int bytesWritten); + Debug.Assert(result); + WritePropertyNameUnescaped(utf8PropertyName.Slice(0, bytesWritten)); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Float.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Float.cs index 60ba0b03872d0..2af8936863bc5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Float.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Float.cs @@ -366,5 +366,13 @@ private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, float v Debug.Assert(result); BytesPending += bytesWritten; } + + internal void WritePropertyName(float value) + { + Span utf8PropertyName = stackalloc byte[JsonConstants.MaximumFormatSingleLength]; + bool result = TryFormatSingle(value, utf8PropertyName, out int bytesWritten); + Debug.Assert(result); + WritePropertyNameUnescaped(utf8PropertyName.Slice(0, bytesWritten)); + } } } 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 b18a107c78b35..062e1ea758e6e 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,13 @@ 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); + WritePropertyNameUnescaped(utf8PropertyName.Slice(0, bytesWritten)); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Literal.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Literal.cs index c5772f978005d..625d7711e0732 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Literal.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Literal.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Buffers; +using System.Buffers.Text; using System.Diagnostics; using System.Runtime.CompilerServices; @@ -503,5 +504,15 @@ private void WriteLiteralIndented(ReadOnlySpan escapedPropertyName, ReadOn value.CopyTo(output.Slice(BytesPending)); BytesPending += value.Length; } + + internal void WritePropertyName(bool value) + { + Span utf8PropertyName = stackalloc byte[JsonConstants.MaximumFormatBooleanLength]; + + bool result = Utf8Formatter.TryFormat(value, utf8PropertyName, out int bytesWritten); + Debug.Assert(result); + + WritePropertyNameUnescaped(utf8PropertyName.Slice(0, bytesWritten)); + } } } 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 360bea1089d0c..f24aad62ac1a7 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,18 @@ private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, long va Debug.Assert(result); BytesPending += bytesWritten; } + + internal void WritePropertyName(int value) + => WritePropertyName((long)value); + + internal void WritePropertyName(long value) + { + Span utf8PropertyName = stackalloc byte[JsonConstants.MaximumFormatInt64Length]; + + bool result = Utf8Formatter.TryFormat(value, utf8PropertyName, out int bytesWritten); + Debug.Assert(result); + + WritePropertyNameUnescaped(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 ddcca39fa28f4..d0c19f67a5180 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 @@ -255,6 +255,15 @@ public void WritePropertyName(ReadOnlySpan utf8PropertyName) _tokenType = JsonTokenType.PropertyName; } + private void WritePropertyNameUnescaped(ReadOnlySpan utf8PropertyName) + { + JsonWriterHelper.ValidateProperty(utf8PropertyName); + WriteStringByOptionsPropertyName(utf8PropertyName); + + _currentDepth &= JsonConstants.RemoveFlagsBitMask; + _tokenType = JsonTokenType.PropertyName; + } + private void WriteStringEscapeProperty(ReadOnlySpan utf8PropertyName, int firstEscapeIndexProp) { Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.UnsignedNumber.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.UnsignedNumber.cs index 48d9518445434..0430768b43be6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.UnsignedNumber.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.UnsignedNumber.cs @@ -441,5 +441,18 @@ private void WriteNumberIndented(ReadOnlySpan escapedPropertyName, ulong v Debug.Assert(result); BytesPending += bytesWritten; } + + internal void WritePropertyName(uint value) + => WritePropertyName((ulong)value); + + internal void WritePropertyName(ulong value) + { + Span utf8PropertyName = stackalloc byte[JsonConstants.MaximumFormatUInt64Length]; + + bool result = Utf8Formatter.TryFormat(value, utf8PropertyName, out int bytesWritten); + Debug.Assert(result); + + WritePropertyNameUnescaped(utf8PropertyName.Slice(0, bytesWritten)); + } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Dictionary.NonStringKey.cs b/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Dictionary.NonStringKey.cs new file mode 100644 index 0000000000000..98b96d5d95a0f --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Dictionary.NonStringKey.cs @@ -0,0 +1,579 @@ +// 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.Collections.Specialized; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public partial class DictionaryTests + { + public abstract class DictionaryKeyTestsBase + { + protected abstract TKey Key { get; } + protected abstract TValue Value { get; } + protected virtual string _expectedJson => $"{{\"{Key}\":{Value}}}"; + + protected virtual void Validate(Dictionary dictionary) + { + bool success = dictionary.TryGetValue(Key, out TValue value); + Assert.True(success); + Assert.Equal(Value, value); + } + + private Dictionary BuildDictionary() + { + var dictionary = new Dictionary(); + dictionary.Add(Key, Value); + + return dictionary; + } + + [Fact] + public void TestDictionaryKey() + { + Dictionary dictionary = BuildDictionary(); + + string json = JsonSerializer.Serialize(dictionary); + Assert.Equal(_expectedJson, json); + + Dictionary dictionaryCopy = JsonSerializer.Deserialize>(json); + Validate(dictionaryCopy); + } + + [Fact] + public async Task TestDictionaryKeyAsync() + { + 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); + } + } + + public class DictionaryBoolKey : DictionaryKeyTestsBase + { + protected override bool Key => true; + protected override int Value => 1; + } + + public class DictionaryByteKey : DictionaryKeyTestsBase + { + protected override byte Key => byte.MaxValue; + protected override int Value => 1; + } + + public class DictionaryCharKey : DictionaryKeyTestsBase + { + protected override string _expectedJson => @"{""\uFFFF"":""\uFFFF""}"; + protected override char Key => char.MaxValue; + protected override char Value => char.MaxValue; + } + + public class DictionaryDateTimeKey : DictionaryKeyTestsBase + { + protected override string _expectedJson => $@"{{""{DateTime.MaxValue:O}"":1}}"; + protected override DateTime Key => DateTime.MaxValue; + protected override int Value => 1; + } + + public class DictionaryDateTimeOffsetKey : DictionaryKeyTestsBase + { + protected override string _expectedJson => $@"{{""{DateTimeOffset.MaxValue:O}"":1}}"; + protected override DateTimeOffset Key => DateTimeOffset.MaxValue; + protected override int Value => 1; + } + + public class DictionaryDecimalKey : DictionaryKeyTestsBase + { + protected override string _expectedJson => $@"{{""{JsonSerializer.Serialize(decimal.MaxValue)}"":1}}"; + protected override decimal Key => decimal.MaxValue; + protected override int Value => 1; + } + + public class DictionaryDoubleKey : DictionaryKeyTestsBase + { + protected override string _expectedJson => $@"{{""{JsonSerializer.Serialize(double.MaxValue)}"":1}}"; + protected override double Key => double.MaxValue; + protected override int Value => 1; + } + + public class DictionaryEnumKey : DictionaryKeyTestsBase + { + protected override MyEnum Key => MyEnum.Foo; + protected override int Value => 1; + } + + public class DictionaryEnumFlagsKey : DictionaryKeyTestsBase + { + protected override MyEnumFlags Key => MyEnumFlags.Foo | MyEnumFlags.Bar; + protected override int Value => 1; + } + + public class DictionaryGuidKey : DictionaryKeyTestsBase + { + // 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 class DictionaryInt16Key : DictionaryKeyTestsBase + { + protected override short Key => short.MaxValue; + protected override int Value => 1; + } + + public class DictionaryInt32Key : DictionaryKeyTestsBase + { + protected override int Key => int.MaxValue; + protected override int Value => 1; + } + + public class DictionaryInt64Key : DictionaryKeyTestsBase + { + protected override long Key => long.MaxValue; + protected override int Value => 1; + } + + public class DictionarySByteKey : DictionaryKeyTestsBase + { + protected override sbyte Key => sbyte.MaxValue; + protected override int Value => 1; + } + + public class DictionarySingleKey : DictionaryKeyTestsBase + { + protected override string _expectedJson => $@"{{""{JsonSerializer.Serialize(float.MaxValue)}"":1}}"; + protected override float Key => float.MaxValue; + protected override int Value => 1; + } + + public class DictionaryStringKey : DictionaryKeyTestsBase + { + protected override string Key => "KeyString"; + protected override int Value => 1; + } + + public class DictionaryUInt16Key : DictionaryKeyTestsBase + { + protected override ushort Key => ushort.MaxValue; + protected override int Value => 1; + } + + public class DictionaryUInt32Key : DictionaryKeyTestsBase + { + protected override uint Key => uint.MaxValue; + protected override int Value => 1; + } + + public class DictionaryUInt64Key : DictionaryKeyTestsBase + { + protected override ulong Key => ulong.MaxValue; + protected override int Value => 1; + } + + public abstract class DictionaryUnsupportedKeyTestsBase + { + private Dictionary _dictionary => BuildDictionary(); + protected abstract TKey Key { get; } + private Dictionary BuildDictionary() + { + return new Dictionary() { { Key, default } }; + } + + [Fact] + public void ThrowUnsupported_Serialize() + => Assert.Throws(() => JsonSerializer.Serialize(_dictionary)); + + [Fact] + public Task ThrowUnsupported_SerializeAsync() + => Assert.ThrowsAsync(() => JsonSerializer.SerializeAsync(new MemoryStream(), _dictionary)); + + [Fact] + public void ThrowUnsupported_Deserialize() => Assert.Throws(() + => JsonSerializer.Deserialize>(@"{""foo"":1}")); + + [Fact] + public Task ThrowUnsupported_DeserializeAsync() => Assert.ThrowsAsync(() + => JsonSerializer.DeserializeAsync>(new MemoryStream(Encoding.UTF8.GetBytes(@"{""foo"":1}"))).AsTask()); + + [Fact] + public void DoesNotThrowIfEmpty_Serialize() + => JsonSerializer.Serialize(new Dictionary()); + + [Fact] + public Task DoesNotThrowIfEmpty_SerializeAsync() + => JsonSerializer.SerializeAsync(new MemoryStream(), new Dictionary()); + + [Fact] + public void DoesNotThrowIfEmpty_Deserialize() + => JsonSerializer.Deserialize>("{}"); + + [Fact] + public Task DoesNotThrowIfEmpty_DeserializeAsync() + => JsonSerializer.DeserializeAsync>(new MemoryStream(Encoding.UTF8.GetBytes("{}"))).AsTask(); + } + + public class DictionaryMyPublicClassKeyUnsupported : DictionaryUnsupportedKeyTestsBase + { + protected override MyPublicClass Key => new MyPublicClass(); + } + + public class DictionaryMyPublicStructKeyUnsupported : DictionaryUnsupportedKeyTestsBase + { + protected override MyPublicStruct Key => new MyPublicStruct(); + } + + public class DictionaryUriKeyUnsupported : DictionaryUnsupportedKeyTestsBase + { + protected override Uri Key => new Uri("http://foo"); + } + + public class DictionaryObjectKeyUnsupported : DictionaryUnsupportedKeyTestsBase + { + protected override object Key => new object(); + } + + public class DictionaryPolymorphicKeyUnsupported : DictionaryUnsupportedKeyTestsBase + { + protected override object Key => new Uri("http://foo"); + } + + public class DictionaryNonStringKeyTests + { + [Fact] + public void TestGenericDictionaryKeyObject() + { + var dictionary = new Dictionary(); + // Add multiple supported types. + dictionary.Add(1, 1); + dictionary.Add(new Guid("08314FA2-B1FE-4792-BCD1-6E62338AC7F3"), 2); + dictionary.Add("KeyString", 3); + dictionary.Add(MyEnum.Foo, 4); + dictionary.Add(MyEnumFlags.Foo | MyEnumFlags.Bar, 5); + + const string expected = @"{""1"":1,""08314fa2-b1fe-4792-bcd1-6e62338ac7f3"":2,""KeyString"":3,""Foo"":4,""Foo, Bar"":5}"; + + string json = JsonSerializer.Serialize(dictionary); + Assert.Equal(expected, json); + // object type is not supported on deserialization. + Assert.Throws(() => JsonSerializer.Deserialize>(json)); + + var @object = new ClassWithDictionary { Dictionary = dictionary }; + json = JsonSerializer.Serialize(@object); + Assert.Equal($@"{{""Dictionary"":{expected}}}", json); + Assert.Throws(() => JsonSerializer.Deserialize(json)); + } + + [Fact] + public void TestNonGenericDictionaryKeyObject() + { + IDictionary dictionary = new OrderedDictionary(); + // Add multiple supported types. + dictionary.Add(1, 1); + dictionary.Add(new Guid("08314FA2-B1FE-4792-BCD1-6E62338AC7F3"), 2); + dictionary.Add("KeyString", 3); + dictionary.Add(MyEnum.Foo, 4); + dictionary.Add(MyEnumFlags.Foo | MyEnumFlags.Bar, 5); + + const string expected = @"{""1"":1,""08314fa2-b1fe-4792-bcd1-6e62338ac7f3"":2,""KeyString"":3,""Foo"":4,""Foo, Bar"":5}"; + string json = JsonSerializer.Serialize(dictionary); + Assert.Equal(expected, json); + + dictionary = JsonSerializer.Deserialize(json); + Assert.IsType>(dictionary); + + dictionary = JsonSerializer.Deserialize(json); + foreach (object key in dictionary.Keys) + { + Assert.IsType(key); + } + + var @object = new ClassWithIDictionary { Dictionary = dictionary }; + json = JsonSerializer.Serialize(@object); + Assert.Equal($@"{{""Dictionary"":{expected}}}", json); + + @object = JsonSerializer.Deserialize(json); + Assert.IsType>(@object.Dictionary); + } + + [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); + } + + [Fact] + public static void TestNotSuportedExceptionIsThrown() + { + // Dictionary> + Assert.Null(JsonSerializer.Deserialize>("null")); + Assert.Throws(() => JsonSerializer.Deserialize>("\"\"")); + Assert.NotNull(JsonSerializer.Deserialize>("{}")); + + Assert.Throws(() => JsonSerializer.Deserialize>(@"{""Foo"":1}")); + + // UnsupportedDictionaryWrapper + Assert.Throws(() => JsonSerializer.Deserialize("\"\"")); + Assert.NotNull(JsonSerializer.Deserialize("{}")); + Assert.Null(JsonSerializer.Deserialize("null")); + Assert.NotNull(JsonSerializer.Deserialize(@"{""Dictionary"":null}")); + Assert.NotNull(JsonSerializer.Deserialize(@"{""Dictionary"":{}}")); + + Assert.Throws(() => JsonSerializer.Deserialize(@"{""Dictionary"":{""Foo"":1}}")); + } + + [Fact] + public void TestPolicyOnlyAppliesToString() + { + var opts = new JsonSerializerOptions + { + DictionaryKeyPolicy = new FixedNamingPolicy() + }; + + var stringIntDictionary = new Dictionary { { "1", 1 } }; + string json = JsonSerializer.Serialize(stringIntDictionary, opts); + Assert.Equal($@"{{""{FixedNamingPolicy.FixedName}"":1}}", json); + + var intIntDictionary = new Dictionary { { 1, 1 } }; + json = JsonSerializer.Serialize(intIntDictionary, opts); + Assert.Equal(@"{""1"":1}", json); + + var objectIntDictionary = new Dictionary { { "1", 1 } }; + json = JsonSerializer.Serialize(objectIntDictionary, opts); + Assert.Equal($@"{{""{FixedNamingPolicy.FixedName}"":1}}", json); + + objectIntDictionary = new Dictionary { { 1, 1 } }; + json = JsonSerializer.Serialize(objectIntDictionary, opts); + Assert.Equal(@"{""1"":1}", json); + } + + [Fact] + public async Task TestPolicyOnlyAppliesToStringAsync() + { + var opts = new JsonSerializerOptions + { + DictionaryKeyPolicy = new FixedNamingPolicy() + }; + + MemoryStream stream = new MemoryStream(); + + var stringIntDictionary = new Dictionary { { "1", 1 } }; + await JsonSerializer.SerializeAsync(stream, stringIntDictionary, opts); + + string json = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal($@"{{""{FixedNamingPolicy.FixedName}"":1}}", json); + + stream.Position = 0; + stream.SetLength(0); + + var intIntDictionary = new Dictionary { { 1, 1 } }; + await JsonSerializer.SerializeAsync(stream, intIntDictionary, opts); + + json = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(@"{""1"":1}", json); + + stream.Position = 0; + stream.SetLength(0); + + var objectIntDictionary = new Dictionary { { "1", 1 } }; + await JsonSerializer.SerializeAsync(stream, objectIntDictionary, opts); + + json = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal($@"{{""{FixedNamingPolicy.FixedName}"":1}}", json); + + stream.Position = 0; + stream.SetLength(0); + + objectIntDictionary = new Dictionary { { 1, 1 } }; + await JsonSerializer.SerializeAsync(stream, objectIntDictionary, opts); + + json = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(@"{""1"":1}", json); + } + + [Fact] + public void TestEnumKeyWithNotValidIdentifier() + { + var myEnumIntDictionary = new Dictionary(); + myEnumIntDictionary.Add((MyEnum)(-1), 1); + + string json = JsonSerializer.Serialize(myEnumIntDictionary); + Assert.Equal(@"{""-1"":1}", json); + + myEnumIntDictionary = JsonSerializer.Deserialize>(json); + Assert.Equal(1, myEnumIntDictionary[(MyEnum)(-1)]); + + var myEnumFlagsIntDictionary = new Dictionary(); + myEnumFlagsIntDictionary.Add((MyEnumFlags)(-1), 1); + + json = JsonSerializer.Serialize(myEnumFlagsIntDictionary); + Assert.Equal(@"{""-1"":1}", json); + + myEnumFlagsIntDictionary = JsonSerializer.Deserialize>(json); + Assert.Equal(1, myEnumFlagsIntDictionary[(MyEnumFlags)(-1)]); + } + + [Theory] + [MemberData(nameof(DictionaryKeysWithSpecialCharacters))] + public void EnsureNonStringKeysDontGetEscapedOnSerialize(object key, string expectedKeySerialized) + { + Dictionary root = new Dictionary(); + root.Add(key, 1); + + string json = JsonSerializer.Serialize(root); + Assert.Contains(expectedKeySerialized, json); + } + + public static IEnumerable DictionaryKeysWithSpecialCharacters => + new List + { + new object[] { float.MaxValue, JsonSerializer.Serialize(float.MaxValue) }, + new object[] { double.MaxValue, JsonSerializer.Serialize(double.MaxValue) }, + new object[] { DateTimeOffset.MaxValue, JsonSerializer.Serialize(DateTimeOffset.MaxValue) } + }; + + [Theory] + [MemberData(nameof(EscapedMemberData))] + public void TestEscapedValuesOnDeserialize(string escapedPropertyName, object expectedDictionaryKey, Type dictionaryType) + { + string json = $@"{{""{escapedPropertyName}"":1}}"; + IDictionary root = (IDictionary)JsonSerializer.Deserialize(json, dictionaryType); + + bool containsKey = root.Contains(expectedDictionaryKey); + Assert.True(containsKey); + Assert.Equal(1, root[expectedDictionaryKey]); + } + + [Theory] + [MemberData(nameof(EscapedMemberData))] + public async Task TestEscapedValuesOnDeserializeAsync(string escapedPropertyName, object expectedDictionaryKey, Type dictionaryType) + { + string json = $@"{{""{escapedPropertyName}"":1}}"; + MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + IDictionary root = (IDictionary)await JsonSerializer.DeserializeAsync(stream, dictionaryType); + + bool containsKey = root.Contains(expectedDictionaryKey); + Assert.True(containsKey); + Assert.Equal(1, root[expectedDictionaryKey]); + } + + public static IEnumerable EscapedMemberData => + new List + { + new object[] { @"\u0031\u0032\u0037", + sbyte.MaxValue, typeof(Dictionary) }, + new object[] { @"\u0032\u0035\u0035", + byte.MaxValue, typeof(Dictionary) }, + new object[] { @"\u0033\u0032\u0037\u0036\u0037", + short.MaxValue, typeof(Dictionary) }, + new object[] { @"\u0036\u0035\u0035\u0033\u0035", + ushort.MaxValue, typeof(Dictionary) }, + new object[] { @"\u0032\u0031\u0034\u0037\u0034\u0038\u0033\u0036\u0034\u0037", + int.MaxValue, typeof(Dictionary) }, + new object[] { @"\u0034\u0032\u0039\u0034\u0039\u0036\u0037\u0032\u0039\u0035", + uint.MaxValue, typeof(Dictionary) }, + new object[] { @"\u0039\u0032\u0032\u0033\u0033\u0037\u0032\u0030\u0033\u0036\u0038\u0035\u0034\u0037\u0037\u0035\u0038\u0030\u0037", + long.MaxValue, typeof(Dictionary) }, + new object[] { @"\u0031\u0038\u0034\u0034\u0036\u0037\u0034\u0034\u0030\u0037\u0033\u0037\u0030\u0039\u0035\u0035\u0031\u0036\u0031\u0035", + ulong.MaxValue, typeof(Dictionary) }, + // Do not use max values on floating point types since it may have different string representations depending on the tfm. + new object[] { @"\u0033\u002e\u0031\u0032\u0035\u0065\u0037", + 3.125e7f, typeof(Dictionary) }, + new object[] { @"\u0033\u002e\u0031\u0032\u0035\u0065\u0037", + 3.125e7d, typeof(Dictionary) }, + new object[] { @"\u0033\u002e\u0031\u0032\u0035\u0065\u0037", + 3.125e7m, typeof(Dictionary) }, + new object[] { @"\u0039\u0039\u0039\u0039\u002d\u0031\u0032\u002d\u0033\u0031\u0054\u0032\u0033\u003a\u0035\u0039\u003a\u0035\u0039\u002e\u0039\u0039\u0039\u0039\u0039\u0039\u0039", + DateTime.MaxValue, typeof(Dictionary) }, + new object[] { @"\u0039\u0039\u0039\u0039\u002d\u0031\u0032\u002d\u0033\u0031\u0054\u0032\u0033\u003a\u0035\u0039\u003a\u0035\u0039\u002e\u0039\u0039\u0039\u0039\u0039\u0039\u0039\u002b\u0030\u0030\u003a\u0030\u0030", + DateTimeOffset.MaxValue, typeof(Dictionary) }, + new object[] { @"\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u002d\u0030\u0030\u0030\u0030\u002d\u0030\u0030\u0030\u0030\u002d\u0030\u0030\u0030\u0030\u002d\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0030", + Guid.Empty, typeof(Dictionary) }, + new object[] { @"\u0042\u0061\u0072", + MyEnum.Bar, typeof(Dictionary) }, + new object[] { @"\u0042\u0061\u0072\u002c\u0042\u0061\u007a", + MyEnumFlags.Bar | MyEnumFlags.Baz, typeof(Dictionary) }, + new object[] { @"\u002b", '+', typeof(Dictionary) } + }; + } + + public class MyPublicClass { } + + public struct MyPublicStruct { } + + public enum MyEnum + { + Foo, + Bar + } + + [Flags] + public enum MyEnumFlags + { + Foo = 1, + Bar = 2, + Baz = 4 + } + + private class ClassWithIDictionary + { + public IDictionary Dictionary { get; set; } + } + + private class ClassWithDictionary + { + public Dictionary Dictionary { get; set; } + } + + private class ClassWithExtensionData + { + [JsonExtensionData] + public Dictionary Overflow { get; set; } + } + + private class UnsupportedDictionaryWrapper + { + public Dictionary Dictionary { get; set; } + } + + public class FixedNamingPolicy : JsonNamingPolicy + { + public const string FixedName = nameof(FixedName); + public override string ConvertName(string name) => FixedName; + } + + public class SuffixNamingPolicy : JsonNamingPolicy + { + public const string Suffix = "_Suffix"; + public override string ConvertName(string name) => name + Suffix; + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Dictionary.cs b/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Dictionary.cs index bf1fb5a1587aa..9dd3c4d4a2fcb 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Dictionary.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Dictionary.cs @@ -641,13 +641,6 @@ public class Poco public int Id { get; set; } } - [Fact] - public static void FirstGenericArgNotStringFail() - { - Assert.Throws(() => JsonSerializer.Deserialize>(@"{1:1}")); - Assert.Throws(() => JsonSerializer.Deserialize>(@"{1:1}")); - } - [Fact] public static void DictionaryOfList() { @@ -1619,8 +1612,7 @@ public static void DictionaryNotSupported() NotSupportedException ex = Assert.Throws(() => JsonSerializer.Deserialize(json)); // The exception contains the type. - Assert.Contains(typeof(Dictionary).ToString(), ex.Message); - Assert.DoesNotContain("Path: ", ex.Message); + Assert.Contains(typeof(Dictionary).ToString(), ex.Message); } [Fact] @@ -1840,12 +1832,12 @@ public static void NullDictionaryValuesShouldDeserializeAsNull() public class ClassWithNotSupportedDictionary { - public Dictionary MyDictionary { get; set; } + public Dictionary MyDictionary { get; set; } } public class ClassWithNotSupportedDictionaryButIgnored { - [JsonIgnore] public Dictionary MyDictionary { get; set; } + [JsonIgnore] public Dictionary MyDictionary { get; set; } } public class AllSingleUpperPropertiesParent @@ -2143,15 +2135,6 @@ public static void VerifyDictionaryThatHasIncomatibleEnumeratorWithPoco() Assert.Throws(() => JsonSerializer.Serialize(dictionary)); } - - [Fact] - public static void VerifyIDictionaryWithNonStringKey() - { - IDictionary dictionary = new Hashtable(); - dictionary.Add(1, "value"); - Assert.Throws(() => JsonSerializer.Serialize(dictionary)); - } - private class ClassWithoutParameterlessCtor { public ClassWithoutParameterlessCtor(int num) { } diff --git a/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Read.cs b/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Read.cs index 3e9eae828e837..70340547c6b63 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Read.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Read.cs @@ -1148,10 +1148,10 @@ private static IEnumerable CustomInterfaces_Dictionaries() } [Fact] - public static void IReadOnlyDictionary_NonStringKey_NotSupported() + public static void IReadOnlyDictionary_NotSupportedKey() { - Assert.Throws(() => JsonSerializer.Deserialize>("")); - Assert.Throws(() => JsonSerializer.Serialize(new GenericIReadOnlyDictionaryWrapper())); + Assert.Throws(() => JsonSerializer.Deserialize>(@"{""http://foo"":1}")); + Assert.Throws(() => JsonSerializer.Serialize(new GenericIReadOnlyDictionaryWrapper(new Dictionary { { new Uri("http://foo"), 1 } }))); } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.DerivedTypes.cs b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.DerivedTypes.cs index 37b2765cb2b2e..b97696d5facf4 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.DerivedTypes.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.DerivedTypes.cs @@ -76,7 +76,7 @@ public static void CustomUnsupportedDictionaryConverter() { DictionaryWrapper = new UnsupportedDictionaryWrapper() }; - wrapper.DictionaryWrapper[1] = 1; + wrapper.DictionaryWrapper[new int[,] { }] = 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/ExtensionDataTests.cs b/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs index 8c95c471e509e..73906d8b745c3 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs @@ -866,9 +866,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/Serialization/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs index c2fd2148b3284..8fb2731ae7876 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs @@ -851,18 +851,41 @@ public static void JsonIgnoreAttribute_UnsupportedCollection() JsonSerializerOptions options = new JsonSerializerOptions(); // Unsupported collections will throw on serialize by default. - Assert.Throws(() => JsonSerializer.Serialize(new ClassWithUnsupportedDictionary(), options)); + // Only when the collection contains elements. - // Unsupported collections will throw on deserialize by default. + var dictionary = new Dictionary(); + // Uri is an unsupported dictionary key. + dictionary.Add(new Uri("http://foo"), "bar"); + + var concurrentDictionary = new ConcurrentDictionary(dictionary); + + var instance = new ClassWithUnsupportedDictionary() + { + MyConcurrentDict = concurrentDictionary, + MyIDict = dictionary + }; + + var instanceWithIgnore = new ClassWithIgnoredUnsupportedDictionary + { + MyConcurrentDict = concurrentDictionary, + MyIDict = dictionary + }; + + Assert.Throws(() => JsonSerializer.Serialize(instance, options)); + + // Unsupported collections will throw on deserialize by default if they contain elements. options = new JsonSerializerOptions(); Assert.Throws(() => JsonSerializer.Deserialize(wrapperJson, options)); options = new JsonSerializerOptions(); - // Unsupported collections will throw on serialize by default. - Assert.Throws(() => JsonSerializer.Serialize(new WrapperForClassWithUnsupportedDictionary(), options)); + // Unsupported collections will throw on serialize by default if they contain elements. + Assert.Throws(() => JsonSerializer.Serialize(instance, options)); // When ignored, we can serialize and deserialize without exceptions. options = new JsonSerializerOptions(); + + Assert.NotNull(JsonSerializer.Serialize(instanceWithIgnore, options)); + ClassWithIgnoredUnsupportedDictionary obj = JsonSerializer.Deserialize(json, options); Assert.Null(obj.MyDict); 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 ddfccacb6f8a5..918787c3201ec 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) true @@ -41,6 +41,7 @@ +