Add serializer support for IDictionary non-primitive types (dotnet/corefx#37186)
authorSteve Harter <steveharter@users.noreply.github.com>
Fri, 26 Apr 2019 18:59:28 +0000 (11:59 -0700)
committerGitHub <noreply@github.com>
Fri, 26 Apr 2019 18:59:28 +0000 (11:59 -0700)
Commit migrated from https://github.com/dotnet/corefx/commit/c5bb6abec986627f90b2e7e648dc4e733cdbf63b

21 files changed:
src/libraries/System.Text.Json/src/System.Text.Json.csproj
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/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.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.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/ReadStackFrame.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs
src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs
src/libraries/System.Text.Json/tests/Serialization/PolymorphicTests.cs
src/libraries/System.Text.Json/tests/Serialization/TestClasses.SimpleTestClassWithNullables.cs

index 5070433..28d6bbc 100644 (file)
@@ -17,7 +17,6 @@
     <Compile Include="System\Text\Json\JsonHelpers.cs" />
     <Compile Include="System\Text\Json\JsonHelpers.Date.cs" />
     <Compile Include="System\Text\Json\JsonTokenType.cs" />
-    <Compile Include="System\Text\Json\Serialization\JsonSerializer.Write.HandleDictionary.cs" />
     <Compile Include="System\Text\Json\ThrowHelper.cs" />
     <Compile Include="System\Text\Json\ThrowHelper.Serialization.cs" />
     <Compile Include="System\Text\Json\Document\JsonDocument.cs" />
@@ -90,6 +89,7 @@
     <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.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.String.cs" />
index a9c5c5a..df4f992 100644 (file)
@@ -9,6 +9,18 @@ namespace System.Text.Json.Serialization
 {
     internal partial class JsonClassInfo
     {
+        private JsonPropertyInfo AddPolicyProperty(Type propertyType, JsonSerializerOptions options)
+        {
+            // A policy property is not a real property on a type; instead it leverages the existing converter
+            // logic and generic support to avoid boxing. It is used with values types and elements from collections and
+            // dictionaries. Typically it would represent a CLR type such as System.String.
+            return AddProperty(
+                propertyType,
+                propertyInfo : null,        // Not a real property so this is null.
+                classType : typeof(object), // A dummy type (not used).
+                options : options);
+
+        }
         private JsonPropertyInfo AddProperty(Type propertyType, PropertyInfo propertyInfo, Type classType, JsonSerializerOptions options)
         {
             JsonPropertyInfo jsonInfo = CreateProperty(propertyType, propertyType, propertyInfo, classType, options);
index d26b4ec..a4f8122 100644 (file)
@@ -114,7 +114,7 @@ namespace System.Text.Json.Serialization
             else if (ClassType == ClassType.Enumerable || ClassType == ClassType.Dictionary)
             {
                 // Add a single property that maps to the class type so we can have policies applied.
-                JsonPropertyInfo jsonPropertyInfo = AddProperty(type, propertyInfo: null, type, options);
+                JsonPropertyInfo jsonPropertyInfo = AddPolicyProperty(type, options);
 
                 // Use the type from the property policy to get any late-bound concrete types (from an interface like IDictionary).
                 CreateObject = options.ClassMaterializerStrategy.CreateConstructor(jsonPropertyInfo.RuntimePropertyType);
@@ -127,7 +127,7 @@ namespace System.Text.Json.Serialization
             else if (ClassType == ClassType.Value)
             {
                 // Add a single property that maps to the class type so we can have policies applied.
-                AddProperty(type, propertyInfo: null, type, options);
+                AddPolicyProperty(type, options);
             }
             else
             {
@@ -319,19 +319,22 @@ namespace System.Text.Json.Serialization
 
                     if (propertyType.IsGenericType)
                     {
-                        if (GetClassType(propertyType) == ClassType.Dictionary)
+                        if (GetClassType(propertyType) == ClassType.Dictionary &&
+                            args.Length >= 2) // It is >= 2 in case there is a Dictionary<TKey, TValue, TSomeExtension>.
                         {
+                            
                             elementType = args[1];
                         }
-                        else
+                        else if (args.Length >= 1) // It is >= 1 in case there is an IEnumerable<T, TSomeExtension>.
                         {
+                            Debug.Assert(GetClassType(propertyType) == ClassType.Enumerable);
                             elementType = args[0];
                         }
                     }
                     else
                     {
                         // Unable to determine collection type; attempt to use object which will be used to create loosely-typed collection.
-                        return typeof(object);
+                        elementType = typeof(object);
                     }
                 }
             }
index 1fb7bc1..f46a5c9 100644 (file)
@@ -251,7 +251,7 @@ namespace System.Text.Json.Serialization
             other._escapedName = _escapedName;
         }
 
-        public abstract object GetValueAsObject(object obj, JsonSerializerOptions options);
+        public abstract object GetValueAsObject(object obj);
 
         public TAttribute GetAttribute<TAttribute>() where TAttribute : Attribute
         {
@@ -266,7 +266,7 @@ namespace System.Text.Json.Serialization
 
         public abstract void Read(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader);
         public abstract void ReadEnumerable(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader);
-        public abstract void SetValueAsObject(object obj, object value, JsonSerializerOptions options);
+        public abstract void SetValueAsObject(object obj, object value);
 
         public abstract void Write(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer);
 
index b1e5e12..12d1e3a 100644 (file)
@@ -53,11 +53,7 @@ namespace System.Text.Json.Serialization
                 _isPropertyPolicy = true;
                 HasGetter = true;
                 HasSetter = true;
-
-                if (ClassType == ClassType.Dictionary)
-                {
-                    ValueConverter = DefaultConverters<TRuntimeProperty>.s_converter;
-                }
+                ValueConverter = DefaultConverters<TRuntimeProperty>.s_converter;
             }
 
             GetPolicies(options);
@@ -69,7 +65,7 @@ namespace System.Text.Json.Serialization
             base.GetPolicies(options);
         }
 
-        public override object GetValueAsObject(object obj, JsonSerializerOptions options)
+        public override object GetValueAsObject(object obj)
         {
             if (_isPropertyPolicy)
             {
@@ -80,7 +76,7 @@ namespace System.Text.Json.Serialization
             return Get((TClass)obj);
         }
 
-        public override void SetValueAsObject(object obj, object value, JsonSerializerOptions options)
+        public override void SetValueAsObject(object obj, object value)
         {
             Debug.Assert(Set != null);
             TDeclaredProperty typedValue = (TDeclaredProperty)value;
index b07fe02..14c445c 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.Collections.Generic;
 using System.Diagnostics;
 using System.Reflection;
@@ -79,7 +78,7 @@ namespace System.Text.Json.Serialization
         public override void ApplyNullValue(JsonSerializerOptions options, ref ReadStack state)
         {
             Debug.Assert(state.Current.JsonPropertyInfo != null);
-            state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, null, options);
+            state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, value : null);
         }
 
         // todo: have the caller check if current.Enumerator != null and call WriteEnumerable of the underlying property directly to avoid an extra virtual call.
index d2d0699..afdfcdc 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.Generic;
 using System.Diagnostics;
 using System.Reflection;
index ab2efb3..5d12c8f 100644 (file)
@@ -40,42 +40,40 @@ namespace System.Text.Json.Serialization
                 ThrowHelper.ThrowJsonReaderException_DeserializeUnableToConvertValue(arrayType, reader, state);
             }
 
-            Debug.Assert(state.Current.IsPropertyEnumerable());
-            if (state.Current.IsPropertyEnumerable())
+            Debug.Assert(state.Current.IsPropertyEnumerable || state.Current.IsDictionary);
+
+            if (state.Current.EnumerableCreated)
             {
-                if (state.Current.EnumerableCreated)
-                {
-                    // A nested json array so push a new stack frame.
-                    Type elementType = state.Current.JsonClassInfo.ElementClassInfo.GetPolicyProperty().RuntimePropertyType;
+                // A nested json array so push a new stack frame.
+                Type elementType = state.Current.JsonClassInfo.ElementClassInfo.GetPolicyProperty().RuntimePropertyType;
 
-                    state.Push();
+                state.Push();
 
-                    state.Current.Initialize(elementType, options);
-                    state.Current.PopStackOnEndArray = true;
-                }
-                else
-                {
-                    state.Current.EnumerableCreated = true;
-                }
+                state.Current.Initialize(elementType, options);
+                state.Current.PopStackOnEnd = true;
+            }
+            else
+            {
+                state.Current.EnumerableCreated = true;
+            }
 
-                jsonPropertyInfo = state.Current.JsonPropertyInfo;
+            jsonPropertyInfo = state.Current.JsonPropertyInfo;
 
-                // If current property is already set (from a constructor, for example) leave as-is
-                if (jsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue, options) == null)
+            // If current property is already set (from a constructor, for example) leave as-is.
+            if (jsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue) == null)
+            {
+                // Create the enumerable.
+                object value = ReadStackFrame.CreateEnumerableValue(ref reader, ref state, options);
+                if (value != null)
                 {
-                    // Create the enumerable.
-                    object value = ReadStackFrame.CreateEnumerableValue(ref reader, ref state, options);
-                    if (value != null)
+                    if (state.Current.ReturnValue != null)
                     {
-                        if (state.Current.ReturnValue != null)
-                        {
-                            state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, value, options);
-                        }
-                        else
-                        {
-                            // Primitive arrays being returned without object
-                            state.Current.SetReturnValue(value, options);
-                        }
+                        state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, value);
+                    }
+                    else
+                    {
+                        // Primitive arrays being returned without object
+                        state.Current.SetReturnValue(value);
                     }
                 }
             }
