Add extension data support to Json serializer (dotnet/corefx#37690)
authorSteve Harter <steveharter@users.noreply.github.com>
Fri, 17 May 2019 14:56:47 +0000 (07:56 -0700)
committerGitHub <noreply@github.com>
Fri, 17 May 2019 14:56:47 +0000 (07:56 -0700)
Commit migrated from https://github.com/dotnet/corefx/commit/71487a4a5bf7c0043cca4672ff3352c098dabd8c

34 files changed:
src/libraries/System.Text.Json/ref/System.Text.Json.cs
src/libraries/System.Text.Json/src/Resources/Strings.resx
src/libraries/System.Text.Json/src/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs
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/JsonExtensionDataAttribute.cs [new file with mode: 0644]
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.HandleArray.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleNull.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleObject.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.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/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleEnumerable.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs
src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs
src/libraries/System.Text.Json/tests/Serialization/Array.ReadTests.cs
src/libraries/System.Text.Json/tests/Serialization/Array.WriteTests.cs
src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs
src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs
src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs
src/libraries/System.Text.Json/tests/Serialization/TestClasses.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj

index fe1345d..47fbbcb 100644 (file)
@@ -351,6 +351,11 @@ namespace System.Text.Json.Serialization
         protected JsonAttribute() { }
     }
     [System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false)]
+    public sealed partial class JsonExtensionDataAttribute : System.Text.Json.Serialization.JsonAttribute
+    {
+        public JsonExtensionDataAttribute() { }
+    }
+    [System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false)]
     public sealed partial class JsonIgnoreAttribute : System.Text.Json.Serialization.JsonAttribute
     {
         public JsonIgnoreAttribute() { }
index dc3eb4a..52c72c4 100644 (file)
   <data name="DeserializeTypeNotSupported" xml:space="preserve">
     <value>Deserialization of type {0} is not supported.</value>
   </data>
+  <data name="SerializationDataExtensionPropertyInvalid" xml:space="preserve">
+    <value>The data extension property '{0}.{1}' does not match the required signature of IDictionary&lt;string, JsonElement&gt; or IDictionary&lt;string, object&gt;.</value>
+  </data>
+  <data name="SerializationDuplicateAttribute" xml:space="preserve">
+    <value>A class cannot have more than one property that has the attribute '{0}'.</value>
+  </data>
+  <data name="SerializationNotSupportedCollectionType" xml:space="preserve">
+    <value>The collection type '{0}' is not supported.</value>
+  </data>
+  <data name="SerializationDataExtensionPropertyInvalidElement" xml:space="preserve">
+    <value>The data extension property '{0}.{1}' cannot contain dictionary values of type '{2}'. Dictionary values must be of type JsonElement.</value>
+  </data>
+  <data name="SerializationNotSupportedCollection" xml:space="preserve">
+    <value>The collection type '{0}' on '{1}' is not supported.</value>
+  </data>
 </root>
\ No newline at end of file
index 15106dc..04c2ed9 100644 (file)
@@ -76,6 +76,7 @@
     <Compile Include="System\Text\Json\Serialization\JsonClassInfo.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonClassInfo.AddProperty.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonEnumerableConverter.cs" />
+    <Compile Include="System\Text\Json\Serialization\JsonExtensionDataAttribute.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonIgnoreAttribute.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonNamingPolicy.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonPropertyInfo.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonPropertyInfoNullable.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonPropertyNameAttribute.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.HandleArray.cs" />
+    <Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.HandleDictionary.cs" />
+    <Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.HandleObject.cs" />
+    <Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.HandlePropertyName.cs" />
+    <Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.HandleValue.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.Helpers.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.Stream.cs" />
-    <Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.HandleValue.cs" />
-    <Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.HandleObject.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.String.cs" />
-    <Compile Include="System\Text\Json\Serialization\JsonSerializer.Write.Helpers.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.HandleNull.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.Span.cs" />
-    <Compile Include="System\Text\Json\Serialization\JsonSerializer.Write.ByteArray.cs" />
-    <Compile Include="System\Text\Json\Serialization\JsonSerializer.Write.Stream.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonSerializer.Write.cs" />
+    <Compile Include="System\Text\Json\Serialization\JsonSerializer.Write.ByteArray.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonSerializer.Write.HandleDictionary.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonSerializer.Write.HandleEnumerable.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonSerializer.Write.HandleObject.cs" />
+    <Compile Include="System\Text\Json\Serialization\JsonSerializer.Write.Helpers.cs" />
+    <Compile Include="System\Text\Json\Serialization\JsonSerializer.Write.Stream.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonSerializer.Write.String.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonSerializerOptions.cs" />
     <Compile Include="System\Text\Json\Serialization\Policies\JsonValueConverter.cs" />
index 251fe29..4485067 100644 (file)
@@ -77,5 +77,19 @@ namespace System.Text.Json
         /// Otherwise, returns <see langword="false"/>.
         /// </summary>
         public static bool IsDigit(byte value) => (uint)(value - '0') <= '9' - '0';
+
+        /// <summary>
+        /// Calls Encoding.UTF8.GetString that supports netstandard.
+        /// </summary>
+        /// <param name="bytes">The utf8 bytes to convert.</param>
+        /// <returns></returns>
+        internal static string Utf8GetString(ReadOnlySpan<byte> bytes)
+        {
+            return Encoding.UTF8.GetString(bytes
+#if netstandard
+                        .ToArray()
+#endif
+                );
+        }
     }
 }
index 05b9ca1..c757848 100644 (file)
@@ -54,13 +54,19 @@ namespace System.Text.Json.Serialization
 
         internal static JsonPropertyInfo CreateProperty(Type declaredPropertyType, Type runtimePropertyType, PropertyInfo propertyInfo, Type parentClassType, JsonSerializerOptions options)
         {
+            bool hasIgnoreAttribute = (JsonPropertyInfo.GetAttribute<JsonIgnoreAttribute>(propertyInfo) != null);
+            if (hasIgnoreAttribute)
+            {
+                return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(propertyInfo, options);
+            }
+
             Type collectionElementType = null;
             switch (GetClassType(runtimePropertyType))
             {
                 case ClassType.Enumerable:
                 case ClassType.Dictionary:
                 case ClassType.Unknown:
-                    collectionElementType = GetElementType(runtimePropertyType);
+                    collectionElementType = GetElementType(runtimePropertyType, parentClassType, propertyInfo);
                     break;
             }
 
@@ -79,10 +85,12 @@ namespace System.Text.Json.Serialization
             JsonPropertyInfo jsonInfo = (JsonPropertyInfo)Activator.CreateInstance(
                 propertyInfoClassType,
                 BindingFlags.Instance | BindingFlags.Public,
-                binder: null,
-                new object[] { parentClassType, declaredPropertyType, runtimePropertyType, propertyInfo, collectionElementType, options },
+                binder: null, 
+                args: null,
                 culture: null);
 
+            jsonInfo.Initialize(parentClassType, declaredPropertyType, runtimePropertyType, propertyInfo, collectionElementType, options);
+
             return jsonInfo;
         }
 
