Fix JSON deserializer property name escaping and address naming feedback (dotnet...
authorSteve Harter <steveharter@users.noreply.github.com>
Wed, 24 Apr 2019 18:37:04 +0000 (11:37 -0700)
committerGitHub <noreply@github.com>
Wed, 24 Apr 2019 18:37:04 +0000 (11:37 -0700)
Commit migrated from https://github.com/dotnet/corefx/commit/cf0624854a1a4d8eda961a6eb06bb803b0555dfd

13 files changed:
src/libraries/System.Text.Json/src/Resources/Strings.resx
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoCommon.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullable.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs
src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs
src/libraries/System.Text.Json/tests/Serialization/PropertyNameTests.cs
src/libraries/System.Text.Json/tests/Serialization/TestClasses.cs

index 4184ecbdba25e3ab5e0ffeee65e15d028fa07dc0..a403565943ca023833843928def64b36470375d0 100644 (file)
     <value>Cannot write a comment value which contains the end of comment delimiter.</value>
   </data>
   <data name="SerializerPropertyNameConflict" xml:space="preserve">
-    <value>The property '{0}.{1}' has the same name as a previous property based on naming or casing policies.</value>
+    <value>The JSON property name for '{0}.{1}' collides with another property.</value>
   </data>
   <data name="SerializerPropertyNameNull" xml:space="preserve">
-    <value>The property name for '{0}.{1}' cannot be null as a result of naming policies.</value>
+    <value>The JSON property name for '{0}.{1}' cannot be null.</value>
   </data>
 </root>
\ No newline at end of file
index 1e5998046863ae6b35e706a868c9f13f611733fd..a9c5c5a9aa1d668e4d6664644b7fe25e512ba0b5 100644 (file)
@@ -25,7 +25,7 @@ namespace System.Text.Json.Serialization
 
             if (propertyInfo != null)
             {
-                _propertyRefs.Add(new PropertyRef(GetKey(jsonInfo.CompareName), jsonInfo));
+                _propertyRefs.Add(new PropertyRef(GetKey(jsonInfo.NameUsedToCompare), jsonInfo));
             }
             else
             {
@@ -60,7 +60,7 @@ namespace System.Text.Json.Serialization
 
             JsonPropertyInfo jsonInfo = (JsonPropertyInfo)Activator.CreateInstance(
                 propertyInfoClassType,
-                BindingFlags.Instance | BindingFlags.NonPublic,
+                BindingFlags.Instance | BindingFlags.Public,
                 binder: null,
                 new object[] { parentClassType, declaredPropertyType, runtimePropertyType, propertyInfo, collectionElementType, options },
                 culture: null);
index c97b0f6d26028ca399e24d32584a85bf3e87c74d..d26b4ec1e6bcf73a44bcb628b259737f308da1f9 100644 (file)
@@ -102,7 +102,7 @@ namespace System.Text.Json.Serialization
                         }
 
                         // If the JsonPropertyNameAttribute or naming policy results in collisions, throw an exception.
-                        if (!propertyNames.Add(jsonPropertyInfo.CompareNameAsString))
+                        if (!propertyNames.Add(jsonPropertyInfo.NameUsedToCompareAsString))
                         {
                             ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(this, jsonPropertyInfo);
                         }
@@ -242,7 +242,7 @@ namespace System.Text.Json.Serialization
             {
                 if (propertyName.Length <= PropertyNameKeyLength ||
                     // We compare the whole name, although we could skip the first 6 bytes (but it's likely not any faster)
-                    propertyName.SequenceEqual((ReadOnlySpan<byte>)propertyRef.Info.CompareName))
+                    propertyName.SequenceEqual(propertyRef.Info.NameUsedToCompare))
                 {
                     info = propertyRef.Info;
                     return true;
index ff4a2a360eb6999f0b01328ba3f427a2edf76438..32c4ffbc3de4273b3ae4fb37e0439982db78df79 100644 (file)
@@ -5,7 +5,7 @@
 namespace System.Text.Json.Serialization
 {
     /// <summary>
-    /// Determines the naming policy used to convert a JSON name to another format, such as a camel-casing format.
+    /// Determines the naming policy used to convert a string-based name to another format, such as a camel-casing format.
     /// </summary>
     public abstract class JsonNamingPolicy
     {
@@ -17,7 +17,7 @@ namespace System.Text.Json.Serialization
         public static JsonNamingPolicy CamelCase { get; } = new JsonCamelCaseNamePolicy();
 
         /// <summary>
-        /// Converts the provided name.
+        /// When overridden in a derived class, converts the specified name according to the policy.
         /// </summary>
         /// <param name="name">The name to convert.</param>
         /// <returns>The converted name.</returns>
index f40258686284dacdbc90559b8a401d5c428dc863..1fb7bc1b4bcb3c17cc2b1b5de853d51148374980 100644 (file)
@@ -17,37 +17,36 @@ namespace System.Text.Json.Serialization
         private static readonly JsonEnumerableConverter s_jsonArrayConverter = new DefaultArrayConverter();
         private static readonly JsonEnumerableConverter s_jsonEnumerableConverter = new DefaultEnumerableConverter();
 
-        internal ClassType ClassType;
+        public ClassType ClassType;
 
         // The name of the property with any casing policy or the name specified from JsonPropertyNameAttribute.
         private byte[] _name { get; set; }
-        internal ReadOnlySpan<byte> Name => _name;
-        internal string NameAsString { get; private set; }
+        public ReadOnlySpan<byte> Name => _name;
+        public string NameAsString { get; private set; }
 
         // Used to support case-insensitive comparison
-        private byte[] _compareName { get; set; }
-        internal ReadOnlySpan<byte> CompareName => _compareName;
-        internal string CompareNameAsString { get; private set; }
+        private byte[] _nameUsedToCompare { get; set; }
+        public ReadOnlySpan<byte> NameUsedToCompare => _nameUsedToCompare;
+        public string NameUsedToCompareAsString { get; private set; }
 
         // The escaped name passed to the writer.
-        internal byte[] _escapedName { get; private set; }
-        internal ReadOnlySpan<byte> EscapedName => _escapedName;
+        public byte[] _escapedName { get; private set; }
 
-        internal bool HasGetter { get; set; }
-        internal bool HasSetter { get; set; }
-        internal bool ShouldSerialize { get; private set; }
-        internal bool ShouldDeserialize { get; private set; }
+        public bool HasGetter { get; set; }
+        public bool HasSetter { get; set; }
+        public bool ShouldSerialize { get; private set; }
+        public bool ShouldDeserialize { get; private set; }
 
-        internal bool IgnoreNullValues { get; private set; }
+        public bool IgnoreNullValues { get; private set; }
 
 
         // todo: to minimize hashtable lookups, cache JsonClassInfo:
         //public JsonClassInfo ClassInfo;
 
         // Constructor used for internal identifiers
-        internal JsonPropertyInfo() { }
+        public JsonPropertyInfo() { }
 
-        internal JsonPropertyInfo(
+        public JsonPropertyInfo(
             Type parentClassType,
             Type declaredPropertyType,
             Type runtimePropertyType,
@@ -70,21 +69,21 @@ namespace System.Text.Json.Serialization
             CanBeNull = IsNullableType || !runtimePropertyType.IsValueType;
         }
 
-        internal bool CanBeNull { get; private set; }
-        internal JsonClassInfo ElementClassInfo { get; private set; }
-        internal JsonEnumerableConverter EnumerableConverter { get; private set; }
+        public bool CanBeNull { get; private set; }
+        public JsonClassInfo ElementClassInfo { get; private set; }
+        public JsonEnumerableConverter EnumerableConverter { get; private set; }
 
-        internal bool IsNullableType { get; private set; }
+        public bool IsNullableType { get; private set; }
 
-        internal PropertyInfo PropertyInfo { get; private set; }
+        public PropertyInfo PropertyInfo { get; private set; }
 
-        internal Type ParentClassType { get; private set; }
+        public Type ParentClassType { get; private set; }
 
-        internal Type DeclaredPropertyType { get; private set; }
+        public Type DeclaredPropertyType { get; private set; }
 
-        internal Type RuntimePropertyType { get; private set; }
+        public Type RuntimePropertyType { get; private set; }
 
-        internal virtual void GetPolicies(JsonSerializerOptions options)
+        public virtual void GetPolicies(JsonSerializerOptions options)
         {
             DetermineSerializationCapabilities(options);
             DeterminePropertyName(options);
@@ -99,8 +98,8 @@ namespace System.Text.Json.Serialization
                 if (nameAttribute != null)
                 {
                     NameAsString = nameAttribute.Name;
-                    
-                    // This is detected and thrown by caller.
+
+                    // null is not valid; JsonClassInfo throws an InvalidOperationException after this return.
                     if (NameAsString == null)
                     {
                         return;
@@ -110,7 +109,7 @@ namespace System.Text.Json.Serialization
                 {
                     NameAsString = options.PropertyNamingPolicy.ConvertName(PropertyInfo.Name);
 
-                    // This is detected and thrown by caller.
+                    // null is not valid; JsonClassInfo throws an InvalidOperationException after this return.
                     if (NameAsString == null)
                     {
                         return;
@@ -127,16 +126,21 @@ namespace System.Text.Json.Serialization
                 // Set the compare name.
                 if (options.PropertyNameCaseInsensitive)
                 {
-                    CompareNameAsString = NameAsString.ToUpperInvariant();
-                    _compareName = Encoding.UTF8.GetBytes(CompareNameAsString);
+                    NameUsedToCompareAsString = NameAsString.ToUpperInvariant();
+                    _nameUsedToCompare = Encoding.UTF8.GetBytes(NameUsedToCompareAsString);
                 }
                 else
                 {
-                    CompareNameAsString = NameAsString;
-                    _compareName = _name;
+                    NameUsedToCompareAsString = NameAsString;
+                    _nameUsedToCompare = _name;
                 }
 
                 // Cache the escaped name.
+#if true
+                // temporary behavior until the writer can accept escaped string.
+                _escapedName = _name;
+#else
+                
                 int valueIdx = JsonWriterHelper.NeedsEscaping(_name);
                 if (valueIdx == -1)
                 {
@@ -144,18 +148,25 @@ namespace System.Text.Json.Serialization
                 }
                 else
                 {
+                    byte[] pooledName = null;
                     int length = JsonWriterHelper.GetMaxEscapedLength(_name.Length, valueIdx);
 
-                    byte[] tempArray = ArrayPool<byte>.Shared.Rent(length);
+                    Span<byte> escapedName = length <= JsonConstants.StackallocThreshold ?
+                        stackalloc byte[length] :
+                        (pooledName = ArrayPool<byte>.Shared.Rent(length));
+
+                    JsonWriterHelper.EscapeString(_name, escapedName, 0, out int written);
 
-                    JsonWriterHelper.EscapeString(_name, tempArray, valueIdx, out int written);
-                    _escapedName = new byte[written];
-                    tempArray.CopyTo(_escapedName, 0);
+                    _escapedName = escapedName.Slice(0, written).ToArray();
 
-                    // We clear the array because it is "user data" (although a property name).
-                    new Span<byte>(tempArray, 0, written).Clear();
-                    ArrayPool<byte>.Shared.Return(tempArray);
+                    if (pooledName != null)
+                    {
+                        // We clear the array because it is "user data" (although a property name).
+                        new Span<byte>(pooledName, 0, written).Clear();
+                        ArrayPool<byte>.Shared.Return(pooledName);
+                    }
                 }
+#endif
             }
         }
 
@@ -226,40 +237,40 @@ namespace System.Text.Json.Serialization
         }
 
         // After the property is added, clear any state not used later.
-        internal void ClearUnusedValuesAfterAdd()
+        public void ClearUnusedValuesAfterAdd()
         {
             NameAsString = null;
-            CompareNameAsString = null;
+            NameUsedToCompareAsString = null;
         }
 
         // Copy any settings defined at run-time to the new property.
-        internal void CopyRuntimeSettingsTo(JsonPropertyInfo other)
+        public void CopyRuntimeSettingsTo(JsonPropertyInfo other)
         {
             other._name = _name;
-            other._compareName = _compareName;
+            other._nameUsedToCompare = _nameUsedToCompare;
             other._escapedName = _escapedName;
         }
 
-        internal abstract object GetValueAsObject(object obj, JsonSerializerOptions options);
+        public abstract object GetValueAsObject(object obj, JsonSerializerOptions options);
 
-        internal TAttribute GetAttribute<TAttribute>() where TAttribute : Attribute
+        public TAttribute GetAttribute<TAttribute>() where TAttribute : Attribute
         {
             return (TAttribute)PropertyInfo?.GetCustomAttribute(typeof(TAttribute), inherit: false);
         }
 
-        internal abstract void ApplyNullValue(JsonSerializerOptions options, ref ReadStack state);
-            
-        internal abstract IList CreateConverterList();
+        public abstract void ApplyNullValue(JsonSerializerOptions options, ref ReadStack state);
+
+        public abstract IList CreateConverterList();
 
-        internal abstract Type GetConcreteType(Type interfaceType);
+        public abstract Type GetConcreteType(Type interfaceType);
 
-        internal abstract void Read(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader);
-        internal abstract void ReadEnumerable(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader);
-        internal abstract void SetValueAsObject(object obj, object value, JsonSerializerOptions options);
+        public abstract void Read(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader);
+        public abstract void ReadEnumerable(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader);
+        public abstract void SetValueAsObject(object obj, object value, JsonSerializerOptions options);
 
-        internal abstract void Write(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer);
+        public abstract void Write(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer);
 
-        internal abstract void WriteDictionary(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer);
-        internal abstract void WriteEnumerable(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer);
+        public abstract void WriteDictionary(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer);
+        public abstract void WriteEnumerable(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer);
     }
 }
index f99150d10145d9c2b8997544039c68cc1e0e8306..b1e5e12d6b1943ca3f92d929974a56a2c4505f7d 100644 (file)
@@ -16,16 +16,16 @@ namespace System.Text.Json.Serialization
     /// </summary>
     internal abstract class JsonPropertyInfoCommon<TClass, TDeclaredProperty, TRuntimeProperty> : JsonPropertyInfo
     {
-        internal bool _isPropertyPolicy;
-        internal Func<TClass, TDeclaredProperty> Get { get; private set; }
-        internal Action<TClass, TDeclaredProperty> Set { get; private set; }
+        public bool _isPropertyPolicy;
+        public Func<TClass, TDeclaredProperty> Get { get; private set; }
+        public Action<TClass, TDeclaredProperty> Set { get; private set; }
 
         public JsonValueConverter<TRuntimeProperty> ValueConverter { get; internal set; }
 
         // Constructor used for internal identifiers
-        internal JsonPropertyInfoCommon() { }
+        public JsonPropertyInfoCommon() { }
 
-        internal JsonPropertyInfoCommon(
+        public JsonPropertyInfoCommon(
             Type parentClassType,
             Type declaredPropertyType,
             Type runtimePropertyType,
@@ -63,13 +63,13 @@ namespace System.Text.Json.Serialization
             GetPolicies(options);
         }
 
-        internal override void GetPolicies(JsonSerializerOptions options)
+        public override void GetPolicies(JsonSerializerOptions options)
         {
             ValueConverter = DefaultConverters<TRuntimeProperty>.s_converter;
             base.GetPolicies(options);
         }
 
-        internal override object GetValueAsObject(object obj, JsonSerializerOptions options)
+        public override object GetValueAsObject(object obj, JsonSerializerOptions options)
         {
             if (_isPropertyPolicy)
             {
@@ -80,7 +80,7 @@ namespace System.Text.Json.Serialization
             return Get((TClass)obj);
         }
 
-        internal override void SetValueAsObject(object obj, object value, JsonSerializerOptions options)
+        public override void SetValueAsObject(object obj, object value, JsonSerializerOptions options)
         {
             Debug.Assert(Set != null);
             TDeclaredProperty typedValue = (TDeclaredProperty)value;
@@ -91,13 +91,13 @@ namespace System.Text.Json.Serialization
             }
         }
 
-        internal override IList CreateConverterList()
+        public override IList CreateConverterList()
         {
             return new List<TDeclaredProperty>();
         }
 
         // Map interfaces to a well-known implementation.
-        internal override Type GetConcreteType(Type interfaceType)
+        public override Type GetConcreteType(Type interfaceType)
         {
             if (interfaceType.IsAssignableFrom(typeof(IDictionary<string, TRuntimeProperty>)) ||
                 interfaceType.IsAssignableFrom(typeof(IReadOnlyDictionary<string, TRuntimeProperty>)))
index 6dfea95e90096125340f780851f002222e1b6587..b07fe0245a01f3f5e985ba2fae34443d5e5477b6 100644 (file)
@@ -17,9 +17,9 @@ namespace System.Text.Json.Serialization
         where TRuntimeProperty : TDeclaredProperty
     {
         // Constructor used for internal identifiers
-        internal JsonPropertyInfoNotNullable() { }
+        public JsonPropertyInfoNotNullable() { }
 
-        internal JsonPropertyInfoNotNullable(
+        public JsonPropertyInfoNotNullable(
             Type parentClassType,
             Type declaredPropertyType,
             Type runtimePropertyType,
@@ -30,7 +30,7 @@ namespace System.Text.Json.Serialization
         {
         }
 
-        internal override void Read(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader)
+        public override void Read(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader)
         {
             if (ElementClassInfo != null)
             {
@@ -65,7 +65,7 @@ namespace System.Text.Json.Serialization
         }
 
         // If this method is changed, also change JsonPropertyInfoNullable.ReadEnumerable and JsonSerializer.ApplyObjectToEnumerable
-        internal override void ReadEnumerable(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader)
+        public override void ReadEnumerable(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader)
         {
             if (ValueConverter == null || !ValueConverter.TryRead(RuntimePropertyType, ref reader, out TRuntimeProperty value))
             {
@@ -76,14 +76,14 @@ namespace System.Text.Json.Serialization
             JsonSerializer.ApplyValueToEnumerable(ref value, options, ref state.Current);
         }
 
-        internal override void ApplyNullValue(JsonSerializerOptions options, ref ReadStack state)
+        public override void ApplyNullValue(JsonSerializerOptions options, ref ReadStack state)
         {
             Debug.Assert(state.Current.JsonPropertyInfo != null);
             state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, null, options);
         }
 
         // todo: have the caller check if current.Enumerator != null and call WriteEnumerable of the underlying property directly to avoid an extra virtual call.
-        internal override void Write(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
+        public override void Write(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
         {
             if (current.Enumerator != null)
             {
@@ -128,12 +128,13 @@ namespace System.Text.Json.Serialization
             }
         }
 
-        internal override void WriteDictionary(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
+        public override void WriteDictionary(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
         {
             JsonSerializer.WriteDictionary(ValueConverter, options, ref current, writer);
         }
 
-        internal override void WriteEnumerable(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
+
+        public override void WriteEnumerable(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
         {
             if (ValueConverter != null)
             {
index a3a31d09c6da378c36360e35403c7d6fafc44d53..d2d069965bc6ceb00526baa867b800f8a4278b93 100644 (file)
@@ -19,7 +19,7 @@ namespace System.Text.Json.Serialization
         // should this be cached somewhere else so that it's not populated per TClass as well as TProperty?
         private static readonly Type s_underlyingType = typeof(TProperty);
 
-        internal JsonPropertyInfoNullable(
+        public JsonPropertyInfoNullable(
             Type parentClassType,
             Type declaredPropertyType,
             Type runtimePropertyType,
@@ -30,7 +30,7 @@ namespace System.Text.Json.Serialization
         {
         }
 
-        internal override void Read(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader)
+        public override void Read(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader)
         {
             if (ElementClassInfo != null)
             {
@@ -61,7 +61,7 @@ namespace System.Text.Json.Serialization
             }
         }
 
-        internal override void ReadEnumerable(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader)
+        public override void ReadEnumerable(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader)
         {
             if (ValueConverter == null || !ValueConverter.TryRead(typeof(TProperty), ref reader, out TProperty value))
             {
@@ -74,14 +74,14 @@ namespace System.Text.Json.Serialization
             JsonSerializer.ApplyValueToEnumerable(ref nullableValue, options, ref state.Current);
         }
 
-        internal override void ApplyNullValue(JsonSerializerOptions options, ref ReadStack state)
+        public override void ApplyNullValue(JsonSerializerOptions options, ref ReadStack state)
         {
             TProperty? nullableValue = null;
             JsonSerializer.ApplyValueToEnumerable(ref nullableValue, options, ref state.Current);
         }
 
         // todo: have the caller check if current.Enumerator != null and call WriteEnumerable of the underlying property directly to avoid an extra virtual call.
-        internal override void Write(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
+        public override void Write(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
         {
             if (current.Enumerator != null)
             {
@@ -126,12 +126,12 @@ namespace System.Text.Json.Serialization
             }
         }
 
-        internal override void WriteDictionary(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
+        public override void WriteDictionary(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
         {
             JsonSerializer.WriteDictionary(ValueConverter, options, ref current, writer);
         }
 
-        internal override void WriteEnumerable(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
+        public override void WriteEnumerable(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
         {
             if (ValueConverter != null)
             {
index 3635039a1856fc286e9eff001868bbf09f5c25a1..08b8d0bfc23959bbaafe30fdf596740221bc8f1a 100644 (file)
@@ -42,8 +42,6 @@ namespace System.Text.Json.Serialization
                         Debug.Assert(state.Current.ReturnValue != default);
                         Debug.Assert(state.Current.JsonClassInfo != default);
 
-                        ReadOnlySpan<byte> propertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
-
                         if (state.Current.IsDictionary())
                         {
                             string keyName = reader.GetString();
@@ -57,6 +55,14 @@ namespace System.Text.Json.Serialization
                         }
                         else
                         {
+                            ReadOnlySpan<byte> propertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
+                            if (reader._stringHasEscaping)
+                            {
+                                int idx = propertyName.IndexOf(JsonConstants.BackSlash);
+                                Debug.Assert(idx != -1);
+                                propertyName = GetUnescapedString(propertyName, idx);
+                            }
+
                             state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(options, propertyName, ref state.Current);
                             if (state.Current.JsonPropertyInfo == null)
                             {
@@ -100,5 +106,28 @@ namespace System.Text.Json.Serialization
 
             return;
         }
+
+        private static ReadOnlySpan<byte> GetUnescapedString(ReadOnlySpan<byte> utf8Source, int idx)
+        {
+            // The escaped name is always longer than the unescaped, so it is safe to use escaped name for the buffer length.
+            int length = utf8Source.Length;
+            byte[] pooledName = null;
+
+            Span<byte> unescapedName = length <= JsonConstants.StackallocThreshold ?
+                stackalloc byte[length] :
+                (pooledName = ArrayPool<byte>.Shared.Rent(length));
+
+            JsonReaderHelper.Unescape(utf8Source, unescapedName, idx, out int written);
+            ReadOnlySpan<byte> propertyName = unescapedName.Slice(0, written).ToArray();
+
+            if (pooledName != null)
+            {
+                // We clear the array because it is "user data" (although a property name).
+                new Span<byte>(pooledName, 0, written).Clear();
+                ArrayPool<byte>.Shared.Return(pooledName);
+            }
+
+            return propertyName;
+        }
     }
 }
index d826a685172051538051c5fe8f3883caef7df1a0..bcacc6fe8b57926bbb30dc65fae6964d04847050 100644 (file)
@@ -119,6 +119,11 @@ namespace System.Text.Json.Serialization
             }
             else
             {
+#if true
+                // temporary behavior until the writer can accept escaped string.
+                byte[] utf8Key = Encoding.UTF8.GetBytes(key);
+                converter.Write(utf8Key, value, writer);
+#else
                 byte[] pooledKey = null;
                 byte[] utf8Key = Encoding.UTF8.GetBytes(key);
                 int length = JsonWriterHelper.GetMaxEscapedLength(utf8Key.Length, 0);
@@ -133,8 +138,11 @@ namespace System.Text.Json.Serialization
 
                 if (pooledKey != null)
                 {
+                    // We clear the array because it is "user data" (although a property name).
+                    new Span<byte>(pooledKey, 0, written).Clear();
                     ArrayPool<byte>.Shared.Return(pooledKey);
                 }
+#endif
             }
         }
     }
index 0d984b96cc5a1c25231e703935346be42c73175f..13c7a09351f4e0df6761ab59c5ca3237fe9e6c81 100644 (file)
@@ -55,5 +55,41 @@ namespace System.Text.Json.Serialization.Tests
             // todo: this should throw a JsonReaderException
             Assert.Throws<ArgumentException>(() => JsonSerializer.Parse<Dictionary<string, string>>(@"{""Hello"":""World"", ""Hello"":""World""}"));
         }
+
+        [Fact]
+        public static void UnicodePropertyNames()
+        {
+            {
+                Dictionary<string, int> obj = JsonSerializer.Parse<Dictionary<string, int>>(@"{""Aѧ"":1}");
+                Assert.Equal(1, obj["Aѧ"]);
+
+                // Verify the name is escaped after serialize.
+                string json = JsonSerializer.ToString(obj);
+                Assert.Equal(@"{""A\u0467"":1}", json);
+            }
+
+            {
+                // We want to go over StackallocThreshold=256 to force a pooled allocation, so this property is 200 chars and 400 bytes.
+                const int charsInProperty = 200;
+
+                string longPropertyName = new string('ѧ', charsInProperty);
+
+                Dictionary<string, int> obj = JsonSerializer.Parse<Dictionary<string, int>>($"{{\"{longPropertyName}\":1}}");
+                Assert.Equal(1, obj[longPropertyName]);
+
+                // Verify the name is escaped after serialize.
+                string json = JsonSerializer.ToString(obj);
+
+                // Duplicate the unicode character 'charsInProperty' times.
+                string longPropertyNameEscaped = new StringBuilder().Insert(0, @"\u0467", charsInProperty).ToString();
+
+                string expectedJson = $"{{\"{longPropertyNameEscaped}\":1}}";
+                Assert.Equal(expectedJson, json);
+
+                // Verify the name is unescaped after deserialize.
+                obj = JsonSerializer.Parse<Dictionary<string, int>>(json);
+                Assert.Equal(1, obj[longPropertyName]);
+            }
+        }
     }
 }
index 0bb3ba169d7a9969add2616d51d3b891f80bfcee..b9de228660fdf7599ac0baf0c4dc2a01a34a356d 100644 (file)
@@ -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.Collections.Generic;
+using System.Text.Json.Tests;
 using Xunit;
 
 namespace System.Text.Json.Serialization.Tests
@@ -226,6 +228,37 @@ namespace System.Text.Json.Serialization.Tests
                 Assert.Equal(1, obj.MyInt1);
             }
         }
+
+        [Fact]
+        public static void UnicodePropertyNames()
+        {
+            {
+                ClassWithUnicodeProperty obj = JsonSerializer.Parse<ClassWithUnicodeProperty>(@"{""Aѧ"":1}");
+                Assert.Equal(1, obj.Aѧ);
+
+                // Verify the name is escaped after serialize.
+                string json = JsonSerializer.ToString(obj);
+                Assert.Contains(@"""A\u0467"":1", json);
+
+                // Verify the name is unescaped after deserialize.
+                obj = JsonSerializer.Parse<ClassWithUnicodeProperty>(json);
+                Assert.Equal(1, obj.Aѧ);
+            }
+
+            {
+                // We want to go over StackallocThreshold=256 to force a pooled allocation, so this property is 400 chars and 401 bytes.
+                ClassWithUnicodeProperty obj = JsonSerializer.Parse<ClassWithUnicodeProperty>(@"{""Aѧ34567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"":1}");
+                Assert.Equal(1, obj.Aѧ34567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890);
+
+                // Verify the name is escaped after serialize.
+                string json = JsonSerializer.ToString(obj);
+                Assert.Contains(@"""A\u046734567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"":1", json);
+
+                // Verify the name is unescaped after deserialize.
+                obj = JsonSerializer.Parse<ClassWithUnicodeProperty>(json);
+                Assert.Equal(1, obj.Aѧ34567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890);
+            }
+        }
     }
 
     public class OverridePropertyNameDesignTime_TestClass
index 826e1d478a31b20332329d37de243a23cd0f7bc3..1bc8df11ed955f3bfcaa7c5e754fc91d98b38270 100644 (file)
@@ -962,4 +962,12 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Equal(98052, mainSite.zip);
         }
     }
+
+    public class ClassWithUnicodeProperty
+    {
+        public int Aѧ { get; set; }
+
+        // A 400 character property name with a unicode character making it 401 bytes.
+        public int Aѧ34567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 { get; set; }
+    }
 }