@@ -117,8 +115,8 @@ namespace System.Text.Json.Serialization
                 setPropertyDirectly = false;
             }
 
-            bool valueReturning = state.Current.PopStackOnEndArray;
-            if (state.Current.PopStackOnEndArray)
+            bool valueReturning = state.Current.PopStackOnEnd;
+            if (state.Current.PopStackOnEnd)
             {
                 state.Pop();
             }
@@ -132,7 +130,7 @@ namespace System.Text.Json.Serialization
                     state.Current.ReturnValue = value;
                     return true;
                 }
-                else if (state.Current.IsEnumerable())
+                else if (state.Current.IsEnumerable || state.Current.IsDictionary)
                 {
                     // Returning a non-converted list.
                     return true;
@@ -153,7 +151,7 @@ namespace System.Text.Json.Serialization
         // If this method is changed, also change ApplyValueToEnumerable.
         internal static void ApplyObjectToEnumerable(object value, JsonSerializerOptions options, ref ReadStackFrame frame, bool setPropertyDirectly = false)
         {
-            if (frame.IsEnumerable())
+            if (frame.IsEnumerable)
             {
                 if (frame.TempEnumerableValues != null)
                 {
@@ -164,7 +162,7 @@ namespace System.Text.Json.Serialization
                     ((IList)frame.ReturnValue).Add(value);
                 }
             }
-            else if (!setPropertyDirectly && frame.IsPropertyEnumerable())
+            else if (!setPropertyDirectly && frame.IsPropertyEnumerable)
             {
                 Debug.Assert(frame.JsonPropertyInfo != null);
                 Debug.Assert(frame.ReturnValue != null);
@@ -174,23 +172,18 @@ namespace System.Text.Json.Serialization
                 }
                 else
                 {
-                    ((IList)frame.JsonPropertyInfo.GetValueAsObject(frame.ReturnValue, options)).Add(value);
+                    ((IList)frame.JsonPropertyInfo.GetValueAsObject(frame.ReturnValue)).Add(value);
                 }
             }
-            else if (frame.IsDictionary())
-            {
-                ((IDictionary)frame.ReturnValue).Add(frame.KeyName, value);
-            }
-            else if (frame.IsPropertyADictionary())
+            else if (frame.IsDictionary)
             {
-                Debug.Assert(frame.JsonPropertyInfo != null);
                 Debug.Assert(frame.ReturnValue != null);
-                frame.JsonPropertyInfo.SetValueAsObject(frame.ReturnValue, value, options);
+                ((IDictionary)frame.JsonPropertyInfo.GetValueAsObject(frame.ReturnValue)).Add(frame.KeyName, value);
             }
             else
             {
                 Debug.Assert(frame.JsonPropertyInfo != null);
-                frame.JsonPropertyInfo.SetValueAsObject(frame.ReturnValue, value, options);
+                frame.JsonPropertyInfo.SetValueAsObject(frame.ReturnValue, value);
             }
         }
 
@@ -200,7 +193,7 @@ namespace System.Text.Json.Serialization
             JsonSerializerOptions options,
             ref ReadStackFrame frame)
         {
-            if (frame.IsEnumerable())
+            if (frame.IsEnumerable)
             {
                 if (frame.TempEnumerableValues != null)
                 {
@@ -211,7 +204,7 @@ namespace System.Text.Json.Serialization
                     ((IList<TProperty>)frame.ReturnValue).Add(value);
                 }
             }
-            else if (frame.IsPropertyEnumerable())
+            else if (frame.IsPropertyEnumerable)
             {
                 Debug.Assert(frame.JsonPropertyInfo != null);
                 Debug.Assert(frame.ReturnValue != null);
@@ -221,24 +214,18 @@ namespace System.Text.Json.Serialization
                 }
                 else
                 {
-                    ((IList<TProperty>)frame.JsonPropertyInfo.GetValueAsObject(frame.ReturnValue, options)).Add(value);
+                    ((IList<TProperty>)frame.JsonPropertyInfo.GetValueAsObject(frame.ReturnValue)).Add(value);
                 }
             }