index 8b53fce..d993307 100644 (file)
@@ -11,7 +11,7 @@ using System.Text.Json.Serialization.Converters;
 
 namespace System.Text.Json.Serialization
 {
-    [DebuggerDisplay("ClassType.{ClassType} {Type.Name}")]
+    [DebuggerDisplay("ClassType.{ClassType}, {Type.Name}")]
     internal sealed partial class JsonClassInfo
     {
         // The length of the property name embedded in the key (in bytes).
@@ -27,6 +27,8 @@ namespace System.Text.Json.Serialization
 
         internal ClassType ClassType { get; private set; }
 
+        public JsonPropertyInfo DataExtensionProperty { get; private set; }
+
         // If enumerable, the JsonClassInfo for the element type.
         internal JsonClassInfo ElementClassInfo { get; private set; }
 
@@ -38,6 +40,8 @@ namespace System.Text.Json.Serialization
             // Todo: when using PropertyNameCaseInsensitive we also need to use the hashtable with case-insensitive
             // comparison to handle Turkish etc. cultures properly.
 
+            Debug.Assert(_propertyRefs != null);
+
             // Set the sorted property cache. Overwrite any existing cache which can occur in multi-threaded cases.
             if (frame.PropertyRefCache != null)
             {
@@ -51,9 +55,9 @@ namespace System.Text.Json.Serialization
                     for (int iProperty = 0; iProperty < _propertyRefs.Count; iProperty++)
                     {
                         PropertyRef propertyRef = _propertyRefs[iProperty];
-
                         bool found = false;
                         int iCacheProperty = 0;
+
                         for (; iCacheProperty < cache.Count; iCacheProperty++)
                         {
                             if (IsPropertyRefEqual(ref propertyRef, cache[iCacheProperty]))
@@ -99,10 +103,7 @@ namespace System.Text.Json.Serialization
                         {
                             JsonPropertyInfo jsonPropertyInfo = AddProperty(propertyInfo.PropertyType, propertyInfo, type, options);
 
-                            if (jsonPropertyInfo.NameAsString == null)
-                            {
-                                ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameNull(this, jsonPropertyInfo);
-                            }
+                            Debug.Assert(jsonPropertyInfo.NameUsedToCompareAsString != null);
 
                             // If the JsonPropertyNameAttribute or naming policy results in collisions, throw an exception.
                             if (!propertyNames.Add(jsonPropertyInfo.NameUsedToCompareAsString))
@@ -113,7 +114,10 @@ namespace System.Text.Json.Serialization
                             jsonPropertyInfo.ClearUnusedValuesAfterAdd();
                         }
                     }
+
+                    DetermineExtensionDataProperty();
                     break;
+
                 case ClassType.Enumerable:
                 case ClassType.Dictionary:
                     // Add a single property that maps to the class type so we can have policies applied.
@@ -123,7 +127,7 @@ namespace System.Text.Json.Serialization
                     CreateObject = options.ClassMaterializerStrategy.CreateConstructor(policyProperty.RuntimePropertyType);
 
                     // Create a ClassInfo that maps to the element type which is used for (de)serialization and policies.
-                    Type elementType = GetElementType(type);
+                    Type elementType = GetElementType(type, parentType : null, memberInfo: null);
                     ElementClassInfo = options.GetOrAddClass(elementType);
                     break;
                 case ClassType.Value:
@@ -137,12 +141,53 @@ namespace System.Text.Json.Serialization
             }
         }
 
+        private void DetermineExtensionDataProperty()
+        {
+            JsonPropertyInfo jsonPropertyInfo = GetPropertyThatHasAttribute(typeof(JsonExtensionDataAttribute));
+            if (jsonPropertyInfo != null)
+            {
+                Type declaredPropertyType = jsonPropertyInfo.DeclaredPropertyType;
+                if (!typeof(IDictionary<string, JsonElement>).IsAssignableFrom(declaredPropertyType) &&
+                    !typeof(IDictionary<string, object>).IsAssignableFrom(declaredPropertyType))
+                {
+                    ThrowHelper.ThrowInvalidOperationException_SerializationDataExtensionPropertyInvalid(this, jsonPropertyInfo);
+                }
+
+                DataExtensionProperty = jsonPropertyInfo;
+            }
+        }
+
+        private JsonPropertyInfo GetPropertyThatHasAttribute(Type attributeType)
+        {
+            Debug.Assert(_propertyRefs != null);
+
+            JsonPropertyInfo property = null;
+
+            for (int iProperty = 0; iProperty < _propertyRefs.Count; iProperty++)
+            {
+                PropertyRef propertyRef = _propertyRefs[iProperty];
+                JsonPropertyInfo jsonPropertyInfo = propertyRef.Info;
+                Attribute attribute = jsonPropertyInfo.PropertyInfo.GetCustomAttribute(attributeType);
+                if (attribute != null)
+                {
+                    if (property != null)
+                    {
+                        ThrowHelper.ThrowInvalidOperationException_SerializationDuplicateAttribute(attributeType);
+                    }
+
+                    property = jsonPropertyInfo;
+                }
+            }
+
+            return property;
+        }
+
         internal JsonPropertyInfo GetProperty(JsonSerializerOptions options, ReadOnlySpan<byte> propertyName, ref ReadStackFrame frame)
         {
             // If we should compare with case-insensitive, normalize to an uppercase format since that is what is cached on the propertyInfo.
             if (options.PropertyNameCaseInsensitive)
             {
-                string utf16PropertyName = Encoding.UTF8.GetString(propertyName.ToArray());
+                string utf16PropertyName = JsonHelpers.Utf8GetString(propertyName);
                 string upper = utf16PropertyName.ToUpperInvariant();
                 propertyName = Encoding.UTF8.GetBytes(upper);
             }
@@ -200,10 +245,9 @@ namespace System.Text.Json.Serialization
 
             if (!hasPropertyCache)
             {
-                if (propertyIndex == 0)
+                if (propertyIndex == 0 && frame.PropertyRefCache == null)
                 {
                     // Create the temporary list on first property access to prevent a partially filled List.
-                    Debug.Assert(frame.PropertyRefCache == null);
                     frame.PropertyRefCache = new List<PropertyRef>();
                 }
 
@@ -258,7 +302,7 @@ namespace System.Text.Json.Serialization
             if (propertyRef.Key == other.Key)
             {
                 if (propertyRef.Info.Name.Length <= PropertyNameKeyLength ||
-                    propertyRef.Info.Name.SequenceEqual(other.Info.Name))
+                    propertyRef.Info.Name.AsSpan().SequenceEqual(other.Info.Name.AsSpan()))
                 {
                     return true;
                 }
@@ -308,37 +352,41 @@ namespace System.Text.Json.Serialization
             return key;
         }
 
-        public static Type GetElementType(Type propertyType)
+        // Return the element type of the IEnumerable, or return null if not an IEnumerable.
+        public static Type GetElementType(Type propertyType, Type parentType, MemberInfo memberInfo)
         {
-            Type elementType = null;
-            if (typeof(IEnumerable).IsAssignableFrom(propertyType))
+            if (!typeof(IEnumerable).IsAssignableFrom(propertyType))
+            {
+                return null;
+            }
+
+            // Check for Array.
+            Type elementType = propertyType.GetElementType();
+            if (elementType != null)
+            {
+                return elementType;
+            }
+
+            // Check for Dictionary<TKey, TValue> or IEnumerable<T>
+            if (propertyType.IsGenericType)
             {
-                elementType = propertyType.GetElementType();
-                if (elementType == null)
+                Type[] args = propertyType.GetGenericArguments();
+                ClassType classType = GetClassType(propertyType);
+
+                if (classType == ClassType.Dictionary &&
+                    args.Length >= 2 && // It is >= 2 in case there is a IDictionary<TKey, TValue, TSomeExtension>.
+                    args[0].UnderlyingSystemType == typeof(string))
                 {
-                    Type[] args = propertyType.GetGenericArguments();
+                    return args[1];
+                }
 
-                    if (propertyType.IsGenericType)
-                    {
-                        if (GetClassType(propertyType) == ClassType.Dictionary &&
-                            args.Length >= 2) // It is >= 2 in case there is a Dictionary<TKey, TValue, TSomeExtension>.
-                        {
-                            elementType = args[1];
-                        }
-                        else if (GetClassType(propertyType) == ClassType.Enumerable && args.Length >= 1) // It is >= 1 in case there is an IEnumerable<T, TSomeExtension>.
-                        {
-                            elementType = args[0];
-                        }
-                    }
-                    else
-                    {
-                        // Unable to determine collection type; attempt to use object which will be used to create loosely-typed collection.
-                        elementType = typeof(object);
-                    }
+                if (classType == ClassType.Enumerable && args.Length >= 1) // It is >= 1 in case there is an IEnumerable<T, TSomeExtension>.
+                {
+                    return args[0];
                 }
             }
 
-            return elementType;
+            throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(propertyType, parentType, memberInfo);
         }
 
         internal static ClassType GetClassType(Type type)
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonExtensionDataAttribute.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonExtensionDataAttribute.cs
new file mode 100644 (file)
index 0000000..9e6dcd8
--- /dev/null
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System.Text.Json.Serialization
+{
+    /// <summary>
+    /// When placed on a property of type <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/>, any
+    /// properties that do not have a matching member are added to that Dictionary during deserialization and written during serialization.
+    /// </summary>
+    /// <remarks>
+    /// The TKey value must be <see cref="string"/> and TValue must be <see cref="JsonElement"/> or <see cref="object"/>.
+    /// If there is more than one extension property on a type, or it the property is not of the correct type,
+    /// an <see cref="InvalidOperationException"/> is thrown during the first serialization or deserialization of that type.
+    /// </remarks>
+    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
+    public sealed class JsonExtensionDataAttribute : JsonAttribute
+    {
+    }
+}
index 8c429bc..6160df0 100644 (file)
@@ -11,7 +11,7 @@ using System.Text.Json.Serialization.Policies;
 
 namespace System.Text.Json.Serialization
 {
-    [DebuggerDisplay("{PropertyInfo}")]
+    [DebuggerDisplay("PropertyInfo={PropertyInfo}, Element={ElementClassInfo}")]
     internal abstract class JsonPropertyInfo
     {
         // Cache the converters so they don't get created for every enumerable property.
@@ -20,35 +20,33 @@ namespace System.Text.Json.Serialization
         private static readonly JsonEnumerableConverter s_jsonIEnumerableConstuctibleConverter = new DefaultIEnumerableConstructibleConverter();
         private static readonly JsonEnumerableConverter s_jsonImmutableConverter = new DefaultImmutableConverter();
 
+        public static readonly JsonPropertyInfo s_missingProperty = new JsonPropertyInfoNotNullable<object, object, object>();
+
         public ClassType ClassType;
 
         // The name of the property with any casing policy or the name specified from JsonPropertyNameAttribute.
-        private byte[] _name { get; set; }
-        public ReadOnlySpan<byte> Name => _name;
+        public byte[] Name { get; private set; }
         public string NameAsString { get; private set; }
 
         // Used to support case-insensitive comparison
-        private byte[] _nameUsedToCompare { get; set; }
-        public ReadOnlySpan<byte> NameUsedToCompare => _nameUsedToCompare;
+        public byte[] NameUsedToCompare { get; private set; }
         public string NameUsedToCompareAsString { get; private set; }
 
         // The escaped name passed to the writer.
-        public byte[] _escapedName { get; private set; }
+        public byte[] EscapedName { 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; }
 
+        public bool IsPropertyPolicy {get; protected set;}
         public bool IgnoreNullValues { get; private set; }
 
         // todo: to minimize hashtable lookups, cache JsonClassInfo:
         //public JsonClassInfo ClassInfo;
 
-        // Constructor used for internal identifiers
-        public JsonPropertyInfo() { }
-
-        public JsonPropertyInfo(
+        public virtual void Initialize(
             Type parentClassType,
             Type declaredPropertyType,
             Type runtimePropertyType,
@@ -94,100 +92,95 @@ namespace System.Text.Json.Serialization
 
         private void DeterminePropertyName(JsonSerializerOptions options)
         {
-            if (PropertyInfo != null)
+            if (PropertyInfo == null)
             {
-                JsonPropertyNameAttribute nameAttribute = GetAttribute<JsonPropertyNameAttribute>();
-                if (nameAttribute != null)
-                {
-                    NameAsString = nameAttribute.Name;
+                return;
+            }
 
-                    // null is not valid; JsonClassInfo throws an InvalidOperationException after this return.
-                    if (NameAsString == null)
-                    {
-                        return;
-                    }
-                }
-                else if (options.PropertyNamingPolicy != null)
+            JsonPropertyNameAttribute nameAttribute = GetAttribute<JsonPropertyNameAttribute>(PropertyInfo);
+            if (nameAttribute != null)
+            {
+                string name = nameAttribute.Name;
+                if (name == null)
                 {
-                    NameAsString = options.PropertyNamingPolicy.ConvertName(PropertyInfo.Name);
-
-                    // null is not valid; JsonClassInfo throws an InvalidOperationException after this return.
-                    if (NameAsString == null)
-                    {
-                        return;
-                    }
+                    ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameNull(ParentClassType, this);
                 }
-                else
+
+                NameAsString = name;
+            }
+            else if (options.PropertyNamingPolicy != null)
+            {
+                string name = options.PropertyNamingPolicy.ConvertName(PropertyInfo.Name);
+                if (name == null)
                 {
-                    NameAsString = PropertyInfo.Name;
+                    ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameNull(ParentClassType, this);
                 }
 
-                // At this point propertyName is valid UTF16, so just call the simple UTF16->UTF8 encoder.
-                _name = Encoding.UTF8.GetBytes(NameAsString);
+                NameAsString = name;
+            }
+            else
+            {
+                NameAsString = PropertyInfo.Name;
+            }
 
-                // Set the compare name.
-                if (options.PropertyNameCaseInsensitive)
-                {
-                    NameUsedToCompareAsString = NameAsString.ToUpperInvariant();
-                    _nameUsedToCompare = Encoding.UTF8.GetBytes(NameUsedToCompareAsString);
-                }
-                else
-                {
-                    NameUsedToCompareAsString = NameAsString;
-                    _nameUsedToCompare = _name;
-                }
+            Debug.Assert(NameAsString != null);
+
+            // At this point propertyName is valid UTF16, so just call the simple UTF16->UTF8 encoder.
+            Name = Encoding.UTF8.GetBytes(NameAsString);
 
-                // Cache the escaped name.
+            // Set the compare name.
+            if (options.PropertyNameCaseInsensitive)
+            {
+                NameUsedToCompareAsString = NameAsString.ToUpperInvariant();
+                NameUsedToCompare = Encoding.UTF8.GetBytes(NameUsedToCompareAsString);
+            }
+            else
+            {
+                NameUsedToCompareAsString = NameAsString;
+                NameUsedToCompare = Name;
+            }
+
+            // Cache the escaped name.
 #if true
-                // temporary behavior until the writer can accept escaped string.
-                _escapedName = _name;
+            // temporary behavior until the writer can accept escaped string.
+            EscapedName = Name;
 #else
-                
-                int valueIdx = JsonWriterHelper.NeedsEscaping(_name);
-                if (valueIdx == -1)
-                {
-                    _escapedName = _name;
-                }
-                else
-                {
-                    byte[] pooledName = null;
-                    int length = JsonWriterHelper.GetMaxEscapedLength(_name.Length, valueIdx);
+            int valueIdx = JsonWriterHelper.NeedsEscaping(_name);
+            if (valueIdx == -1)
+            {
+                _escapedName = _name;
+            }
+            else
+            {
+                byte[] pooledName = null;
+                int length = JsonWriterHelper.GetMaxEscapedLength(_name.Length, valueIdx);
 
-                    Span<byte> escapedName = length <= JsonConstants.StackallocThreshold ?
-                        stackalloc byte[length] :
-                        (pooledName = 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, escapedName, 0, out int written);
 
-                    _escapedName = escapedName.Slice(0, written).ToArray();
+                _escapedName = escapedName.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);
-                    }
+                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
             }
+#endif
         }
 
         private void DetermineSerializationCapabilities(JsonSerializerOptions options)
         {
-            bool hasIgnoreAttribute = (GetAttribute<JsonIgnoreAttribute>() != null);
-
-            if (hasIgnoreAttribute)
-            {
-                // We don't serialize or deserialize.
-                return;
-            }
-
-            if (ClassType != ClassType.Enumerable)
+            if (ClassType != ClassType.Enumerable && ClassType != ClassType.Dictionary)
             {
-                // We serialize if there is a getter + no [Ignore] attribute + not ignoring readonly properties.
+                // We serialize if there is a getter + not ignoring readonly properties.
                 ShouldSerialize = HasGetter && (HasSetter || !options.IgnoreReadOnlyProperties);
 
-                // We deserialize if there is a setter + no [Ignore] attribute
+                // We deserialize if there is a setter. 
                 ShouldDeserialize = HasSetter;
             }
             else
@@ -198,7 +191,8 @@ namespace System.Text.Json.Serialization
                     {
                         ShouldDeserialize = true;
                     }
-                    else if (RuntimePropertyType.IsAssignableFrom(typeof(IList)))
+                    else if (!RuntimePropertyType.IsArray &&
+                        (typeof(IList).IsAssignableFrom(RuntimePropertyType) || typeof(IDictionary).IsAssignableFrom(RuntimePropertyType)))
                     {
                         ShouldDeserialize = true;
                     }
@@ -218,7 +212,7 @@ namespace System.Text.Json.Serialization
                     }
                     else if (typeof(IEnumerable).IsAssignableFrom(RuntimePropertyType))
                     {
-                        Type elementType = JsonClassInfo.GetElementType(RuntimePropertyType);
+                        Type elementType = JsonClassInfo.GetElementType(RuntimePropertyType, ParentClassType, PropertyInfo);
 
                         // If the property type only has interface(s) exposed by JsonEnumerableT<T> then use JsonEnumerableT as the converter.
                         if (RuntimePropertyType.IsAssignableFrom(typeof(JsonEnumerableT<>).MakeGenericType(elementType)))
@@ -260,16 +254,30 @@ namespace System.Text.Json.Serialization
         // Copy any settings defined at run-time to the new property.
         public void CopyRuntimeSettingsTo(JsonPropertyInfo other)
         {
-            other._name = _name;
-            other._nameUsedToCompare = _nameUsedToCompare;
-            other._escapedName = _escapedName;
+            other.Name = Name;
+            other.NameUsedToCompare = NameUsedToCompare;
+            other.EscapedName = EscapedName;
+        }
+
+        // Create a property that is either ignored at run-time. It uses typeof(int) in order to prevent
+        // issues with unsupported types and helps ensure we don't accidently (de)serialize it.
+        public static JsonPropertyInfo CreateIgnoredPropertyPlaceholder(PropertyInfo propertyInfo, JsonSerializerOptions options)
+        {
+            JsonPropertyInfo jsonPropertyInfo = new JsonPropertyInfoNotNullable<int, int, int>();
+            jsonPropertyInfo.PropertyInfo = propertyInfo;
+            jsonPropertyInfo.DeterminePropertyName(options);
+
+            Debug.Assert(!jsonPropertyInfo.ShouldDeserialize);
+            Debug.Assert(!jsonPropertyInfo.ShouldSerialize);
+
+            return jsonPropertyInfo;
         }
 
         public abstract object GetValueAsObject(object obj);
 
-        public TAttribute GetAttribute<TAttribute>() where TAttribute : Attribute
+        public static TAttribute GetAttribute<TAttribute>(PropertyInfo propertyInfo) where TAttribute : Attribute
         {
-            return (TAttribute)PropertyInfo?.GetCustomAttribute(typeof(TAttribute), inherit: false);
+            return (TAttribute)propertyInfo?.GetCustomAttribute(typeof(TAttribute), inherit: false);
         }
 
         public abstract IEnumerable CreateImmutableCollectionFromList(string delegateKey, IList sourceList);
index 708f2d9..ba67427 100644 (file)
@@ -16,24 +16,21 @@ namespace System.Text.Json.Serialization
     /// </summary>
     internal abstract class JsonPropertyInfoCommon<TClass, TDeclaredProperty, TRuntimeProperty> : JsonPropertyInfo
     {
-        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
-        public JsonPropertyInfoCommon() { }
-
-        public JsonPropertyInfoCommon(
+        public override void Initialize(
             Type parentClassType,
             Type declaredPropertyType,
             Type runtimePropertyType,
             PropertyInfo propertyInfo,
             Type elementType,
-            JsonSerializerOptions options) :
-            base(parentClassType, declaredPropertyType, runtimePropertyType, propertyInfo, elementType, options)
+            JsonSerializerOptions options)
         {
+            base.Initialize(parentClassType, declaredPropertyType, runtimePropertyType, propertyInfo, elementType, options);
+
             if (propertyInfo != null)
             {
                 if (propertyInfo.GetMethod?.IsPublic == true)
@@ -50,7 +47,7 @@ namespace System.Text.Json.Serialization
             }
             else
             {
-                _isPropertyPolicy = true;
+                IsPropertyPolicy = true;
                 HasGetter = true;
                 HasSetter = true;
                 ValueConverter = DefaultConverters<TRuntimeProperty>.s_converter;
@@ -67,7 +64,7 @@ namespace System.Text.Json.Serialization
 
         public override object GetValueAsObject(object obj)
         {
-            if (_isPropertyPolicy)
+            if (IsPropertyPolicy)
             {
                 return obj;
             }
index 9c95b56..fa326d0 100644 (file)
@@ -4,7 +4,6 @@
 
 using System.Collections.Generic;
 using System.Diagnostics;
-using System.Reflection;
 
 namespace System.Text.Json.Serialization
 {
@@ -15,48 +14,33 @@ namespace System.Text.Json.Serialization
         JsonPropertyInfoCommon<TClass, TDeclaredProperty, TRuntimeProperty>
         where TRuntimeProperty : TDeclaredProperty
     {
-        // Constructor used for internal identifiers
-        public JsonPropertyInfoNotNullable() { }
-
-        public JsonPropertyInfoNotNullable(
-            Type parentClassType,
-            Type declaredPropertyType,
-            Type runtimePropertyType,
-            PropertyInfo propertyInfo,
-            Type elementType,
-            JsonSerializerOptions options) :
-            base(parentClassType, declaredPropertyType, runtimePropertyType, propertyInfo, elementType, options)
-        {
-        }
-
         public override void Read(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader)
         {
+            Debug.Assert(ShouldDeserialize);
+
             if (ElementClassInfo != null)
             {
                 // Forward the setter to the value-based JsonPropertyInfo.
                 JsonPropertyInfo propertyInfo = ElementClassInfo.GetPolicyProperty();
                 propertyInfo.ReadEnumerable(tokenType, options, ref state, ref reader);
             }
-            else if (ShouldDeserialize)
+            else
             {
-                if (ValueConverter != null)
+                if (ValueConverter != null && ValueConverter.TryRead(RuntimePropertyType, ref reader, out TRuntimeProperty value))
                 {
-                    if (ValueConverter.TryRead(RuntimePropertyType, ref reader, out TRuntimeProperty value))
+                    if (state.Current.ReturnValue == null)
                     {
-                        if (state.Current.ReturnValue == null)
-                        {
-                            state.Current.ReturnValue = value;
-                        }
-                        else
-                        {
-                            // Null values were already handled.
-                            Debug.Assert(value != null);
-
-                            Set((TClass)state.Current.ReturnValue, value);
-                        }
-
-                        return;
+                        state.Current.ReturnValue = value;
                     }
+                    else
+                    {
+                        // Null values were already handled.
+                        Debug.Assert(value != null);
+
+                        Set((TClass)state.Current.ReturnValue, value);
+                    }
+
+                    return;
                 }
 
                 ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType, reader, state.PropertyPath);
@@ -66,61 +50,64 @@ namespace System.Text.Json.Serialization
         // If this method is changed, also change JsonPropertyInfoNullable.ReadEnumerable and JsonSerializer.ApplyObjectToEnumerable
         public override void ReadEnumerable(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader)
         {
+            Debug.Assert(ShouldDeserialize);
+
             if (ValueConverter == null || !ValueConverter.TryRead(RuntimePropertyType, ref reader, out TRuntimeProperty value))
             {
                 ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType, reader, state.PropertyPath);
                 return;
             }
 
-            JsonSerializer.ApplyValueToEnumerable(ref value, options, ref state, ref reader);
+            JsonSerializer.ApplyValueToEnumerable(ref value, ref state, ref reader);
         }
 
         public override void Write(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
         {
             Debug.Assert(current.Enumerator == null);
+            Debug.Assert(ShouldSerialize);
 
-            if (ShouldSerialize)
+            TRuntimeProperty value;
+            if (IsPropertyPolicy)
             {
-                TRuntimeProperty value;
-                if (_isPropertyPolicy)
-                {
-                    value = (TRuntimeProperty)current.CurrentValue;
-                }
-                else
+                value = (TRuntimeProperty)current.CurrentValue;
+            }
+            else
+            {
+                value = (TRuntimeProperty)Get((TClass)current.CurrentValue);
+            }
+
+            if (value == null)
+            {
+                Debug.Assert(EscapedName != null);
+
+                if (!IgnoreNullValues)
                 {
-                    value = (TRuntimeProperty)Get((TClass)current.CurrentValue);
+                    writer.WriteNull(EscapedName);
                 }
-
-                if (value == null)
+            }
+            else if (ValueConverter != null)
+            {
+                if (EscapedName != null)
                 {
-                    Debug.Assert(_escapedName != null);
-
-                    if (!IgnoreNullValues)
-                    {
-                        writer.WriteNull(_escapedName);
-                    }
+                    ValueConverter.Write(EscapedName, value, writer);
                 }
-                else if (ValueConverter != null)
+                else
                 {
-                    if (_escapedName != null)
-                    {
-                        ValueConverter.Write(_escapedName, value, writer);
-                    }
-                    else
-                    {
-                        ValueConverter.Write(value, writer);
-                    }
+                    ValueConverter.Write(value, writer);
                 }
             }
         }
 
         public override void WriteDictionary(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
         {
+            Debug.Assert(ShouldSerialize);
             JsonSerializer.WriteDictionary(ValueConverter, options, ref current, writer);
         }
 
         public override void WriteEnumerable(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
         {
+            Debug.Assert(ShouldSerialize);
+
             if (ValueConverter != null)
             {
                 Debug.Assert(current.Enumerator != null);
index e276e29..0a476b1 100644 (file)
@@ -4,7 +4,6 @@
 
 using System.Collections.Generic;
 using System.Diagnostics;
-using System.Reflection;
 
 namespace System.Text.Json.Serialization
 {
@@ -18,46 +17,32 @@ 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);
 
-        public JsonPropertyInfoNullable(
-            Type parentClassType,
-            Type declaredPropertyType,
-            Type runtimePropertyType,
-            PropertyInfo propertyInfo,
-            Type elementType,
-            JsonSerializerOptions options) :
-            base(parentClassType, declaredPropertyType, runtimePropertyType, propertyInfo, elementType, options)
-        {
-        }
-
         public override void Read(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader)
         {
             Debug.Assert(ElementClassInfo == null);
+            Debug.Assert(ShouldDeserialize);
 
-            if (ShouldDeserialize)
+            if (ValueConverter != null && ValueConverter.TryRead(s_underlyingType, ref reader, out TProperty value))
             {
-                if (ValueConverter != null)
+                if (state.Current.ReturnValue == null)
                 {
-                    if (ValueConverter.TryRead(s_underlyingType, ref reader, out TProperty value))
-                    {
-                        if (state.Current.ReturnValue == null)
-                        {
-                            state.Current.ReturnValue = value;
-                        }
-                        else
-                        {
-                            Set((TClass)state.Current.ReturnValue, value);
-                        }
-
-                        return;
-                    }
+                    state.Current.ReturnValue = value;
+                }
+                else
+                {
+                    Set((TClass)state.Current.ReturnValue, value);
                 }
 
-                ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType, reader, state.PropertyPath);
+                return;
             }
+
+            ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType, reader, state.PropertyPath);
         }
 
         public override void ReadEnumerable(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader)
         {
+            Debug.Assert(ShouldDeserialize);
+
             if (ValueConverter == null || !ValueConverter.TryRead(typeof(TProperty), ref reader, out TProperty value))
             {
                 ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType, reader, state.PropertyPath);
@@ -65,21 +50,23 @@ namespace System.Text.Json.Serialization
             }
 
             TProperty? nullableValue = new TProperty?(value);
-            JsonSerializer.ApplyValueToEnumerable(ref nullableValue, options, ref state, ref reader);
+            JsonSerializer.ApplyValueToEnumerable(ref nullableValue, ref state, ref reader);
         }
 
         public override void Write(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
         {
+            Debug.Assert(ShouldSerialize);
+
             if (current.Enumerator != null)
             {
                 // Forward the setter to the value-based JsonPropertyInfo.
                 JsonPropertyInfo propertyInfo = ElementClassInfo.GetPolicyProperty();
                 propertyInfo.WriteEnumerable(options, ref current, writer);
             }
-            else if (ShouldSerialize)
+            else
             {
                 TProperty? value;
-                if (_isPropertyPolicy)
+                if (IsPropertyPolicy)
                 {
                     value = (TProperty?)current.CurrentValue;
                 }
@@ -90,18 +77,18 @@ namespace System.Text.Json.Serialization
 
                 if (value == null)
                 {
-                    Debug.Assert(_escapedName != null);
+                    Debug.Assert(EscapedName != null);
 
                     if (!IgnoreNullValues)
                     {
-                        writer.WriteNull(_escapedName);
+                        writer.WriteNull(EscapedName);
                     }
                 }
                 else if (ValueConverter != null)
                 {
-                    if (_escapedName != null)
+                    if (EscapedName != null)
                     {
-                        ValueConverter.Write(_escapedName, value.GetValueOrDefault(), writer);
+                        ValueConverter.Write(EscapedName, value.GetValueOrDefault(), writer);
                     }
                     else
                     {
@@ -113,6 +100,8 @@ namespace System.Text.Json.Serialization
 
         public override void WriteEnumerable(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer)
         {
+            Debug.Assert(ShouldSerialize);
+
             if (ValueConverter != null)
             {
                 Debug.Assert(current.Enumerator != null);
index b36dbdf..8ff408c 100644 (file)
@@ -16,12 +16,9 @@ namespace System.Text.Json.Serialization
             ref Utf8JsonReader reader,
             ref ReadStack state)
         {
-            JsonPropertyInfo jsonPropertyInfo;
+            JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo;
 
-            jsonPropertyInfo = state.Current.JsonPropertyInfo;
-
-            bool skip = jsonPropertyInfo != null && !jsonPropertyInfo.ShouldDeserialize;
-            if (skip || state.Current.Skip())
+            if (state.Current.SkipProperty)
             {
                 // The array is not being applied to the object.
                 state.Push();
@@ -38,27 +35,26 @@ namespace System.Text.Json.Serialization
                 jsonPropertyInfo = state.Current.JsonClassInfo.CreatePolymorphicProperty(jsonPropertyInfo, typeof(object), options);
             }
 
+            // Verify that we don't have a multidimensional array.
             Type arrayType = jsonPropertyInfo.RuntimePropertyType;
             if (!typeof(IEnumerable).IsAssignableFrom(arrayType) || (arrayType.IsArray && arrayType.GetArrayRank() > 1))
             {
                 ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(arrayType, reader, state.PropertyPath);
             }
 
-            Debug.Assert(state.Current.IsPropertyEnumerable || state.Current.IsDictionary);
+            Debug.Assert(state.Current.IsProcessingEnumerableOrDictionary);
 
-            if (state.Current.EnumerableCreated)
+            if (state.Current.PropertyInitialized)
             {
                 // A nested json array so push a new stack frame.
                 Type elementType = state.Current.JsonClassInfo.ElementClassInfo.GetPolicyProperty().RuntimePropertyType;
 
                 state.Push();
-
                 state.Current.Initialize(elementType, options);
-                state.Current.PopStackOnEnd = true;
             }
             else
             {
-                state.Current.EnumerableCreated = true;
+                state.Current.PropertyInitialized = true;
             }
 
             jsonPropertyInfo = state.Current.JsonPropertyInfo;
@@ -85,8 +81,8 @@ namespace System.Text.Json.Serialization
 
         private static bool HandleEndArray(
             JsonSerializerOptions options,
-            ref ReadStack state,
-            ref Utf8JsonReader reader)
+            ref Utf8JsonReader reader,
+            ref ReadStack state)
         {
             bool lastFrame = state.IsLastFrame;
 
@@ -99,7 +95,6 @@ namespace System.Text.Json.Serialization
 
             IEnumerable value = ReadStackFrame.GetEnumerableValue(state.Current);
             bool setPropertyDirectly;
-            bool popStackOnEnd = state.Current.PopStackOnEnd;
 
             if (state.Current.TempEnumerableValues != null)
             {
@@ -109,11 +104,9 @@ namespace System.Text.Json.Serialization
                 value = converter.CreateFromList(ref state, (IList)value, options);
                 setPropertyDirectly = true;
             }
-            else if (!popStackOnEnd)
+            else if (state.Current.IsEnumerableProperty)
             {
-                Debug.Assert(state.Current.IsPropertyEnumerable);
-
-                // We added the items to the list property already.
+                // We added the items to the list already.
                 state.Current.ResetProperty();
                 return false;
             }
@@ -122,11 +115,6 @@ namespace System.Text.Json.Serialization
                 setPropertyDirectly = false;
             }
 
-            if (popStackOnEnd)
-            {
-                state.Pop();
-            }
-
             if (lastFrame)
             {
                 if (state.Current.ReturnValue == null)
@@ -143,12 +131,15 @@ namespace System.Text.Json.Serialization
                 }
                 // else there must be an outer object, so we'll return false here.
             }
+            else if (state.Current.IsEnumerable)
+            {
+                state.Pop();
+            }
 
-            ApplyObjectToEnumerable(value, options, ref state, ref reader, setPropertyDirectly: setPropertyDirectly);
+            ApplyObjectToEnumerable(value, ref state, ref reader, setPropertyDirectly: setPropertyDirectly);
 
-            if (!popStackOnEnd)
+            if (state.Current.IsEnumerableProperty)
             {
-                Debug.Assert(state.Current.IsPropertyEnumerable);
                 state.Current.ResetProperty();
             }
 
@@ -158,11 +149,12 @@ namespace System.Text.Json.Serialization
         // If this method is changed, also change ApplyValueToEnumerable.
         internal static void ApplyObjectToEnumerable(
             object value,
-            JsonSerializerOptions options,
             ref ReadStack state,
             ref Utf8JsonReader reader,
             bool setPropertyDirectly = false)
         {
+            Debug.Assert(!state.Current.SkipProperty);
+
             if (state.Current.IsEnumerable)
             {
                 if (state.Current.TempEnumerableValues != null)
@@ -174,7 +166,7 @@ namespace System.Text.Json.Serialization
                     ((IList)state.Current.ReturnValue).Add(value);
                 }
             }
-            else if (!setPropertyDirectly && state.Current.IsPropertyEnumerable)
+            else if (!setPropertyDirectly && state.Current.IsEnumerableProperty)
             {
                 Debug.Assert(state.Current.JsonPropertyInfo != null);
                 Debug.Assert(state.Current.ReturnValue != null);
@@ -189,10 +181,11 @@ namespace System.Text.Json.Serialization
                     list.Add(value);
                 }
             }
-            else if (state.Current.IsDictionary)
+            else if (state.Current.IsDictionary || (state.Current.IsDictionaryProperty && !setPropertyDirectly))
             {
                 Debug.Assert(state.Current.ReturnValue != null);
                 IDictionary dictionary = (IDictionary)state.Current.JsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue);
+
                 string key = state.Current.KeyName;
                 Debug.Assert(!string.IsNullOrEmpty(key));
                 if (!dictionary.Contains(key))
@@ -214,10 +207,11 @@ namespace System.Text.Json.Serialization
         // If this method is changed, also change ApplyObjectToEnumerable.
         internal static void ApplyValueToEnumerable<TProperty>(
             ref TProperty value,
-            JsonSerializerOptions options,
             ref ReadStack state,
             ref Utf8JsonReader reader)
         {
+            Debug.Assert(!state.Current.SkipProperty);
+
             if (state.Current.IsEnumerable)
             {
                 if (state.Current.TempEnumerableValues != null)
@@ -229,7 +223,7 @@ namespace System.Text.Json.Serialization
                     ((IList<TProperty>)state.Current.ReturnValue).Add(value);
                 }
             }
-            else if (state.Current.IsPropertyEnumerable)
+            else if (state.Current.IsEnumerableProperty)
             {
                 Debug.Assert(state.Current.JsonPropertyInfo != null);
                 Debug.Assert(state.Current.ReturnValue != null);
@@ -244,7 +238,7 @@ namespace System.Text.Json.Serialization
                     list.Add(value);
                 }
             }
-            else if (state.Current.IsDictionary)
+            else if (state.Current.IsProcessingDictionary)
             {
                 Debug.Assert(state.Current.ReturnValue != null);
                 IDictionary<string, TProperty> dictionary = (IDictionary<string, TProperty>)state.Current.JsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue);
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs
new file mode 100644 (file)
index 0000000..97540f5
--- /dev/null
@@ -0,0 +1,95 @@
+// 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.Diagnostics;
+
+namespace System.Text.Json.Serialization
+{
+    public static partial class JsonSerializer
+    {
+        private static void HandleStartDictionary(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state)
+        {
+            Debug.Assert(!state.Current.IsProcessingEnumerable);
+
+            JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo;
+            if (jsonPropertyInfo == null)
+            {
+                jsonPropertyInfo = state.Current.JsonClassInfo.CreateRootObject(options);
+            }
+
+            Debug.Assert(jsonPropertyInfo != null);
+
+            // A nested object or dictionary so push new frame.
+            if (state.Current.PropertyInitialized)
+            {
+                Debug.Assert(state.Current.IsDictionary);
+
+                JsonClassInfo classInfoTemp = state.Current.JsonClassInfo;
+                state.Push();
+                state.Current.JsonClassInfo = classInfoTemp.ElementClassInfo;
+                state.Current.InitializeJsonPropertyInfo();
+
+                ClassType classType = state.Current.JsonClassInfo.ClassType;
+                if (classType == ClassType.Value &&
+                    jsonPropertyInfo.ElementClassInfo.Type != typeof(object) &&
+                    jsonPropertyInfo.ElementClassInfo.Type != typeof(JsonElement))
+                {
+                    ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.JsonClassInfo.Type, reader, state.PropertyPath);
+                }
+
+                JsonClassInfo classInfo = state.Current.JsonClassInfo;
+                state.Current.ReturnValue = classInfo.CreateObject();
+                return;
+            }
+
+            state.Current.PropertyInitialized = true;
+
+            // If current property is already set (from a constructor, for example) leave as-is.
+            if (jsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue) == null)
+            {
+                // Create the dictionary.
+                JsonClassInfo dictionaryClassInfo = options.GetOrAddClass(jsonPropertyInfo.RuntimePropertyType);
+                IDictionary value = (IDictionary)dictionaryClassInfo.CreateObject();
+                if (value != null)
+                {
+                    if (state.Current.ReturnValue != null)
+                    {
+                        state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, value);
+                    }
+                    else
+                    {
+                        // A dictionary is being returned directly, or a nested dictionary.
+                        state.Current.SetReturnValue(value);
+                    }
+                }
+            }
+        }
+
+        private static void HandleEndDictionary(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state)
+        {
+            if (state.Current.IsDictionaryProperty)
+            {
+                // We added the items to the dictionary already.
+                state.Current.ResetProperty();
+            }
+            else
+            {
+                object value = state.Current.ReturnValue;
+
+                if (state.IsLastFrame)
+                {
+                    // Set the return value directly since this will be returned to the user.
+                    state.Current.Reset();
+                    state.Current.ReturnValue = value;
+                }
+                else
+                {
+                    state.Pop();
+                    ApplyObjectToEnumerable(value, ref state, ref reader);
+                }
+            }
+        }
+    }
+}
index 7f7ecb1..b5b9e5f 100644 (file)
@@ -2,7 +2,6 @@
 // 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.Diagnostics;
 
 namespace System.Text.Json.Serialization
@@ -11,7 +10,7 @@ namespace System.Text.Json.Serialization
     {
         private static bool HandleNull(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options)
         {
-            if (state.Current.Skip())
+            if (state.Current.SkipProperty)
             {
                 return false;
             }
@@ -32,14 +31,14 @@ namespace System.Text.Json.Serialization
 
             if (state.Current.IsEnumerable || state.Current.IsDictionary)
             {
-                ApplyObjectToEnumerable(null, options, ref state, ref reader);
+                ApplyObjectToEnumerable(null, ref state, ref reader);
                 return false;
             }
 
-            if (state.Current.IsPropertyEnumerable)
+            if (state.Current.IsEnumerableProperty || state.Current.IsDictionaryProperty)
             {
-                bool setPropertyToNull = !state.Current.EnumerableCreated;
-                ApplyObjectToEnumerable(null, options, ref state, ref reader, setPropertyDirectly: setPropertyToNull);
+                bool setPropertyToNull = !state.Current.PropertyInitialized;
+                ApplyObjectToEnumerable(null, ref state, ref reader, setPropertyDirectly: setPropertyToNull);
                 return false;
             }
 
index 425c4d7..b561568 100644 (file)
@@ -10,89 +10,45 @@ namespace System.Text.Json.Serialization
     {
         private static void HandleStartObject(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state)
         {
-            if (state.Current.Skip())
-            {
-                state.Push();
-                state.Current.Drain = true;
-                return;
-            }
+            Debug.Assert(!state.Current.IsProcessingDictionary);
 
             if (state.Current.IsProcessingEnumerable)
             {
+                // A nested object within an enumerable.
                 Type objType = state.Current.GetElementType();
                 state.Push();
                 state.Current.Initialize(objType, options);
             }
             else if (state.Current.JsonPropertyInfo != null)
             {
-                if (state.Current.IsDictionary)
-                {
-                    // Verify that the Dictionary can be deserialized by having <string> as first generic argument.
-                    Type[] args = state.Current.JsonClassInfo.Type.GetGenericArguments();
-                    if (args.Length == 0 || args[0].UnderlyingSystemType != typeof(string))
-                    {
-                        ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.JsonClassInfo.Type, reader, state.PropertyPath);
-                    }
-
-                    if (state.Current.ReturnValue == null)
-                    {
-                        // The Dictionary created below will be returned to corresponding Parse() etc method.
-                        // Ensure any nested array creates a new frame.
-                        state.Current.EnumerableCreated = true;
-                    }
-                    else
-                    {
-                        ClassType classType = state.Current.JsonClassInfo.ElementClassInfo.ClassType;
-
-                        // Verify that the second parameter is not a value.
-                        if (state.Current.JsonClassInfo.ElementClassInfo.ClassType == ClassType.Value)
-                        {
-                            ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.JsonClassInfo.Type, reader, state.PropertyPath);
-                        }
-
-                        // A nested object, dictionary or enumerable.
-                        JsonClassInfo classInfoTemp = state.Current.JsonClassInfo;
-                        state.Push();
-                        state.Current.JsonClassInfo = classInfoTemp.ElementClassInfo;
-                        state.Current.InitializeJsonPropertyInfo();
-                    }
-                }
-                else
-                {
-                    // Nested object.
-                    Type objType = state.Current.JsonPropertyInfo.RuntimePropertyType;
-                    state.Push();
-                    state.Current.Initialize(objType, options);
-                }
+                // Nested object.
+                Type objType = state.Current.JsonPropertyInfo.RuntimePropertyType;
+                state.Push();
+                state.Current.Initialize(objType, options);
             }
 
             JsonClassInfo classInfo = state.Current.JsonClassInfo;
             state.Current.ReturnValue = classInfo.CreateObject();
         }
 
-        private static bool HandleEndObject(JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader)
+        private static void HandleEndObject(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state)
         {
-            bool isLastFrame = state.IsLastFrame;
-            if (state.Current.Drain)
-            {
-                state.Pop();
-                return isLastFrame;
-            }
+            Debug.Assert(!state.Current.IsProcessingDictionary);
 
             state.Current.JsonClassInfo.UpdateSortedPropertyCache(ref state.Current);
 
             object value = state.Current.ReturnValue;
 
-            if (isLastFrame)
+            if (state.IsLastFrame)
             {
                 state.Current.Reset();
                 state.Current.ReturnValue = value;
-                return true;
             }
-
-            state.Pop();
-            ApplyObjectToEnumerable(value, options, ref state, ref reader);
-            return false;
+            else
+            {
+                state.Pop();
+                ApplyObjectToEnumerable(value, ref state, ref reader);
+            }
         }
     }
 }
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs
new file mode 100644 (file)
index 0000000..e7b0ff2
--- /dev/null
@@ -0,0 +1,131 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Buffers;
+using System.Collections;
+using System.Diagnostics;
+
+namespace System.Text.Json.Serialization
+{
+    public static partial class JsonSerializer
+    {
+        private static void HandlePropertyName(
+            JsonSerializerOptions options,
+            ref Utf8JsonReader reader,
+            ref ReadStack state)
+        {
+            if (state.Current.Drain)
+            {
+                return;
+            }
+
+            Debug.Assert(state.Current.ReturnValue != default);
+            Debug.Assert(state.Current.JsonClassInfo != default);
+
+            if (state.Current.IsProcessingDictionary)
+            {
+                if (ReferenceEquals(state.Current.JsonClassInfo.DataExtensionProperty, state.Current.JsonPropertyInfo))
+                {
+                    ReadOnlySpan<byte> propertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
+
+                    // todo: use a cleaner call to get the unescaped string once https://github.com/dotnet/corefx/issues/35386 is implemented.
+                    if (reader._stringHasEscaping)
+                    {
+                        int idx = propertyName.IndexOf(JsonConstants.BackSlash);
+                        Debug.Assert(idx != -1);
+                        propertyName = GetUnescapedString(propertyName, idx);
+                    }
+
+                    ProcessMissingProperty(propertyName, options, ref reader, ref state);
+                }
+                else
+                {
+                    string keyName = reader.GetString();
+                    if (options.DictionaryKeyPolicy != null)
+                    {
+                        keyName = options.DictionaryKeyPolicy.ConvertName(keyName);
+                    }
+
+                    if (state.Current.IsDictionary)
+                    {
+                        state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.GetPolicyProperty();
+                    }
+
+                    Debug.Assert(state.Current.IsDictionary ||
+                        (state.Current.IsDictionaryProperty && state.Current.JsonPropertyInfo != null));
+
+                    state.Current.KeyName = keyName;
+                }
+            }
+            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)
+                {
+                    if (state.Current.JsonClassInfo.DataExtensionProperty == null)
+                    {
+                        state.Current.JsonPropertyInfo = JsonPropertyInfo.s_missingProperty;
+                    }
+                    else
+                    {
+                        ProcessMissingProperty(propertyName, options, ref reader, ref state);
+                    }
+                }
+                else
+                {
+                    state.Current.PropertyIndex++;
+                }
+            }
+        }
+
+        private static void ProcessMissingProperty(
+            ReadOnlySpan<byte> unescapedPropertyName,
+            JsonSerializerOptions options,
+            ref Utf8JsonReader reader,
+            ref ReadStack state)
+        {
+            JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.DataExtensionProperty;
+
+            Debug.Assert(jsonPropertyInfo != null);
+            Debug.Assert(state.Current.ReturnValue != null);
+
+            IDictionary extensionData = (IDictionary)jsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue);
+            if (extensionData == null)
+            {
+                Type type = jsonPropertyInfo.DeclaredPropertyType;
+
+                // Create the appropriate dictionary type. We already verified the types.
+                Debug.Assert(type.IsGenericType);
+                Debug.Assert(type.GetGenericArguments().Length == 2);
+                Debug.Assert(type.GetGenericArguments()[0].UnderlyingSystemType == typeof(string));
+                Debug.Assert(
+                    type.GetGenericArguments()[1].UnderlyingSystemType == typeof(object) ||
+                    type.GetGenericArguments()[1].UnderlyingSystemType == typeof(JsonElement));
+
+                extensionData = (IDictionary)options.GetOrAddClass(type).CreateObject();
+                jsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, extensionData);
+            }
+
+            JsonElement jsonElement;
+            using (JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader))
+            {
+                jsonElement = jsonDocument.RootElement.Clone();
+            }
+
+            string keyName = JsonHelpers.Utf8GetString(unescapedPropertyName);
+
+            // Currently we don't apply any naming policy. If we do, we'd have to pass it onto the JsonDocument.
+
+            extensionData.Add(keyName, jsonElement);
+        }
+    }
+}
index 15e9d98..a113129 100644 (file)
@@ -8,7 +8,7 @@ namespace System.Text.Json.Serialization
     {
         private static bool HandleValue(JsonTokenType tokenType, JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state)
         {
-            if (state.Current.Skip())
+            if (state.Current.SkipProperty)
             {
                 return false;
             }
index 59aa8e6..f95f24b 100644 (file)
@@ -3,6 +3,7 @@
 // See the LICENSE file in the project root for more information.
 
 using System.Buffers;
+using System.Collections;
 using System.Diagnostics;
 
 namespace System.Text.Json.Serialization
@@ -13,9 +14,6 @@ namespace System.Text.Json.Serialization
     /// </summary>
     public static partial class JsonSerializer
     {
-        internal static readonly JsonPropertyInfo s_missingProperty = new JsonPropertyInfoNotNullable<object, object, object>();
-
-        // todo: for readability, refactor this method to split by ClassType(Enumerable, Object, or Value) like Write()
         private static void ReadCore(
             JsonSerializerOptions options,
             ref Utf8JsonReader reader,
@@ -38,63 +36,49 @@ namespace System.Text.Json.Serialization
                     }
                     else if (tokenType == JsonTokenType.PropertyName)
                     {
-                        if (!state.Current.Drain)
+                        HandlePropertyName(options, ref reader, ref state);
+                    }
+                    else if (tokenType == JsonTokenType.StartObject)
+                    {
+                        if (state.Current.SkipProperty)
                         {
-                            Debug.Assert(state.Current.ReturnValue != default);
-                            Debug.Assert(state.Current.JsonClassInfo != default);
-
-                            if (state.Current.IsDictionary)
-                            {
-                                string keyName = reader.GetString();
-                                if (options.DictionaryKeyPolicy != null)
-                                {
-                                    keyName = options.DictionaryKeyPolicy.ConvertName(keyName);
-                                }
-
-                                state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.GetPolicyProperty();
-                                state.Current.KeyName = keyName;
-                            }
-                            else
+                            state.Push();
+                            state.Current.Drain = true;
+                        }
+                        else if (state.Current.IsProcessingValue)
+                        {
+                            if (HandleValue(tokenType, options, ref reader, ref state))
                             {
-                                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)
-                                {
-                                    state.Current.JsonPropertyInfo = s_missingProperty;
-                                }
-
-                                state.Current.PropertyIndex++;
+                                continue;
                             }
                         }
-                    }
-                    else if (tokenType == JsonTokenType.StartObject)
-                    {
-                        if (!state.Current.IsProcessingProperty)
+                        else if (state.Current.IsProcessingDictionary)
                         {
-                            HandleStartObject(options, ref reader, ref state);
+                            HandleStartDictionary(options, ref reader, ref state);
                         }
-                        else if (HandleValue(tokenType, options, ref reader, ref state))
+                        else
                         {
-                            continue;
+                            HandleStartObject(options, ref reader, ref state);
                         }
                     }
                     else if (tokenType == JsonTokenType.EndObject)
                     {
-                        if (HandleEndObject(options, ref state, ref reader))
+                        if (state.Current.Drain)
                         {
-                            continue;
+                            state.Pop();
+                        }
+                        else if (state.Current.IsProcessingDictionary)
+                        {
+                            HandleEndDictionary(options, ref reader, ref state);
+                        }
+                        else
+                        {
+                            HandleEndObject(options, ref reader, ref state);
                         }
                     }
                     else if (tokenType == JsonTokenType.StartArray)
                     {
-                        if (!state.Current.IsProcessingProperty)
+                        if (!state.Current.IsProcessingValue)
                         {
                             HandleStartArray(options, ref reader, ref state);
                         }
@@ -105,7 +89,7 @@ namespace System.Text.Json.Serialization
                     }
                     else if (tokenType == JsonTokenType.EndArray)
                     {
-                        if (HandleEndArray(options, ref state, ref reader))
+                        if (HandleEndArray(options, ref reader, ref state))
                         {
                             continue;
                         }
index b9b2864..3ffe079 100644 (file)
@@ -2,7 +2,6 @@
 // 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;
 using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
@@ -19,22 +18,11 @@ namespace System.Text.Json.Serialization
             ref WriteStack state)
         {
             JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo;
-            if (!jsonPropertyInfo.ShouldSerialize)
-            {
-                // Ignore writing this property.
-                return true;
-            }
-
             if (state.Current.Enumerator == null)
             {
-                // Verify that the Dictionary can be serialized by having <string> as first generic argument.
-                Type[] args = jsonPropertyInfo.RuntimePropertyType.GetGenericArguments();
-                if (args.Length == 0 || args[0].UnderlyingSystemType != typeof(string))
-                {
-                    ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.JsonClassInfo.Type, state.PropertyPath);
-                }
+                IEnumerable enumerable;
 
-                IEnumerable enumerable = (IEnumerable)jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue);
+                enumerable = (IEnumerable)jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue);
                 if (enumerable == null)
                 {
                     // Write a null object or enumerable.
@@ -48,28 +36,36 @@ namespace System.Text.Json.Serialization
 
             if (state.Current.Enumerator.MoveNext())
             {
-                // Check for polymorphism.
-                if (elementClassInfo.ClassType == ClassType.Unknown)
-                {
-                    object currentValue = ((IDictionaryEnumerator)state.Current.Enumerator).Entry.Value;
-                    GetRuntimeClassInfo(currentValue, ref elementClassInfo, options);
-                }
-
-                if (elementClassInfo.ClassType == ClassType.Value)
-                {
-                    elementClassInfo.GetPolicyProperty().WriteDictionary(options, ref state.Current, writer);
-                }
-                else if (state.Current.Enumerator.Current == null)
+                // Handle DataExtension.
+                if (ReferenceEquals(jsonPropertyInfo, state.Current.JsonClassInfo.DataExtensionProperty))
                 {
-                    writer.WriteNull(jsonPropertyInfo.Name);
+                    WriteExtensionData(writer, ref state.Current);
                 }
                 else
                 {
-                    // An object or another enumerator requires a new stack frame.
-                    var enumerator = (IDictionaryEnumerator)state.Current.Enumerator;
-                    object value = enumerator.Value;
-                    state.Push(elementClassInfo, value);
-                    state.Current.KeyName = (string)enumerator.Key;
+                    // Check for polymorphism.
+                    if (elementClassInfo.ClassType == ClassType.Unknown)
+                    {
+                        object currentValue = ((IDictionaryEnumerator)state.Current.Enumerator).Entry.Value;
+                        GetRuntimeClassInfo(currentValue, ref elementClassInfo, options);
+                    }
+
+                    if (elementClassInfo.ClassType == ClassType.Value)
+                    {
+                        elementClassInfo.GetPolicyProperty().WriteDictionary(options, ref state.Current, writer);
+                    }
+                    else if (state.Current.Enumerator.Current == null)
+                    {
+                        writer.WriteNull(jsonPropertyInfo.Name);
+                    }
+                    else
+                    {
+                        // An object or another enumerator requires a new stack frame.
+                        var enumerator = (IDictionaryEnumerator)state.Current.Enumerator;
+                        object value = enumerator.Value;
+                        state.Push(elementClassInfo, value);
+                        state.Current.KeyName = (string)enumerator.Key;
+                    }
                 }
 
                 return false;
@@ -161,5 +157,21 @@ namespace System.Text.Json.Serialization
 #endif
             }
         }
+
+        private static void WriteExtensionData(Utf8JsonWriter writer, ref WriteStackFrame frame)
+        {
+            DictionaryEntry entry = ((IDictionaryEnumerator)frame.Enumerator).Entry;
+            if (entry.Value is JsonElement element)
+            {
+                Debug.Assert(entry.Key is string);
+
+                string propertyName = (string)entry.Key;
+                element.WriteAsProperty(propertyName.AsSpan(), writer);
+            }
+            else
+            {
+                ThrowHelper.ThrowInvalidOperationException_SerializationDataExtensionPropertyInvalid(frame.JsonClassInfo, entry.Value.GetType());
+            }
+        }
     }
 }
index 6f51097..4be8b9f 100644 (file)
@@ -17,16 +17,9 @@ namespace System.Text.Json.Serialization
         {
             Debug.Assert(state.Current.JsonPropertyInfo.ClassType == ClassType.Enumerable);
 
-            JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo;
-            if (!jsonPropertyInfo.ShouldSerialize)
-            {
-                // Ignore writing this property.
-                return true;
-            }
-
             if (state.Current.Enumerator == null)
             {
-                IEnumerable enumerable = (IEnumerable)jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue);
+                IEnumerable enumerable = (IEnumerable)state.Current.JsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue);
 
                 if (enumerable == null)
                 {
index 1daa62d..f60932e 100644 (file)
@@ -13,8 +13,6 @@ namespace System.Text.Json.Serialization
             Utf8JsonWriter writer,
             ref WriteStack state)
         {
-            JsonClassInfo classInfo = state.Current.JsonClassInfo;
-
             // Write the start.
             if (!state.Current.StartObjectWritten)
             {
@@ -24,6 +22,7 @@ namespace System.Text.Json.Serialization
             // Determine if we are done enumerating properties.
             // If the ClassType is unknown, there will be a policy property applied. There is probably
             // a better way to identify policy properties- maybe not put them in the normal property bag?
+            JsonClassInfo classInfo = state.Current.JsonClassInfo;
             if (classInfo.ClassType != ClassType.Unknown && state.Current.PropertyIndex != classInfo.PropertyCount)
             {
                 HandleObject(options, writer, ref state);
@@ -54,6 +53,11 @@ namespace System.Text.Json.Serialization
                 state.Current.JsonClassInfo.ClassType == ClassType.Unknown);
 
             JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(state.Current.PropertyIndex);
+            if (!jsonPropertyInfo.ShouldSerialize)
+            {
+                state.Current.NextProperty();
+                return true;
+            }
 
             bool obtainedValue = false;
             object currentValue = null;
@@ -122,7 +126,7 @@ namespace System.Text.Json.Serialization
             {
                 if (!jsonPropertyInfo.IgnoreNullValues)
                 {
-                    writer.WriteNull(jsonPropertyInfo._escapedName);
+                    writer.WriteNull(jsonPropertyInfo.EscapedName);
                 }
 
                 state.Current.NextProperty();
index 650db0a..73c735e 100644 (file)
@@ -7,7 +7,7 @@ using System.Diagnostics;
 
 namespace System.Text.Json.Serialization
 {
-    [DebuggerDisplay("Current: ClassType.{Current.JsonClassInfo.ClassType} {Current.JsonClassInfo.Type.Name}")]
+    [DebuggerDisplay("Current: ClassType.{Current.JsonClassInfo.ClassType}, {Current.JsonClassInfo.Type.Name}")]
     internal struct ReadStack
     {
         // A fields is used instead of a property to avoid value semantics.
@@ -83,7 +83,7 @@ namespace System.Text.Json.Serialization
 
         private string GetPropertyName(in ReadStackFrame frame)
         {
-            if (frame.JsonPropertyInfo != null && frame.JsonClassInfo.ClassType == ClassType.Object)
+            if (frame.JsonPropertyInfo?.PropertyInfo != null && frame.JsonClassInfo.ClassType == ClassType.Object)
             {
                 return $".{frame.JsonPropertyInfo.PropertyInfo.Name}";
             }
index bab6bcb..7f3e4d3 100644 (file)
@@ -9,7 +9,7 @@ using System.Runtime.CompilerServices;
 
 namespace System.Text.Json.Serialization
 {
-    [DebuggerDisplay("ClassType.{JsonClassInfo.ClassType} {JsonClassInfo.Type.Name}")]
+    [DebuggerDisplay("ClassType.{JsonClassInfo.ClassType}, {JsonClassInfo.Type.Name}")]
     internal struct ReadStackFrame
     {
         // The object (POCO or IEnumerable) that is being populated
@@ -22,12 +22,11 @@ namespace System.Text.Json.Serialization
         // Current property values.
         public JsonPropertyInfo JsonPropertyInfo;
 
-        // Pop the stack when the current array or dictionary is done.
-        public bool PopStackOnEnd;
-
         // Support System.Array and other types that don't implement IList.
         public IList TempEnumerableValues;
-        public bool EnumerableCreated;
+
+        // Has an array or dictionary property been initialized.
+        public bool PropertyInitialized;
 
         // For performance, we order the properties by the first deserialize and PropertyIndex helps find the right slot quicker.
         public int PropertyIndex;
@@ -37,17 +36,28 @@ namespace System.Text.Json.Serialization
         public bool Drain;
 
         public bool IsDictionary => JsonClassInfo.ClassType == ClassType.Dictionary;
+
+        public bool IsDictionaryProperty => JsonPropertyInfo != null &&
+            !JsonPropertyInfo.IsPropertyPolicy &&
+            JsonPropertyInfo.ClassType == ClassType.Dictionary;
+
         public bool IsEnumerable => JsonClassInfo.ClassType == ClassType.Enumerable;
-        public bool IsProcessingEnumerableOrDictionary => IsProcessingEnumerable || IsDictionary;
-        public bool IsProcessingEnumerable => IsEnumerable || IsPropertyEnumerable;
-        public bool IsPropertyEnumerable => JsonPropertyInfo != null ? JsonPropertyInfo.ClassType == ClassType.Enumerable : false;
 
-        public bool IsProcessingProperty
+        public bool IsEnumerableProperty =>
+            JsonPropertyInfo != null &&
+            !JsonPropertyInfo.IsPropertyPolicy &&
+            JsonPropertyInfo.ClassType == ClassType.Enumerable;
+
+        public bool IsProcessingEnumerableOrDictionary => IsProcessingEnumerable || IsProcessingDictionary;
+        public bool IsProcessingDictionary => IsDictionary || IsDictionaryProperty;
+        public bool IsProcessingEnumerable => IsEnumerable || IsEnumerableProperty;
+
+        public bool IsProcessingValue
         {
             [MethodImpl(MethodImplOptions.AggressiveInlining)]
             get
             {
-                if (JsonPropertyInfo == null || Skip())
+                if (JsonPropertyInfo == null || SkipProperty)
                 {
                     return false;
                 }
@@ -55,8 +65,11 @@ namespace System.Text.Json.Serialization
                 // We've got a property info. If we're a Value or polymorphic Value
                 // (ClassType.Unknown), return true.
                 ClassType type = JsonPropertyInfo.ClassType;
-                return type == ClassType.Value || type == ClassType.Unknown
-                    || (type == ClassType.Dictionary && KeyName != null && JsonClassInfo.ElementClassInfo.ClassType == ClassType.Unknown);
+                return type == ClassType.Value || type == ClassType.Unknown ||
+                    KeyName != null  && (
+                    (IsDictionary && JsonClassInfo.ElementClassInfo.ClassType == ClassType.Unknown) ||
+                    (IsDictionaryProperty && JsonPropertyInfo.ElementClassInfo.ClassType == ClassType.Unknown)
+                    );
             }
         }
 
@@ -86,9 +99,8 @@ namespace System.Text.Json.Serialization
 
         public void ResetProperty()
         {
-            EnumerableCreated = false;
+            PropertyInitialized = false;
             JsonPropertyInfo = null;
-            PopStackOnEnd = false;
             TempEnumerableValues = null;
         }
 
@@ -112,7 +124,7 @@ namespace System.Text.Json.Serialization
                 }
                 else
                 {
-                    converterList =  new List<object>();
+                    converterList = new List<object>();
                 }
 
                 state.Current.TempEnumerableValues = converterList;
@@ -137,17 +149,12 @@ namespace System.Text.Json.Serialization
 
         public Type GetElementType()
         {
-            if (IsPropertyEnumerable)
+            if (IsEnumerableProperty || IsDictionaryProperty)
             {
                 return JsonPropertyInfo.ElementClassInfo.Type;
             }
 
-            if (IsEnumerable)
-            {
-                return JsonClassInfo.ElementClassInfo.Type;
-            }
-
-            if (IsDictionary)
+            if (IsEnumerable || IsDictionary)
             {
                 return JsonClassInfo.ElementClassInfo.Type;
             }
@@ -175,9 +182,8 @@ namespace System.Text.Json.Serialization
             ReturnValue = value;
         }
 
-        public bool Skip()
-        {
-            return Drain || ReferenceEquals(JsonPropertyInfo, JsonSerializer.s_missingProperty);
-        }
+        public bool SkipProperty => Drain ||
+            ReferenceEquals(JsonPropertyInfo, JsonPropertyInfo.s_missingProperty) ||
+            (JsonPropertyInfo?.IsPropertyPolicy == false && JsonPropertyInfo?.ShouldDeserialize == false);
     }
 }
index 82f823b..660bb44 100644 (file)
@@ -46,9 +46,9 @@ namespace System.Text.Json.Serialization
 
         public void WriteObjectOrArrayStart(ClassType classType, Utf8JsonWriter writer, bool writeNull = false)
         {
-            if (JsonPropertyInfo?._escapedName != null)
+            if (JsonPropertyInfo?.EscapedName != null)
             {
-                WriteObjectOrArrayStart(classType, JsonPropertyInfo?._escapedName, writer, writeNull);
+                WriteObjectOrArrayStart(classType, JsonPropertyInfo?.EscapedName, writer, writeNull);
             }
             else if (KeyName != null)
             {
index 6bf5b59..2f69736 100644 (file)
@@ -3,6 +3,7 @@
 // See the LICENSE file in the project root for more information.
 
 using System.Diagnostics;
+using System.Reflection;
 using System.Runtime.CompilerServices;
 using System.Text.Json.Serialization;
 
@@ -13,19 +14,30 @@ namespace System.Text.Json
         [MethodImpl(MethodImplOptions.NoInlining)]
         public static void ThrowArgumentException_DeserializeWrongType(Type type, object value)
         {
-            throw new ArgumentException(SR.Format(SR.DeserializeWrongType, type.FullName, value.GetType().FullName));
+            throw new ArgumentException(SR.Format(SR.DeserializeWrongType, type, value.GetType()));
+        }
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        public static NotSupportedException GetNotSupportedException_SerializationNotSupportedCollection(Type propertyType, Type parentType, MemberInfo memberInfo)
+        {
+            if (parentType != null && parentType != typeof(object) && memberInfo != null)
+            {
+                return new NotSupportedException(SR.Format(SR.SerializationNotSupportedCollection, propertyType, $"{parentType}.{memberInfo.Name}"));
+            }
+
+            return new NotSupportedException(SR.Format(SR.SerializationNotSupportedCollectionType, propertyType));
         }
 
         [MethodImpl(MethodImplOptions.NoInlining)]
         public static void ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType, in Utf8JsonReader reader, string path)
         {
-            ThowJsonException(SR.Format(SR.DeserializeUnableToConvertValue, propertyType.FullName), in reader, path);
+            ThowJsonException(SR.Format(SR.DeserializeUnableToConvertValue, propertyType), in reader, path);
         }
 
         [MethodImpl(MethodImplOptions.NoInlining)]
         public static void ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType, string path)
         {
-            string message = SR.Format(SR.DeserializeUnableToConvertValue, propertyType.FullName) + $" Path: {path}.";
+            string message = SR.Format(SR.DeserializeUnableToConvertValue, propertyType) + $" Path: {path}.";
             throw new JsonException(message, path, null, null);
         }
 
@@ -44,13 +56,13 @@ namespace System.Text.Json
         [MethodImpl(MethodImplOptions.NoInlining)]
         public static void ThrowInvalidOperationException_SerializerPropertyNameConflict(JsonClassInfo jsonClassInfo, JsonPropertyInfo jsonPropertyInfo)
         {
-            throw new InvalidOperationException(SR.Format(SR.SerializerPropertyNameConflict, jsonClassInfo.Type.FullName, jsonPropertyInfo.PropertyInfo.Name));
+            throw new InvalidOperationException(SR.Format(SR.SerializerPropertyNameConflict, jsonClassInfo.Type, jsonPropertyInfo.PropertyInfo.Name));
         }
 
         [MethodImpl(MethodImplOptions.NoInlining)]
-        public static void ThrowInvalidOperationException_SerializerPropertyNameNull(JsonClassInfo jsonClassInfo, JsonPropertyInfo jsonPropertyInfo)
+        public static void ThrowInvalidOperationException_SerializerPropertyNameNull(Type parentType, JsonPropertyInfo jsonPropertyInfo)
         {
-            throw new InvalidOperationException(SR.Format(SR.SerializerPropertyNameNull, jsonClassInfo.Type.FullName, jsonPropertyInfo.PropertyInfo.Name));
+            throw new InvalidOperationException(SR.Format(SR.SerializerPropertyNameNull, parentType, jsonPropertyInfo.PropertyInfo.Name));
         }
 
         public static void ThrowJsonException_DeserializeDataRemaining(long length, long bytesRemaining)
@@ -92,5 +104,23 @@ namespace System.Text.Json
 
             throw new JsonException(message, path, exception.LineNumber, exception.BytePositionInLine, exception);
         }
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        public static void ThrowInvalidOperationException_SerializationDuplicateAttribute(Type attribute)
+        {
+            throw new InvalidOperationException(SR.Format(SR.SerializationDuplicateAttribute, attribute));
+        }
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        public static void ThrowInvalidOperationException_SerializationDataExtensionPropertyInvalid(JsonClassInfo jsonClassInfo, JsonPropertyInfo jsonPropertyInfo)
+        {
+            throw new InvalidOperationException(SR.Format(SR.SerializationDataExtensionPropertyInvalid, jsonClassInfo.Type, jsonPropertyInfo.PropertyInfo.Name));
+        }
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        public static void ThrowInvalidOperationException_SerializationDataExtensionPropertyInvalid(JsonClassInfo jsonClassInfo, Type invalidType)
+        {
+            throw new InvalidOperationException(SR.Format(SR.SerializationDataExtensionPropertyInvalidElement, jsonClassInfo.Type, jsonClassInfo.DataExtensionProperty.PropertyInfo.Name, invalidType));
+        }
     }
 }
index a69e504..5a93117 100644 (file)
@@ -225,7 +225,7 @@ namespace System.Text.Json
         [MethodImpl(MethodImplOptions.NoInlining)]
         public static JsonException GetJsonReaderException(ref Utf8JsonReader json, ExceptionResource resource, byte nextByte, ReadOnlySpan<byte> bytes)
         {
-            string message = GetResourceString(ref json, resource, nextByte, Encoding.UTF8.GetString(bytes.ToArray(), 0, bytes.Length));
+            string message = GetResourceString(ref json, resource, nextByte, JsonHelpers.Utf8GetString(bytes));
 
             long lineNumber = json.CurrentState._lineNumber;
             long bytePositionInLine = json.CurrentState._bytePositionInLine;
index 29458f2..b30e8de 100644 (file)
@@ -37,18 +37,6 @@ namespace System.Text.Json.Serialization.Tests
             }
         }
 
-        private static void VerifyReadNull(SimpleTestClass obj, bool isNull)
-        {
-            if (isNull)
-            {
-                Assert.Null(obj);
-            }
-            else
-            {
-                obj.Verify();
-            }
-        }
-
         [Theory]
         [MemberData(nameof(ReadNullJson))]
         public static void ReadNull(string json, bool element0Null, bool element1Null, bool element2Null)
@@ -64,6 +52,18 @@ namespace System.Text.Json.Serialization.Tests
             VerifyReadNull(list[0], element0Null);
             VerifyReadNull(list[1], element1Null);
             VerifyReadNull(list[2], element2Null);
+
+            static void VerifyReadNull(SimpleTestClass obj, bool isNull)
+            {
+                if (isNull)
+                {
+                    Assert.Null(obj);
+                }
+                else
+                {
+                    obj.Verify();
+                }
+            }
         }
 
         [Fact]
@@ -177,5 +177,17 @@ namespace System.Text.Json.Serialization.Tests
             TestClassWithObjectImmutableTypes obj = JsonSerializer.Parse<TestClassWithObjectImmutableTypes>(TestClassWithObjectImmutableTypes.s_data);
             obj.Verify();
         }
+
+        public static void ClassWithNoSetter()
+        {
+            string json = @"{""MyList"":[1]}";
+            ClassWithListButNoSetter obj = JsonSerializer.Parse<ClassWithListButNoSetter>(json);
+            Assert.Equal(1, obj.MyList[0]);
+        }
+
+        public class ClassWithListButNoSetter
+        {
+            public List<int> MyList { get; } = new List<int>();
+        }
     }
 }
index 2926d4b..b1e7e77 100644 (file)
@@ -65,8 +65,6 @@ namespace System.Text.Json.Serialization.Tests
             }
         }
 
-
-
         [Fact]
         public static void WriteClassWithGenericList()
         {
index 7f99c2b..4e80a3a 100644 (file)
@@ -103,7 +103,7 @@ namespace System.Text.Json.Serialization.Tests
         [Fact]
         public static void FirstGenericArgNotStringFail()
         {
-            Assert.Throws<JsonException>(() => JsonSerializer.Parse<Dictionary<int, int>>(@"{""Key1"":1}"));
+            Assert.Throws<NotSupportedException>(() => JsonSerializer.Parse<Dictionary<int, int>>(@"{""Key1"":1}"));
         }
 
         [Fact]
@@ -301,10 +301,20 @@ namespace System.Text.Json.Serialization.Tests
         [Fact]
         public static void ObjectToStringFail()
         {
+            // Baseline
             string json = @"{""MyDictionary"":{""Key"":""Value""}}";
+            JsonSerializer.Parse<Dictionary<string, object>>(json);
+
             Assert.Throws<JsonException>(() => JsonSerializer.Parse<Dictionary<string, string>>(json));
         }
 
+        [Fact, ActiveIssue("JsonElement fails since it is a struct.")]
+        public static void ObjectToJsonElement()
+        {
+            string json = @"{""MyDictionary"":{""Key"":""Value""}}";
+            JsonSerializer.Parse<Dictionary<string, JsonElement>>(json);
+        }
+
         [Fact]
         public static void HashtableFail()
         {
@@ -315,27 +325,77 @@ namespace System.Text.Json.Serialization.Tests
                 JsonSerializer.Parse<Dictionary<string, string>>(json);
 
                 // We don't support non-generic IDictionary
-                Assert.Throws<JsonException>(() => JsonSerializer.Parse<Hashtable>(json));
+                Assert.Throws<NotSupportedException>(() => JsonSerializer.Parse<Hashtable>(json));
             }
 
             {
                 Hashtable ht = new Hashtable();
                 ht.Add("Key", "Value");
-                Assert.Throws<JsonException>(() => JsonSerializer.ToString(ht));
+
+                Assert.Throws<NotSupportedException>(() => JsonSerializer.ToString(ht));
             }
 
             {
                 string json = @"{""Key"":""Value""}";
 
                 // We don't support non-generic IDictionary
-                Assert.Throws<JsonException>(() => JsonSerializer.Parse<IDictionary>(json));
+                Assert.Throws<NotSupportedException>(() => JsonSerializer.Parse<IDictionary>(json));
             }
 
             {
                 IDictionary ht = new Hashtable();
                 ht.Add("Key", "Value");
-                Assert.Throws<JsonException>(() => JsonSerializer.ToString(ht));
+                Assert.Throws<NotSupportedException>(() => JsonSerializer.ToString(ht));
+            }
+        }
+
+        [Fact]
+        public static void ClassWithNoSetter()
+        {
+            string json = @"{""MyDictionary"":{""Key"":""Value""}}";
+            ClassWithDictionaryButNoSetter obj = JsonSerializer.Parse<ClassWithDictionaryButNoSetter>(json);
+            Assert.Equal("Value", obj.MyDictionary["Key"]);
+        }
+
+        [Fact]
+        public static void DictionaryNotSupported()
+        {
+            string json = @"{""MyDictionary"":{""Key"":""Value""}}";
+
+            try
+            {
+                JsonSerializer.Parse<ClassWithNotSupportedDictionary>(json);
+                Assert.True(false, "Expected NotSupportedException to be thrown.");
             }
+            catch (NotSupportedException e)
+            {
+                // The exception should contain className.propertyName and the invalid type.
+                Assert.Contains("ClassWithNotSupportedDictionary.MyDictionary", e.Message);
+                Assert.Contains("Dictionary`2[System.Int32,System.Int32]", e.Message);
+            }
+        }
+
+        [Fact]
+        public static void DictionaryNotSupportedButIgnored()
+        {
+            string json = @"{""MyDictionary"":{""Key"":1}}";
+            ClassWithNotSupportedDictionaryButIgnored obj = JsonSerializer.Parse<ClassWithNotSupportedDictionaryButIgnored>(json);
+            Assert.Null(obj.MyDictionary);
+        }
+
+        public class ClassWithDictionaryButNoSetter
+        {
+            public Dictionary<string, string> MyDictionary { get; } = new Dictionary<string, string>();
+        }
+
+        public class ClassWithNotSupportedDictionary
+        {
+            public Dictionary<int, int> MyDictionary { get; set; }
+        }
+
+        public class ClassWithNotSupportedDictionaryButIgnored
+        {
+            [JsonIgnore] public Dictionary<int, int> MyDictionary { get; set; }
         }
     }
 }
diff --git a/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs b/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs
new file mode 100644 (file)
index 0000000..c45ed4e
--- /dev/null
@@ -0,0 +1,266 @@
+// 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.Generic;
+using System.Linq;
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+    public static class ExtensionDataTests
+    {
+        [Fact]
+        public static void ExtensionPropertyNotUsed()
+        {
+            string json = @"{""MyNestedClass"":" + SimpleTestClass.s_json + "}";
+            ClassWithExtensionProperty obj = JsonSerializer.Parse<ClassWithExtensionProperty>(json);
+            Assert.Null(obj.MyOverflow);
+        }
+
+        [Fact]
+        public static void ExtensionPropertyRoundTrip()
+        {
+            ClassWithExtensionProperty obj;
+
+            {
+                string json = @"{""MyIntMissing"":2, ""MyInt"":1, ""MyNestedClassMissing"":" + SimpleTestClass.s_json + "}";
+                obj = JsonSerializer.Parse<ClassWithExtensionProperty>(json);
+                Verify();
+            }
+
+            // Round-trip the json.
+            {
+                string json = JsonSerializer.ToString(obj);
+                obj = JsonSerializer.Parse<ClassWithExtensionProperty>(json);
+                Verify();
+            }
+
+            void Verify()
+            {
+                Assert.NotNull(obj.MyOverflow);
+                Assert.NotNull(obj.MyOverflow["MyIntMissing"]);
+                Assert.Equal(1, obj.MyInt);
+                Assert.Equal(2, obj.MyOverflow["MyIntMissing"].GetInt32());
+
+                JsonProperty[] properties = obj.MyOverflow["MyNestedClassMissing"].EnumerateObject().ToArray();
+
+                // Verify a couple properties
+                Assert.Equal(1, properties.Where(prop => prop.Name == "MyInt16").First().Value.GetInt32());
+                Assert.Equal(true, properties.Where(prop => prop.Name == "MyBooleanTrue").First().Value.GetBoolean());
+            }
+        }
+
+        [Fact]
+        public static void ExtensionPropertyAlreadyInstantiated()
+        {
+            Assert.NotNull(new ClassWithExtensionPropertyAlreadyInstantiated().MyOverflow);
+
+            string json = @"{""MyIntMissing"":2}";
+
+            ClassWithExtensionProperty obj = JsonSerializer.Parse<ClassWithExtensionProperty>(json);
+            Assert.Equal(2, obj.MyOverflow["MyIntMissing"].GetInt32());
+        }
+
+        [Fact]
+        public static void ExtensionPropertyAsObject()
+        {
+            string json = @"{""MyIntMissing"":2}";
+
+            ClassWithExtensionPropertyAsObject obj = JsonSerializer.Parse<ClassWithExtensionPropertyAsObject>(json);
+            Assert.IsType<JsonElement>(obj.MyOverflow["MyIntMissing"]);
+            Assert.Equal(2, ((JsonElement)obj.MyOverflow["MyIntMissing"]).GetInt32());
+        }
+
+        [Fact]
+        public static void ExtensionPropertyCamelCasing()
+        {
+            // Currently we apply no naming policy. If we do (such as a ExtensionPropertyNamingPolicy), we'd also have to add functionality to the JsonDocument.
+
+            ClassWithExtensionProperty obj;
+            const string jsonWithProperty = @"{""MyIntMissing"":1}";
+            const string jsonWithPropertyCamelCased = @"{""myIntMissing"":1}";
+
+            {
+                // Baseline Pascal-cased json + no casing option.
+                obj = JsonSerializer.Parse<ClassWithExtensionProperty>(jsonWithProperty);
+                Assert.Equal(1, obj.MyOverflow["MyIntMissing"].GetInt32());
+                string json = JsonSerializer.ToString(obj);
+                Assert.Contains(@"""MyIntMissing"":1", json);
+            }
+
+            {
+                // Pascal-cased json + camel casing option.
+                JsonSerializerOptions options = new JsonSerializerOptions();
+                options.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
+                options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+
+                obj = JsonSerializer.Parse<ClassWithExtensionProperty>(jsonWithProperty, options);
+                Assert.Equal(1, obj.MyOverflow["MyIntMissing"].GetInt32());
+                string json = JsonSerializer.ToString(obj);
+                Assert.Contains(@"""MyIntMissing"":1", json);
+            }
+
+            {
+                // Baseline camel-cased json + no casing option.
+                obj = JsonSerializer.Parse<ClassWithExtensionProperty>(jsonWithPropertyCamelCased);
+                Assert.Equal(1, obj.MyOverflow["myIntMissing"].GetInt32());
+                string json = JsonSerializer.ToString(obj);
+                Assert.Contains(@"""myIntMissing"":1", json);
+            }
+
+            {
+                // Baseline camel-cased json + camel casing option.
+                JsonSerializerOptions options = new JsonSerializerOptions();
+                options.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
+                options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+
+                obj = JsonSerializer.Parse<ClassWithExtensionProperty>(jsonWithPropertyCamelCased, options);
+                Assert.Equal(1, obj.MyOverflow["myIntMissing"].GetInt32());
+                string json = JsonSerializer.ToString(obj);
+                Assert.Contains(@"""myIntMissing"":1", json);
+            }
+        }
+
+        [Fact]
+        public static void NullValuesIgnored()
+        {
+            const string json = @"{""MyNestedClass"":null}";
+            const string jsonMissing = @"{ ""MyNestedClassMissing"":null}";
+
+            {
+                // Baseline with no missing.
+                ClassWithExtensionProperty obj = JsonSerializer.Parse<ClassWithExtensionProperty>(json);
+                Assert.Null(obj.MyOverflow);
+
+                string outJson = JsonSerializer.ToString(obj);
+                Assert.Contains(@"""MyNestedClass"":null", outJson);
+            }
+
+            {
+                // Baseline with missing.
+                ClassWithExtensionProperty obj = JsonSerializer.Parse<ClassWithExtensionProperty>(jsonMissing);
+                Assert.Equal(1, obj.MyOverflow.Count);
+                Assert.Equal(JsonValueType.Null, obj.MyOverflow["MyNestedClassMissing"].Type);
+            }
+
+            {
+                JsonSerializerOptions options = new JsonSerializerOptions();
+                options.IgnoreNullValues = true;
+
+                ClassWithExtensionProperty obj = JsonSerializer.Parse<ClassWithExtensionProperty>(jsonMissing, options);
+
+                // Currently we do not ignore nulls in the extension data. The JsonDocument would also need to support this mode
+                // for any lower-level nulls.
+                Assert.Equal(1, obj.MyOverflow.Count);
+                Assert.Equal(JsonValueType.Null, obj.MyOverflow["MyNestedClassMissing"].Type);
+            }
+        }
+
+        [Fact]
+        public static void InvalidExtensionPropertyFail()
+        {
+            // Baseline
+            JsonSerializer.Parse<ClassWithExtensionProperty>(@"{}");
+            JsonSerializer.Parse<ClassWithExtensionPropertyAsObject>(@"{}");
+
+            Assert.Throws<InvalidOperationException>(() => JsonSerializer.Parse<ClassWithInvalidExtensionProperty>(@"{}"));
+            Assert.Throws<InvalidOperationException>(() => JsonSerializer.Parse<ClassWithTwoExtensionPropertys>(@"{}"));
+        }
+
+        [Fact]
+        public static void IgnoredDataShouldNotBeExtensionData()
+        {
+            ClassWithIgnoredData obj = JsonSerializer.Parse<ClassWithIgnoredData>(@"{""MyInt"":1}");
+
+            Assert.Equal(0, obj.MyInt);
+            Assert.Null(obj.MyOverflow);
+        }
+
+        [Fact]
+        public static void InvalidExtensionValue()
+        {
+            // Baseline
+            ClassWithExtensionPropertyAlreadyInstantiated obj = JsonSerializer.Parse<ClassWithExtensionPropertyAlreadyInstantiated>(@"{}");
+            obj.MyOverflow.Add("test", new object());
+
+            try
+            {
+                JsonSerializer.ToString(obj);
+                Assert.True(false, "InvalidOperationException should have thrown.");
+            }
+            catch (InvalidOperationException e)
+            {
+                // Verify the exception contains the property name and invalid type.
+                Assert.Contains("ClassWithExtensionPropertyAlreadyInstantiated.MyOverflow", e.Message);
+                Assert.Contains("System.Object", e.Message);
+            }
+        }
+
+        [Fact]
+        public static void ObjectTree()
+        {
+            string json = @"{""MyIntMissing"":2, ""MyReference"":{""MyIntMissingChild"":3}}";
+
+            ClassWithReference obj = JsonSerializer.Parse<ClassWithReference>(json);
+            Assert.IsType<JsonElement>(obj.MyOverflow["MyIntMissing"]);
+            Assert.Equal(1, obj.MyOverflow.Count);
+            Assert.Equal(2, obj.MyOverflow["MyIntMissing"].GetInt32());
+
+            ClassWithExtensionProperty child = obj.MyReference;
+            Assert.IsType<JsonElement>(child.MyOverflow["MyIntMissingChild"]);
+            Assert.IsType<JsonElement>(child.MyOverflow["MyIntMissingChild"]);
+            Assert.Equal(1, child.MyOverflow.Count);
+            Assert.Equal(3, child.MyOverflow["MyIntMissingChild"].GetInt32());
+        }
+
+        public class ClassWithExtensionPropertyAlreadyInstantiated
+        {
+            public ClassWithExtensionPropertyAlreadyInstantiated()
+            {
+                MyOverflow = new Dictionary<string, object>();
+            }
+
+            [JsonExtensionData]
+            public Dictionary<string, object> MyOverflow { get; set; }
+        }
+
+        public class ClassWithExtensionPropertyAsObject
+        {
+            [JsonExtensionData]
+            public Dictionary<string, object> MyOverflow { get; set; }
+        }
+
+        public class ClassWithIgnoredData
+        {
+            [JsonExtensionData]
+            public Dictionary<string, object> MyOverflow { get; set; }
+
+            [JsonIgnore]
+            public int MyInt { get; set; }
+        }
+
+        public class ClassWithInvalidExtensionProperty
+        {
+            [JsonExtensionData]
+            public Dictionary<string, int> MyOverflow { get; set; }
+        }
+
+        public class ClassWithTwoExtensionPropertys
+        {
+            [JsonExtensionData]
+            public Dictionary<string, object> MyOverflow1 { get; set; }
+
+            [JsonExtensionData]
+            public Dictionary<string, object> MyOverflow2 { get; set; }
+        }
+
+        public class ClassWithReference
+        {
+            [JsonExtensionData]
+            public Dictionary<string, JsonElement> MyOverflow { get; set; }
+
+            public ClassWithExtensionProperty MyReference { get; set; }
+        }
+    }
+}
index 2806ee5..9fc15c0 100644 (file)
@@ -91,6 +91,54 @@ namespace System.Text.Json.Serialization.Tests
         }
 
         [Fact]
+        public static void ExtensionDataUsesReaderOptions()
+        {
+            // We just verify trailing commas.
+            const string json = @"{""MyIntMissing"":2,}";
+
+            // Verify baseline without options.
+            Assert.Throws<JsonException>(() => JsonSerializer.Parse<ClassWithExtensionProperty>(json));
+
+            // Verify baseline with options.
+            var options = new JsonSerializerOptions();
+            Assert.Throws<JsonException>(() => JsonSerializer.Parse<ClassWithExtensionProperty>(json, options));
+
+            // Set AllowTrailingCommas to true.
+            options = new JsonSerializerOptions();
+            options.AllowTrailingCommas = true;
+            JsonSerializer.Parse<ClassWithExtensionProperty>(json, options);
+        }
+
+        [Fact]
+        public static void ExtensionDataUsesWriterOptions()
+        {
+            // We just verify whitespace.
+
+            ClassWithExtensionProperty obj = JsonSerializer.Parse<ClassWithExtensionProperty>(@"{""MyIntMissing"":2}");
+
+            // Verify baseline without options.
+            string json = JsonSerializer.ToString(obj);
+            Assert.False(HasNewLine());
+
+            // Verify baseline with options.
+            var options = new JsonSerializerOptions();
+            json = JsonSerializer.ToString(obj, options);
+            Assert.False(HasNewLine());
+
+            // Set AllowTrailingCommas to true.
+            options = new JsonSerializerOptions();
+            options.WriteIndented = true;
+            json = JsonSerializer.ToString(obj, options);
+            Assert.True(HasNewLine());
+
+            bool HasNewLine()
+            {
+                int iEnd = json.IndexOf("2", json.IndexOf("MyIntMissing"));
+                return json.Substring(iEnd + 1).StartsWith(Environment.NewLine);
+            }
+        }
+
+        [Fact]
         public static void ReadCommentHandling()
         {
             Assert.Throws<JsonException>(() => JsonSerializer.Parse<object>("/* commment */"));
index dd4f8ec..f85cc12 100644 (file)
@@ -75,25 +75,29 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Equal(@"MyString", obj.MyString);
             Assert.Equal(@"MyStringWithIgnore", obj.MyStringWithIgnore);
             Assert.Equal(2, obj.MyStringsWithIgnore.Length);
+            Assert.Equal(1, obj.MyDictionaryWithIgnore["Key"]);
 
             // Verify serialize.
             string json = JsonSerializer.ToString(obj);
             Assert.Contains(@"""MyString""", json);
             Assert.DoesNotContain(@"MyStringWithIgnore", json);
             Assert.DoesNotContain(@"MyStringsWithIgnore", json);
+            Assert.DoesNotContain(@"MyDictionaryWithIgnore", json);
 
             // Verify deserialize default.
             obj = JsonSerializer.Parse<ClassWithIgnoreAttributeProperty>(@"{}");
             Assert.Equal(@"MyString", obj.MyString);
             Assert.Equal(@"MyStringWithIgnore", obj.MyStringWithIgnore);
             Assert.Equal(2, obj.MyStringsWithIgnore.Length);
+            Assert.Equal(1, obj.MyDictionaryWithIgnore["Key"]);
 
             // Verify deserialize ignores the json for MyStringWithIgnore and MyStringsWithIgnore.
             obj = JsonSerializer.Parse<ClassWithIgnoreAttributeProperty>(
-                @"{""MyString"":""Hello"", ""MyStringWithIgnore"":""IgnoreMe"", ""MyStringsWithIgnore"":[""IgnoreMe""]}");
+                @"{""MyString"":""Hello"", ""MyStringWithIgnore"":""IgnoreMe"", ""MyStringsWithIgnore"":[""IgnoreMe""], ""MyDictionaryWithIgnore"":{""Key"":9}}");
             Assert.Contains(@"Hello", obj.MyString);
             Assert.Equal(@"MyStringWithIgnore", obj.MyStringWithIgnore);
             Assert.Equal(2, obj.MyStringsWithIgnore.Length);
+            Assert.Equal(1, obj.MyDictionaryWithIgnore["Key"]);
         }
 
         // Todo: add tests with missing object property and missing collection property.
@@ -175,12 +179,16 @@ namespace System.Text.Json.Serialization.Tests
         {
             public ClassWithIgnoreAttributeProperty()
             {
+                MyDictionaryWithIgnore = new Dictionary<string, int> { { "Key", 1 } };
                 MyString = "MyString";
                 MyStringWithIgnore = "MyStringWithIgnore";
                 MyStringsWithIgnore = new string[] { "1", "2" };
             }
 
             [JsonIgnore]
+            public Dictionary<string, int> MyDictionaryWithIgnore { get; set; }
+
+            [JsonIgnore]
             public string MyStringWithIgnore { get; set; }
 
             public string MyString { get; set; }
index a12e60e..e645316 100644 (file)
@@ -1280,6 +1280,15 @@ namespace System.Text.Json.Serialization.Tests
         public int Aѧ34567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 { get; set; }
     }
 
+    public class ClassWithExtensionProperty
+    {
+        public SimpleTestClass MyNestedClass { get; set; }
+        public int MyInt { get; set; }
+
+        [JsonExtensionData]
+        public IDictionary<string, JsonElement> MyOverflow { get; set; }
+    }
+
     public class TestClassWithNestedObjectCommentsInner : ITestClass
     {
         public SimpleTestClass MyData { get; set; }
index fb067fa..3f0bb1c 100644 (file)
@@ -29,6 +29,7 @@
     <Compile Include="Serialization\CyclicTests.cs" />
     <Compile Include="Serialization\DictionaryTests.cs" />
     <Compile Include="Serialization\EnumTests.cs" />
+    <Compile Include="Serialization\ExtensionDataTests.cs" />
     <Compile Include="Serialization\JsonElementTests.cs" />
     <Compile Include="Serialization\ExceptionTests.cs" />
     <Compile Include="Serialization\Null.ReadTests.cs" />