-            else if (frame.IsDictionary())
+            else if (frame.IsDictionary)
             {
-                // todo: use TryAdd and throw JsonReaderException
-                ((IDictionary<string, TProperty>)frame.ReturnValue).Add(frame.KeyName, value);
-            }
-            else if (frame.IsPropertyADictionary())
-            {
-                Debug.Assert(frame.JsonPropertyInfo != null);
                 Debug.Assert(frame.ReturnValue != null);
-                ((IDictionary<string, TProperty>)frame.JsonPropertyInfo.GetValueAsObject(frame.ReturnValue, options)).Add(frame.KeyName, value);
+                ((IDictionary<string, TProperty>)frame.JsonPropertyInfo.GetValueAsObject(frame.ReturnValue)).Add(frame.KeyName, value);
             }
             else
             {
                 Debug.Assert(frame.JsonPropertyInfo != null);
-                frame.JsonPropertyInfo.SetValueAsObject(frame.ReturnValue, value, options);
+                frame.JsonPropertyInfo.SetValueAsObject(frame.ReturnValue, value);
             }
         }
     }
index c4ea004..ad97372 100644 (file)
@@ -30,13 +30,13 @@ namespace System.Text.Json.Serialization
                 ThrowHelper.ThrowJsonReaderException_DeserializeCannotBeNull(reader, state);
             }
 
-            if (state.Current.IsEnumerable() || state.Current.IsDictionary())
+            if (state.Current.IsEnumerable || state.Current.IsDictionary)
             {
                 ApplyObjectToEnumerable(null, options, ref state.Current);
                 return false;
             }
 
-            if (state.Current.IsPropertyEnumerable() || state.Current.IsPropertyADictionary())
+            if (state.Current.IsPropertyEnumerable)
             {
                 state.Current.JsonPropertyInfo.ApplyNullValue(options, ref state);
                 return false;
@@ -50,7 +50,7 @@ namespace System.Text.Json.Serialization
 
             if (!propertyInfo.IgnoreNullValues)
             {
-                state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, null, options);
+                state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, value : null);
             }
 
             return false;
index 1289b49..ae17128 100644 (file)
@@ -2,11 +2,13 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
+using System.Diagnostics;
+
 namespace System.Text.Json.Serialization
 {
     public static partial class JsonSerializer
     {
-        private static void HandleStartObject(JsonSerializerOptions options, ref ReadStack state)
+        private static void HandleStartObject(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state)
         {
             if (state.Current.Skip())
             {
@@ -15,23 +17,49 @@ namespace System.Text.Json.Serialization
                 return;
             }
 
-            if (state.Current.IsDictionary())
-            {
-                // Fall through and treat as a return value.
-            }
-            else if (state.Current.IsEnumerable() || state.Current.IsPropertyEnumerable() || state.Current.IsPropertyADictionary())
+            if (state.Current.IsProcessingEnumerable)
             {
-                // An array of objects either on the current property or on a list
                 Type objType = state.Current.GetElementType();
                 state.Push();
-                state.Current.JsonClassInfo = options.GetOrAddClass(objType);
+                state.Current.Initialize(objType, options);
             }
             else if (state.Current.JsonPropertyInfo != null)
             {
-                // Nested object
-                Type objType = state.Current.JsonPropertyInfo.RuntimePropertyType;
-                state.Push();
-                state.Current.JsonClassInfo = options.GetOrAddClass(objType);
+                if (state.Current.IsDictionary)
+                {
+                    // Verify that the Dictionary can be deserialized by having <string> as first generic argument.
+                    Debug.Assert(state.Current.JsonClassInfo.Type.GetGenericArguments().Length >= 1);
+                    if (state.Current.JsonClassInfo.Type.GetGenericArguments()[0].UnderlyingSystemType != typeof(string))
+                    {
+                        ThrowHelper.ThrowJsonReaderException_DeserializeUnableToConvertValue(state.Current.JsonClassInfo.Type, reader, state);
+                    }
+
+                    ClassType classType = state.Current.JsonClassInfo.ElementClassInfo.ClassType;
+
+                    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
+                    {
+                        Debug.Assert(classType == ClassType.Object || classType == ClassType.Dictionary);
+
+                        // A nested object or dictionary.
+                        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);
+                }
             }
 
             JsonClassInfo classInfo = state.Current.JsonClassInfo;
index 51b51aa..4f3c737 100644 (file)
@@ -19,7 +19,7 @@ namespace System.Text.Json.Serialization
                 jsonPropertyInfo = state.Current.JsonClassInfo.CreatePolymorphicProperty(jsonPropertyInfo, typeof(object), options);
             }
 
-            bool lastCall = (!state.Current.IsProcessingEnumerableOrDictionary() && state.Current.ReturnValue == null);
+            bool lastCall = (!state.Current.IsProcessingEnumerableOrDictionary && state.Current.ReturnValue == null);
 
             jsonPropertyInfo.Read(tokenType, options, ref state, ref reader);
             return lastCall;
index 08b8d0b..94b89a4 100644 (file)
@@ -42,7 +42,7 @@ namespace System.Text.Json.Serialization
                         Debug.Assert(state.Current.ReturnValue != default);
                         Debug.Assert(state.Current.JsonClassInfo != default);
 
-                        if (state.Current.IsDictionary())
+                        if (state.Current.IsDictionary)
                         {
                             string keyName = reader.GetString();
                             if (options.DictionaryKeyPolicy != null)
@@ -75,7 +75,7 @@ namespace System.Text.Json.Serialization
                 }
                 else if (tokenType == JsonTokenType.StartObject)
                 {
-                    HandleStartObject(options, ref state);
+                    HandleStartObject(options, ref reader, ref state);
                 }
                 else if (tokenType == JsonTokenType.EndObject)
                 {
index bcacc6f..bbba769 100644 (file)
@@ -18,8 +18,6 @@ namespace System.Text.Json.Serialization
             Utf8JsonWriter writer,
             ref WriteStack state)
         {
-            Debug.Assert(state.Current.JsonPropertyInfo.ClassType == ClassType.Dictionary);
-
             JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo;
             if (!jsonPropertyInfo.ShouldSerialize)
             {
@@ -29,25 +27,17 @@ namespace System.Text.Json.Serialization
 
             if (state.Current.Enumerator == null)
             {
-                IEnumerable enumerable = (IEnumerable)jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue, options);
+                IEnumerable enumerable = (IEnumerable)jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue);
 
                 if (enumerable == null)
                 {
                     // Write a null object or enumerable.
-                    writer.WriteNull(jsonPropertyInfo.Name);
+                    state.Current.WriteObjectOrArrayStart(ClassType.Dictionary, writer, writeNull : true);
                     return true;
                 }
 
                 state.Current.Enumerator = enumerable.GetEnumerator();
-
-                if (jsonPropertyInfo.Name == null)
-                {
-                    writer.WriteStartObject();
-                }
-                else
-                {
-                    writer.WriteStartObject(jsonPropertyInfo.Name);
-                }
+                state.Current.WriteObjectOrArrayStart(ClassType.Dictionary, writer);
             }
 
             if (state.Current.Enumerator.MoveNext())
@@ -55,8 +45,7 @@ namespace System.Text.Json.Serialization
                 // Check for polymorphism.
                 if (elementClassInfo.ClassType == ClassType.Unknown)
                 {
-                    //todo:test
-                    object currentValue = ((IDictionaryEnumerator)(state.Current.Enumerator)).Entry;
+                    object currentValue = ((IDictionaryEnumerator)state.Current.Enumerator).Entry;
                     GetRuntimeClassInfo(currentValue, ref elementClassInfo, options);
                 }
 
@@ -71,8 +60,10 @@ namespace System.Text.Json.Serialization
                 else
                 {
                     // An object or another enumerator requires a new stack frame.
-                    object nextValue = state.Current.Enumerator.Current;
-                    state.Push(elementClassInfo, nextValue);
+                    var enumerator = (IDictionaryEnumerator)state.Current.Enumerator;
+                    object value = enumerator.Value;
+                    state.Push(elementClassInfo, value);
+                    state.Current.KeyName = (string)enumerator.Key;
                 }
 
                 return false;
@@ -81,7 +72,14 @@ namespace System.Text.Json.Serialization
             // We are done enumerating.
             writer.WriteEndObject();
 
-            state.Current.EndDictionary();
+            if (state.Current.PopStackOnEnd)
+            {
+                state.Pop();
+            }
+            else
+            {
+                state.Current.EndDictionary();
+            }
 
             return true;
         }
@@ -119,28 +117,35 @@ namespace System.Text.Json.Serialization
             }
             else
             {
+                byte[] utf8Key = Encoding.UTF8.GetBytes(key);
 #if true
                 // temporary behavior until the writer can accept escaped string.
-                byte[] utf8Key = Encoding.UTF8.GetBytes(key);
                 converter.Write(utf8Key, value, writer);
 #else
-                byte[] pooledKey = null;
-                byte[] utf8Key = Encoding.UTF8.GetBytes(key);
-                int length = JsonWriterHelper.GetMaxEscapedLength(utf8Key.Length, 0);
+                int valueIdx = JsonWriterHelper.NeedsEscaping(utf8Key);
+                if (valueIdx == -1)
+                {
+                    converter.Write(utf8Key, value, writer);
+                }
+                else
+                {
+                    byte[] pooledKey = null;
+                    int length = JsonWriterHelper.GetMaxEscapedLength(utf8Key.Length, valueIdx);
 
-                Span<byte> escapedKey = length <= JsonConstants.StackallocThreshold ?
-                    stackalloc byte[length] :
-                    (pooledKey = ArrayPool<byte>.Shared.Rent(length));
+                    Span<byte> escapedKey = length <= JsonConstants.StackallocThreshold ?
+                        stackalloc byte[length] :
+                        (pooledKey = ArrayPool<byte>.Shared.Rent(length));
 
-                JsonWriterHelper.EscapeString(utf8Key, escapedKey, 0, out int written);
+                    JsonWriterHelper.EscapeString(utf8Key, escapedKey, valueIdx, out int written);
 
-                converter.Write(escapedKey.Slice(0, written), value, writer);
+                    converter.Write(escapedKey.Slice(0, written), value, writer);
 
-                if (pooledKey != null)
-                {
-                    // We clear the array because it is "user data" (although a property name).
-                    new Span<byte>(pooledKey, 0, written).Clear();
-                    ArrayPool<byte>.Shared.Return(pooledKey);
+                    if (pooledKey != null)
+                    {
+                        // We clear the array because it is "user data" (although a property name).
+                        new Span<byte>(pooledKey, 0, written).Clear();
+                        ArrayPool<byte>.Shared.Return(pooledKey);
+                    }
                 }
 #endif
             }
index da494ef..74d0ca5 100644 (file)
@@ -26,25 +26,18 @@ namespace System.Text.Json.Serialization
 
             if (state.Current.Enumerator == null)
             {
-                IEnumerable enumerable = (IEnumerable)jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue, options);
+                IEnumerable enumerable = (IEnumerable)jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue);
 
                 if (enumerable == null)
                 {
                     // Write a null object or enumerable.
-                    writer.WriteNull(jsonPropertyInfo.Name);
+                    state.Current.WriteObjectOrArrayStart(ClassType.Enumerable, writer, writeNull: true);
                     return true;
                 }
 
                 state.Current.Enumerator = enumerable.GetEnumerator();
 
-                if (jsonPropertyInfo.Name == null)
-                {
-                    writer.WriteStartArray();
-                }
-                else
-                {
-                    writer.WriteStartArray(jsonPropertyInfo.Name);
-                }
+                state.Current.WriteObjectOrArrayStart(ClassType.Enumerable, writer);
             }
 
             if (state.Current.Enumerator.MoveNext())
@@ -78,7 +71,7 @@ namespace System.Text.Json.Serialization
             // We are done enumerating.
             writer.WriteEndArray();
 
-            if (state.Current.PopStackOnEndArray)
+            if (state.Current.PopStackOnEnd)
             {
                 state.Pop();
             }
index 8cddfd7..4e8585b 100644 (file)
@@ -18,15 +18,7 @@ namespace System.Text.Json.Serialization
             // Write the start.
             if (!state.Current.StartObjectWritten)
             {
-                if (state.Current.JsonPropertyInfo?._escapedName == null)
-                {
-                    writer.WriteStartObject();
-                }
-                else
-                {
-                    writer.WriteStartObject(state.Current.JsonPropertyInfo._escapedName);
-                }
-                state.Current.StartObjectWritten = true;
+                state.Current.WriteObjectOrArrayStart(ClassType.Object, writer);
             }
 
             // Determine if we are done enumerating properties.
@@ -67,7 +59,7 @@ namespace System.Text.Json.Serialization
             // Check for polymorphism.
             if (jsonPropertyInfo.ClassType == ClassType.Unknown)
             {
-                currentValue = jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue, options);
+                currentValue = jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue);
                 obtainedValue = true;
                 GetRuntimePropertyInfo(currentValue, state.Current.JsonClassInfo, ref jsonPropertyInfo, options);
             }
@@ -108,7 +100,7 @@ namespace System.Text.Json.Serialization
             // A property that returns an object.
             if (!obtainedValue)
             {
-                currentValue = jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue, options);
+                currentValue = jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue);
             }
 
             if (currentValue != null)
index 946815b..54e11c6 100644 (file)
@@ -11,116 +11,74 @@ namespace System.Text.Json.Serialization
     internal struct ReadStackFrame
     {
         // The object (POCO or IEnumerable) that is being populated
-        internal object ReturnValue;
-        internal JsonClassInfo JsonClassInfo;
+        public object ReturnValue;
+        public JsonClassInfo JsonClassInfo;
 
-        // Support Dictionary
-        internal string KeyName;
+        // Support Dictionary keys.
+        public string KeyName;
 
-        // Current property values
-        internal JsonPropertyInfo JsonPropertyInfo;
-        internal bool PopStackOnEndArray;
-        internal bool EnumerableCreated;
+        // Current property values.
+        public JsonPropertyInfo JsonPropertyInfo;
 
-        // Support System.Array and other types that don't implement IList
-        internal IList TempEnumerableValues;
+        // 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;
 
         // For performance, we order the properties by the first deserialize and PropertyIndex helps find the right slot quicker.
-        internal int PropertyIndex;
-        internal List<PropertyRef> PropertyRefCache;
+        public int PropertyIndex;
+        public List<PropertyRef> PropertyRefCache;
+
+        // The current JSON data for a property does not match a given POCO, so ignore the property (recursively).
+        public bool Drain;
 
-        // The current JSON data for a property does not match a given POCO, so ignore the property (recursively for enumerables or object).
-        internal bool Drain;
+        public bool IsDictionary => JsonClassInfo.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;
 
-        internal void Initialize(Type type, JsonSerializerOptions options)
+        public void Initialize(Type type, JsonSerializerOptions options)
         {
             JsonClassInfo = options.GetOrAddClass(type);
+            InitializeJsonPropertyInfo();
+        }
+
+        public void InitializeJsonPropertyInfo()
+        {
             if (JsonClassInfo.ClassType == ClassType.Value || JsonClassInfo.ClassType == ClassType.Enumerable || JsonClassInfo.ClassType == ClassType.Dictionary)
             {
                 JsonPropertyInfo = JsonClassInfo.GetPolicyProperty();
             }
         }
 
-        internal void Reset()
+        public void Reset()
         {
-            ReturnValue = null;
+            Drain = false;
             JsonClassInfo = null;
+            KeyName = null;
             PropertyRefCache = null;
-            PropertyIndex = 0;
-            Drain = false;
-            ResetProperty();
+            ReturnValue = null;
+            EndObject();
         }
 
-        internal void ResetProperty()
+        public void ResetProperty()
         {
-            JsonPropertyInfo = null;
-            PopStackOnEndArray = false;
             EnumerableCreated = false;
+            JsonPropertyInfo = null;
+            PopStackOnEnd = false;
             TempEnumerableValues = null;
-            KeyName = null;
         }
 
-        internal bool IsProcessingEnumerableOrDictionary()
+        public void EndObject()
         {
-            return IsEnumerable() ||IsPropertyEnumerable() || IsDictionary() || IsPropertyADictionary();
-        }
-
-        internal bool IsEnumerable()
-        {
-            return JsonClassInfo.ClassType == ClassType.Enumerable;
-        }
-
-        internal bool IsDictionary()
-        {
-            return JsonClassInfo.ClassType == ClassType.Dictionary;
-        }
-
-        internal bool Skip()
-        {
-            return Drain || ReferenceEquals(JsonPropertyInfo, JsonSerializer.s_missingProperty);
-        }
-
-        internal bool IsPropertyEnumerable()
-        {
-            if (JsonPropertyInfo != null)
-            {
-                return JsonPropertyInfo.ClassType == ClassType.Enumerable;
-            }
-
-            return false;
-        }
-
-        internal bool IsPropertyADictionary()
-        {
-            if (JsonPropertyInfo != null)
-            {
-                return JsonPropertyInfo.ClassType == ClassType.Dictionary;
-            }
-
-            return false;
-        }
-
-        public Type GetElementType()
-        {
-            if (IsPropertyEnumerable())
-            {
-                return JsonPropertyInfo.ElementClassInfo.Type;
-            }
-
-            if (IsEnumerable())
-            {
-                return JsonClassInfo.ElementClassInfo.Type;
-            }
-
-            if (IsDictionary())
-            {
-                return JsonClassInfo.ElementClassInfo.Type;
-            }
-
-            return JsonPropertyInfo.RuntimePropertyType;
+            PropertyIndex = 0;
+            ResetProperty();
         }
 
-        internal static object CreateEnumerableValue(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options)
+        public static object CreateEnumerableValue(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options)
         {
             JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo;
 
@@ -157,9 +115,29 @@ namespace System.Text.Json.Serialization
             }
         }
 
-        internal static IEnumerable GetEnumerableValue(in ReadStackFrame current)
+        public Type GetElementType()
+        {
+            if (IsPropertyEnumerable)
+            {
+                return JsonPropertyInfo.ElementClassInfo.Type;
+            }
+
+            if (IsEnumerable)
+            {
+                return JsonClassInfo.ElementClassInfo.Type;
+            }
+
+            if (IsDictionary)
+            {
+                return JsonClassInfo.ElementClassInfo.Type;
+            }
+
+            return JsonPropertyInfo.RuntimePropertyType;
+        }
+
+        public static IEnumerable GetEnumerableValue(in ReadStackFrame current)
         {
-            if (current.IsEnumerable())
+            if (current.IsEnumerable)
             {
                 if (current.ReturnValue != null)
                 {
@@ -171,10 +149,15 @@ namespace System.Text.Json.Serialization
             return current.TempEnumerableValues;
         }
 
-        internal void SetReturnValue(object value, JsonSerializerOptions options)
+        public void SetReturnValue(object value)
         {
             Debug.Assert(ReturnValue == null);
             ReturnValue = value;
         }
+
+        public bool Skip()
+        {
+            return Drain || ReferenceEquals(JsonPropertyInfo, JsonSerializer.s_missingProperty);
+        }
     }
 }
index 4b614a9..5cc3453 100644 (file)
@@ -44,14 +44,16 @@ namespace System.Text.Json.Serialization
             Current.JsonClassInfo = nextClassInfo;
             Current.CurrentValue = nextValue;
 
-            if (nextClassInfo.ClassType == ClassType.Enumerable)
+            ClassType classType = nextClassInfo.ClassType;
+
+            if (classType == ClassType.Enumerable || nextClassInfo.ClassType == ClassType.Dictionary)
             {
-                Current.PopStackOnEndArray = true;
+                Current.PopStackOnEnd = true;
                 Current.JsonPropertyInfo = Current.JsonClassInfo.GetPolicyProperty();
             }
             else
             {
-                Debug.Assert(nextClassInfo.ClassType == ClassType.Object || nextClassInfo.ClassType == ClassType.Dictionary || nextClassInfo.ClassType == ClassType.Unknown);
+                Debug.Assert(nextClassInfo.ClassType == ClassType.Object || nextClassInfo.ClassType == ClassType.Unknown);
                 Current.PopStackOnEndObject = true;
             }
         }
index 72e3167..82f823b 100644 (file)
@@ -2,31 +2,40 @@
 // 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
 {
     internal struct WriteStackFrame
     {
-        // The object (POCO or IEnumerable) that is being populated
-        internal object CurrentValue;
-        internal JsonClassInfo JsonClassInfo;
+        // The object (POCO or IEnumerable) that is being populated.
+        public object CurrentValue;
+        public JsonClassInfo JsonClassInfo;
 
-        internal IEnumerator Enumerator;
+        // Support Dictionary keys.
+        public string KeyName;
 
-        // Current property values
-        internal JsonPropertyInfo JsonPropertyInfo;
+        // The current enumerator for the IEnumerable or IDictionary.
+        public IEnumerator Enumerator;
+
+        // Current property values.
+        public JsonPropertyInfo JsonPropertyInfo;
 
         // The current property.
-        internal int PropertyIndex;
+        public int PropertyIndex;
+
+        // Has the Start tag been written.
+        public bool StartObjectWritten;
 
-        // Has the Start tag been written
-        internal bool StartObjectWritten;
+        // Pop the stack when the current array or dictionary is done.
+        public bool PopStackOnEnd;
 
-        internal bool PopStackOnEndArray;
-        internal bool PopStackOnEndObject;
+        // Pop the stack when the current object is done.
+        public bool PopStackOnEndObject;
 
-        internal void Initialize(Type type, JsonSerializerOptions options)
+        public void Initialize(Type type, JsonSerializerOptions options)
         {
             JsonClassInfo = options.GetOrAddClass(type);
             if (JsonClassInfo.ClassType == ClassType.Value || JsonClassInfo.ClassType == ClassType.Enumerable || JsonClassInfo.ClassType == ClassType.Dictionary)
@@ -35,41 +44,102 @@ namespace System.Text.Json.Serialization
             }
         }
 
-        internal void Reset()
+        public void WriteObjectOrArrayStart(ClassType classType, Utf8JsonWriter writer, bool writeNull = false)
+        {
+            if (JsonPropertyInfo?._escapedName != null)
+            {
+                WriteObjectOrArrayStart(classType, JsonPropertyInfo?._escapedName, writer, writeNull);
+            }
+            else if (KeyName != null)
+            {
+                byte[] pooledKey = null;
+                byte[] utf8Key = Encoding.UTF8.GetBytes(KeyName);
+                int length = JsonWriterHelper.GetMaxEscapedLength(utf8Key.Length, 0);
+
+                Span<byte> escapedKey = length <= JsonConstants.StackallocThreshold ?
+                    stackalloc byte[length] :
+                    (pooledKey = ArrayPool<byte>.Shared.Rent(length));
+
+                JsonWriterHelper.EscapeString(utf8Key, escapedKey, 0, out int written);
+                Span<byte> propertyName = escapedKey.Slice(0, written);
+
+                WriteObjectOrArrayStart(classType, propertyName, writer, writeNull);
+
+                if (pooledKey != null)
+                {
+                    ArrayPool<byte>.Shared.Return(pooledKey);
+                }
+            }
+            else
+            {
+                Debug.Assert(writeNull == false);
+
+                // Write start without a property name.
+                if (classType == ClassType.Object || classType == ClassType.Dictionary)
+                {
+                    writer.WriteStartObject();
+                    StartObjectWritten = true;
+                }
+                else
+                {
+                    Debug.Assert(classType == ClassType.Enumerable);
+                    writer.WriteStartArray();
+                }
+            }
+        }
+
+        private void WriteObjectOrArrayStart(ClassType classType, ReadOnlySpan<byte> propertyName, Utf8JsonWriter writer, bool writeNull)
+        {
+            if (writeNull)
+            {
+                writer.WriteNull(propertyName);
+            }
+            else if (classType == ClassType.Object || classType == ClassType.Dictionary)
+            {
+                writer.WriteStartObject(propertyName);
+                StartObjectWritten = true;
+            }
+            else
+            {
+                Debug.Assert(classType == ClassType.Enumerable);
+                writer.WriteStartArray(propertyName);
+            }
+        }
+
+        public void Reset()
         {
             CurrentValue = null;
+            Enumerator = null;
+            KeyName = null;
             JsonClassInfo = null;
+            JsonPropertyInfo = null;
+            PropertyIndex = 0;
+            PopStackOnEndObject = false;
+            PopStackOnEnd = false;
             StartObjectWritten = false;
-            EndObject();
-            EndArray();
         }
 
-        internal void EndObject()
+        public void EndObject()
         {
             PropertyIndex = 0;
             PopStackOnEndObject = false;
-            EndProperty();
+            JsonPropertyInfo = null;
         }
 
-        internal void EndDictionary()
+        public void EndDictionary()
         {
             Enumerator = null;
-            EndProperty();
+            PopStackOnEnd = false;
         }
 
-        internal void EndArray()
+        public void EndArray()
         {
             Enumerator = null;
-            PopStackOnEndArray = false;
-            EndProperty();
-        }
-
-        internal void EndProperty()
-        {
+            PopStackOnEnd = false;
             JsonPropertyInfo = null;
         }
 
-        internal void NextProperty()
+        public void NextProperty()
         {
             JsonPropertyInfo = null;
             PropertyIndex++;
index 13c7a09..92fdeaa 100644 (file)
@@ -10,50 +10,221 @@ namespace System.Text.Json.Serialization.Tests
     public static partial class DictionaryTests
     {
         [Fact]
-        public static void DirectReturn()
+        public static void DictionaryOfString()
         {
+            const string JsonString = @"{""Hello"":""World"",""Hello2"":""World2""}";
+
             {
-                Dictionary<string, string> obj = JsonSerializer.Parse<Dictionary<string, string>>(@"{""Hello"":""World"", ""Hello2"":""World2""}");
+                Dictionary<string, string> obj = JsonSerializer.Parse<Dictionary<string, string>>(JsonString);
                 Assert.Equal("World", obj["Hello"]);
                 Assert.Equal("World2", obj["Hello2"]);
 
                 string json = JsonSerializer.ToString(obj);
-                Assert.Equal(@"{""Hello"":""World"",""Hello2"":""World2""}", json);
+                Assert.Equal(JsonString, json);
 
-                // Round-trip the json
-                obj = JsonSerializer.Parse<Dictionary<string, string>>(json);
-                Assert.Equal("World", obj["Hello"]);
-                Assert.Equal("World2", obj["Hello2"]);
+                json = JsonSerializer.ToString<object>(obj);
+                Assert.Equal(JsonString, json);
             }
 
             {
-                IDictionary<string, string> obj = JsonSerializer.Parse<IDictionary<string, string>>(@"{""Hello"":""World""}");
+                IDictionary<string, string> obj = JsonSerializer.Parse<IDictionary<string, string>>(JsonString);
                 Assert.Equal("World", obj["Hello"]);
+                Assert.Equal("World2", obj["Hello2"]);
 
                 string json = JsonSerializer.ToString(obj);
-                Assert.Equal(@"{""Hello"":""World""}", json);
+                Assert.Equal(JsonString, json);
 
-                obj = JsonSerializer.Parse<Dictionary<string, string>>(json);
-                Assert.Equal("World", obj["Hello"]);
+                json = JsonSerializer.ToString<object>(obj);
+                Assert.Equal(JsonString, json);
             }
 
             {
-                IReadOnlyDictionary<string, string> obj = JsonSerializer.Parse<IReadOnlyDictionary<string, string>>(@"{""Hello"":""World""}");
+                IReadOnlyDictionary<string, string> obj = JsonSerializer.Parse<IReadOnlyDictionary<string, string>>(JsonString);
                 Assert.Equal("World", obj["Hello"]);
+                Assert.Equal("World2", obj["Hello2"]);
 
                 string json = JsonSerializer.ToString(obj);
-                Assert.Equal(@"{""Hello"":""World""}", json);
+                Assert.Equal(JsonString, json);
 
-                obj = JsonSerializer.Parse<Dictionary<string, string>>(json);
-                Assert.Equal("World", obj["Hello"]);
+                json = JsonSerializer.ToString<object>(obj);
+                Assert.Equal(JsonString, json);
             }
         }
 
         [Fact]
-        public static void ThrowsOnDuplicateKeys()
+        public static void DuplicateKeysFail()
         {
             // todo: this should throw a JsonReaderException
-            Assert.Throws<ArgumentException>(() => JsonSerializer.Parse<Dictionary<string, string>>(@"{""Hello"":""World"", ""Hello"":""World""}"));
+            Assert.Throws<ArgumentException>(() => JsonSerializer.Parse<Dictionary<string, string>>(
+                @"{""Hello"":""World"", ""Hello"":""World""}"));
+        }
+
+        [Fact]
+        public static void DictionaryOfObjectFail()
+        {
+            Assert.Throws<JsonReaderException>(() => JsonSerializer.Parse<Dictionary<string, object>>(@"{""Key1"":1"));
+        }
+
+        [Fact]
+        public static void FirstGenericArgNotStringFail()
+        {
+            Assert.Throws<JsonReaderException>(() => JsonSerializer.Parse<Dictionary<int, int>>(@"{""Key1"":1}"));
+        }
+
+        [Fact]
+        public static void DictionaryOfList()
+        {
+            const string JsonString = @"{""Key1"":[1,2],""Key2"":[3,4]}";
+
+            IDictionary<string, List<int>> obj = JsonSerializer.Parse<IDictionary<string, List<int>>>(JsonString);
+
+            Assert.Equal(2, obj.Count);
+            Assert.Equal(2, obj["Key1"].Count);
+            Assert.Equal(1, obj["Key1"][0]);
+            Assert.Equal(2, obj["Key1"][1]);
+            Assert.Equal(2, obj["Key2"].Count);
+            Assert.Equal(3, obj["Key2"][0]);
+            Assert.Equal(4, obj["Key2"][1]);
+
+
+            string json = JsonSerializer.ToString(obj);
+            Assert.Equal(JsonString, json);
+        }
+
+        [Fact]
+        public static void DictionaryOfArray()
+        {
+            const string JsonString = @"{""Key1"":[1,2],""Key2"":[3,4]}";
+            Dictionary<string, int[]> obj = JsonSerializer.Parse<Dictionary<string, int[]>>(JsonString);
+
+            Assert.Equal(2, obj.Count);
+            Assert.Equal(2, obj["Key1"].Length);
+            Assert.Equal(1, obj["Key1"][0]);
+            Assert.Equal(2, obj["Key1"][1]);
+            Assert.Equal(2, obj["Key2"].Length);
+            Assert.Equal(3, obj["Key2"][0]);
+            Assert.Equal(4, obj["Key2"][1]);
+
+            string json = JsonSerializer.ToString(obj);
+            Assert.Equal(JsonString, json);
+        }
+
+        [Fact]
+        public static void ListOfDictionary()
+        {
+            const string JsonString = @"[{""Key1"":1,""Key2"":2},{""Key1"":3,""Key2"":4}]";
+            List<Dictionary<string, int>> obj = JsonSerializer.Parse<List<Dictionary<string, int>>>(JsonString);
+
+            Assert.Equal(2, obj.Count);
+            Assert.Equal(2, obj[0].Count);
+            Assert.Equal(1, obj[0]["Key1"]);
+            Assert.Equal(2, obj[0]["Key2"]);
+            Assert.Equal(2, obj[1].Count);
+            Assert.Equal(3, obj[1]["Key1"]);
+            Assert.Equal(4, obj[1]["Key2"]);
+
+            string json = JsonSerializer.ToString(obj);
+            Assert.Equal(JsonString, json);
+
+            json = JsonSerializer.ToString<object>(obj);
+            Assert.Equal(JsonString, json);
+        }
+
+        [Fact]
+        public static void ArrayOfDictionary()
+        {
+            const string JsonString = @"[{""Key1"":1,""Key2"":2},{""Key1"":3,""Key2"":4}]";
+            Dictionary<string, int>[] obj = JsonSerializer.Parse<Dictionary<string, int>[]>(JsonString);
+
+            Assert.Equal(2, obj.Length);
+            Assert.Equal(2, obj[0].Count);
+            Assert.Equal(1, obj[0]["Key1"]);
+            Assert.Equal(2, obj[0]["Key2"]);
+            Assert.Equal(2, obj[1].Count);
+            Assert.Equal(3, obj[1]["Key1"]);
+            Assert.Equal(4, obj[1]["Key2"]);
+
+            string json = JsonSerializer.ToString(obj);
+            Assert.Equal(JsonString, json);
+
+            json = JsonSerializer.ToString<object>(obj);
+            Assert.Equal(JsonString, json);
+        }
+
+        [Fact]
+        public static void DictionaryOfDictionary()
+        {
+            const string JsonString = @"{""Key1"":{""Key1a"":1,""Key1b"":2},""Key2"":{""Key2a"":3,""Key2b"":4}}";
+            Dictionary<string, Dictionary<string, int>> obj = JsonSerializer.Parse<Dictionary<string, Dictionary<string, int>>>(JsonString);
+
+            Assert.Equal(2, obj.Count);
+            Assert.Equal(2, obj["Key1"].Count);
+            Assert.Equal(1, obj["Key1"]["Key1a"]);
+            Assert.Equal(2, obj["Key1"]["Key1b"]);
+            Assert.Equal(2, obj["Key2"].Count);
+            Assert.Equal(3, obj["Key2"]["Key2a"]);
+            Assert.Equal(4, obj["Key2"]["Key2b"]);
+
+            string json = JsonSerializer.ToString(obj);
+            Assert.Equal(JsonString, json);
+
+            json = JsonSerializer.ToString<object>(obj);
+            Assert.Equal(JsonString, json);
+        }
+
+        [Fact]
+        public static void DictionaryOfClasses()
+        {
+            Dictionary<string, SimpleTestClass> obj;
+
+            {
+                string json = @"{""Key1"":" + SimpleTestClass.s_json + @",""Key2"":" + SimpleTestClass.s_json + "}";
+                obj = JsonSerializer.Parse<Dictionary<string, SimpleTestClass>>(json);
+                Assert.Equal(2, obj.Count);
+                obj["Key1"].Verify();
+                obj["Key2"].Verify();
+            }
+
+            {
+                // We can't compare against the json string above because property ordering is not deterministic (based on reflection order)
+                // so just round-trip the json and compare.
+                string json = JsonSerializer.ToString(obj);
+                obj = JsonSerializer.Parse<Dictionary<string, SimpleTestClass>>(json);
+                Assert.Equal(2, obj.Count);
+                obj["Key1"].Verify();
+                obj["Key2"].Verify();
+            }
+
+            {
+                string json = JsonSerializer.ToString<object>(obj);
+                obj = JsonSerializer.Parse<Dictionary<string, SimpleTestClass>>(json);
+                Assert.Equal(2, obj.Count);
+                obj["Key1"].Verify();
+                obj["Key2"].Verify();
+            }
+        }
+
+        [Fact]
+        public static void CamelCaseOption()
+        {
+            var options = new JsonSerializerOptions();
+            options.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
+
+            const string JsonString = @"[{""Key1"":1,""Key2"":2},{""Key1"":3,""Key2"":4}]";
+            Dictionary<string, int>[] obj = JsonSerializer.Parse<Dictionary<string, int>[]>(JsonString, options);
+
+            Assert.Equal(2, obj.Length);
+            Assert.Equal(1, obj[0]["key1"]);
+            Assert.Equal(2, obj[0]["key2"]);
+            Assert.Equal(3, obj[1]["key1"]);
+            Assert.Equal(4, obj[1]["key2"]);
+
+            const string JsonCamel = @"[{""key1"":1,""key2"":2},{""key1"":3,""key2"":4}]";
+            string jsonCamel = JsonSerializer.ToString<object>(obj);
+            Assert.Equal(JsonCamel, jsonCamel);
+
+            jsonCamel = JsonSerializer.ToString<object>(obj, options);
+            Assert.Equal(JsonCamel, jsonCamel);
         }
 
         [Fact]
index 3c9ac76..e6021dd 100644 (file)
@@ -32,7 +32,7 @@ namespace System.Text.Json.Serialization.Tests
             json = JsonSerializer.ToString(null, typeof(object));
             Assert.Equal(@"null", json);
 
-            Decimal pi = 3.1415926535897932384626433833m;
+            decimal pi = 3.1415926535897932384626433833m;
             json = JsonSerializer.ToString<object>(pi);
             Assert.Equal(@"3.1415926535897932384626433833", json);
             json = JsonSerializer.ToString(pi, typeof(object));
@@ -59,13 +59,18 @@ namespace System.Text.Json.Serialization.Tests
         {
             const string ExpectedJson = @"[1,true,{""City"":""MyCity""},null,""foo""]";
 
-            Address address = new Address();
+            var address = new Address();
             address.Initialize();
 
-            object[] array = new object[] { 1, true, address, null, "foo" };
+            var array = new object[] { 1, true, address, null, "foo" };
             string json = JsonSerializer.ToString(array);
             Assert.Equal(ExpectedJson, json);
 
+            var dictionary = new Dictionary<string, string> { { "City", "MyCity" } };
+            var arrayWithDictionary = new object[] { 1, true, dictionary, null, "foo" };
+            json = JsonSerializer.ToString(arrayWithDictionary);
+            Assert.Equal(ExpectedJson, json);
+
             json = JsonSerializer.ToString<object>(array);
             Assert.Equal(ExpectedJson, json);
 
index ac6953d..698c0e6 100644 (file)
@@ -2,6 +2,7 @@
 // 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 Xunit;
 
 namespace System.Text.Json.Serialization.Tests
@@ -42,6 +43,7 @@ namespace System.Text.Json.Serialization.Tests
         public DateTime?[] MyDateTimeArray { get; set; }
         public DateTimeOffset?[] MyDateTimeOffsetArray { get; set; }
         public SampleEnum?[] MyEnumArray { get; set; }
+        public Dictionary<string, string> MyStringToStringDict { get; set; }
     }
 
     public class SimpleTestClassWithNulls : SimpleBaseClassWithNullables, ITestClass
@@ -87,6 +89,7 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Null(MyDateTimeArray);
             Assert.Null(MyDateTimeOffsetArray);
             Assert.Null(MyEnumArray);
+            Assert.Null(MyStringToStringDict);
         }
         public static readonly string s_json =
                 @"{" +
@@ -123,7 +126,8 @@ namespace System.Text.Json.Serialization.Tests
                 @"""MyDecimalArray"" : null," +
                 @"""MyDateTimeArray"" : null," +
                 @"""MyDateTimeOffsetArray"" : null," +
-                @"""MyEnumArray"" : null" +
+                @"""MyEnumArray"" : null," +
+                @"""MyStringToStringDict"" : null" +
                 @"}";
 
         public static readonly byte[] s_data = Encoding.UTF8.GetBytes(s_json);
@@ -166,7 +170,8 @@ namespace System.Text.Json.Serialization.Tests
                 @"""MyDecimalArray"" : [3.3]," +
                 @"""MyDateTimeArray"" : [""2019-01-30T12:01:02.0000000Z""]," +
                 @"""MyDateTimeOffsetArray"" : [""2019-01-30T12:01:02.0000000+01:00""]," +
-                @"""MyEnumArray"" : [2]" +
+                @"""MyEnumArray"" : [2]," +
+                @"""MyStringToStringDict"" : {""key"" : ""value""}" +
                 @"}";
 
         public static readonly byte[] s_data = Encoding.UTF8.GetBytes(s_json);
@@ -208,6 +213,7 @@ namespace System.Text.Json.Serialization.Tests
             MyDateTimeArray = new DateTime?[] { new DateTime(2019, 1, 30, 12, 1, 2, DateTimeKind.Utc) };
             MyDateTimeOffsetArray = new DateTimeOffset?[] { new DateTimeOffset(2019, 1, 30, 12, 1, 2, new TimeSpan(1, 0, 0)) };
             MyEnumArray = new SampleEnum?[] { SampleEnum.Two };
+            MyStringToStringDict = new Dictionary<string, string> { { "key", "value" } };
         }
 
         public void Verify()
@@ -247,6 +253,7 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Equal(new DateTime(2019, 1, 30, 12, 1, 2, DateTimeKind.Utc), MyDateTimeArray[0]);
             Assert.Equal(new DateTimeOffset(2019, 1, 30, 12, 1, 2, new TimeSpan(1, 0, 0)), MyDateTimeOffsetArray[0]);
             Assert.Equal(SampleEnum.Two, MyEnumArray[0]);
+            Assert.Equal("value", MyStringToStringDict["key"]);
         }
     }
 }