Support nullable values in dictionaries (dotnet/corefx#40991)
authorLayomi Akinrinade <laakinri@microsoft.com>
Tue, 17 Sep 2019 12:38:41 +0000 (08:38 -0400)
committerGitHub <noreply@github.com>
Tue, 17 Sep 2019 12:38:41 +0000 (08:38 -0400)
* Support nullable values in dictionaries

* Address review feedback

* Address feedback

* Defer dictionary key escaping to writer

Commit migrated from https://github.com/dotnet/corefx/commit/a308e93b1f1303be7eab5ce2b8ed11e66cc8e01e

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/JsonPropertyInfoNotNullableContravariant.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs
src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.KeyPolicy.cs
src/libraries/System.Text.Json/tests/Serialization/Null.ReadTests.cs
src/libraries/System.Text.Json/tests/Serialization/Null.WriteTests.cs
src/libraries/System.Text.Json/tests/Serialization/NullableTests.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/Serialization/TestClasses.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj

index 7640201..3eb08b7 100644 (file)
@@ -102,11 +102,6 @@ namespace System.Text.Json
             return new List<TDeclaredProperty>();
         }
 
-        public override Type GetDictionaryConcreteType()
-        {
-            return typeof(Dictionary<string, TRuntimeProperty>);
-        }
-
         public override Type GetConcreteType(Type parentType)
         {
             if (JsonClassInfo.IsDeserializedByAssigningFromList(parentType))
@@ -145,28 +140,28 @@ namespace System.Text.Json
                     return instanceOfIList;
                 }
             }
-            else if (instance is ICollection<TRuntimeProperty> instanceOfICollection)
+            else if (instance is ICollection<TDeclaredProperty> instanceOfICollection)
             {
                 if (!instanceOfICollection.IsReadOnly)
                 {
-                    foreach (TRuntimeProperty item in sourceList)
+                    foreach (TDeclaredProperty item in sourceList)
                     {
                         instanceOfICollection.Add(item);
                     }
                     return instanceOfICollection;
                 }
             }
-            else if (instance is Stack<TRuntimeProperty> instanceOfStack)
+            else if (instance is Stack<TDeclaredProperty> instanceOfStack)
             {
-                foreach (TRuntimeProperty item in sourceList)
+                foreach (TDeclaredProperty item in sourceList)
                 {
                     instanceOfStack.Push(item);
                 }
                 return instanceOfStack;
             }
-            else if (instance is Queue<TRuntimeProperty> instanceOfQueue)
+            else if (instance is Queue<TDeclaredProperty> instanceOfQueue)
             {
-                foreach (TRuntimeProperty item in sourceList)
+                foreach (TDeclaredProperty item in sourceList)
                 {
                     instanceOfQueue.Enqueue(item);
                 }
@@ -206,13 +201,13 @@ namespace System.Text.Json
                     return instanceOfIDictionary;
                 }
             }
-            else if (instance is IDictionary<string, TRuntimeProperty> instanceOfGenericIDictionary)
+            else if (instance is IDictionary<string, TDeclaredProperty> instanceOfGenericIDictionary)
             {
                 if (!instanceOfGenericIDictionary.IsReadOnly)
                 {
                     foreach (DictionaryEntry entry in sourceDictionary)
                     {
-                        instanceOfGenericIDictionary.Add((string)entry.Key, (TRuntimeProperty)entry.Value);
+                        instanceOfGenericIDictionary.Add((string)entry.Key, (TDeclaredProperty)entry.Value);
                     }
                     return instanceOfGenericIDictionary;
                 }
@@ -285,9 +280,9 @@ namespace System.Text.Json
             }
         }
 
-        // Creates an IEnumerable<TRuntimePropertyType> and populates it with the items in the
+        // Creates an IEnumerable<TDeclaredPropertyType> and populates it with the items in the
         // sourceList argument then uses the delegateKey argument to identify the appropriate cached
-        // CreateRange<TRuntimePropertyType> method to create and return the desired immutable collection type.
+        // CreateRange<TDeclaredPropertyType> method to create and return the desired immutable collection type.
         public override IEnumerable CreateImmutableCollectionInstance(ref ReadStack state, Type collectionType, string delegateKey, IList sourceList, JsonSerializerOptions options)
         {
             IEnumerable collection = null;
@@ -301,9 +296,9 @@ namespace System.Text.Json
             return collection;
         }
 
-        // Creates an IEnumerable<TRuntimePropertyType> and populates it with the items in the
+        // Creates an IEnumerable<TDeclaredPropertyType> and populates it with the items in the
         // sourceList argument then uses the delegateKey argument to identify the appropriate cached
-        // CreateRange<TRuntimePropertyType> method to create and return the desired immutable collection type.
+        // CreateRange<TDeclaredPropertyType> method to create and return the desired immutable collection type.
         public override IDictionary CreateImmutableDictionaryInstance(ref ReadStack state, Type collectionType, string delegateKey, IDictionary sourceDictionary, JsonSerializerOptions options)
         {
             IDictionary collection = null;
index 39004e0..6a0538b 100644 (file)
@@ -122,5 +122,10 @@ namespace System.Text.Json
                 }
             }
         }
+
+        public override Type GetDictionaryConcreteType()
+        {
+            return typeof(Dictionary<string, TRuntimeProperty>);
+        }
     }
 }
index f1294c2..cc37701 100644 (file)
@@ -123,5 +123,10 @@ namespace System.Text.Json.Serialization
                 }
             }
         }
+
+        public override Type GetDictionaryConcreteType()
+        {
+            return typeof(Dictionary<string, TRuntimeProperty>);
+        }
     }
 }
index 43c03b3..431df44 100644 (file)
@@ -2,8 +2,10 @@
 // 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.Text.Json.Serialization;
 
 namespace System.Text.Json
 {
@@ -79,6 +81,46 @@ namespace System.Text.Json
             }
         }
 
+        protected override void OnWriteDictionary(ref WriteStackFrame current, Utf8JsonWriter writer)
+        {
+            Debug.Assert(Converter != null && current.CollectionEnumerator != null);
+
+            string key = null;
+            TProperty? value = null;
+            if (current.CollectionEnumerator is IEnumerator<KeyValuePair<string, TProperty?>> enumerator)
+            {
+                key = enumerator.Current.Key;
+                value = enumerator.Current.Value;
+            }
+            else if (current.IsIDictionaryConstructible || current.IsIDictionaryConstructibleProperty)
+            {
+                key = (string)((DictionaryEntry)current.CollectionEnumerator.Current).Key;
+                value = (TProperty?)((DictionaryEntry)current.CollectionEnumerator.Current).Value;
+            }
+
+            Debug.Assert(key != null);
+
+            if (value == null)
+            {
+                writer.WriteNull(key);
+            }
+            else
+            {
+                if (Options.DictionaryKeyPolicy != null)
+                {
+                    key = Options.DictionaryKeyPolicy.ConvertName(key);
+
+                    if (key == null)
+                    {
+                        ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(Options.DictionaryKeyPolicy.GetType());
+                    }
+                }
+
+                writer.WritePropertyName(key);
+                Converter.Write(writer, value.GetValueOrDefault(), Options);
+            }
+        }
+
         protected override void OnWriteEnumerable(ref WriteStackFrame current, Utf8JsonWriter writer)
         {
             if (Converter != null)
@@ -106,5 +148,10 @@ namespace System.Text.Json
                 }
             }
         }
+
+        public override Type GetDictionaryConcreteType()
+        {
+            return typeof(Dictionary<string, TProperty?>);
+        }
     }
 }
index bfe3ef5..067e129 100644 (file)
@@ -113,35 +113,33 @@ namespace System.Text.Json
             ref WriteStackFrame current,
             Utf8JsonWriter writer)
         {
-            if (converter == null)
-            {
-                return;
-            }
-
-            Debug.Assert(current.CollectionEnumerator != null);
+            Debug.Assert(converter != null && current.CollectionEnumerator != null);
 
             string key;
             TProperty value;
             if (current.CollectionEnumerator is IEnumerator<KeyValuePair<string, TProperty>> enumerator)
             {
-                // Avoid boxing for strongly-typed enumerators such as returned from IDictionary<string, TRuntimeProperty>
-                value = enumerator.Current.Value;
                 key = enumerator.Current.Key;
+                value = enumerator.Current.Value;
             }
             else if (current.CollectionEnumerator is IEnumerator<KeyValuePair<string, object>> polymorphicEnumerator)
             {
-                value = (TProperty)polymorphicEnumerator.Current.Value;
                 key = polymorphicEnumerator.Current.Key;
+                value = (TProperty)polymorphicEnumerator.Current.Value;
             }
             else if (current.IsIDictionaryConstructible || current.IsIDictionaryConstructibleProperty)
             {
-                value = (TProperty)((DictionaryEntry)current.CollectionEnumerator.Current).Value;
                 key = (string)((DictionaryEntry)current.CollectionEnumerator.Current).Key;
+                value = (TProperty)((DictionaryEntry)current.CollectionEnumerator.Current).Value;
             }
             else
             {
                 // Todo: support non-generic Dictionary here (IDictionaryEnumerator)
-                throw new NotSupportedException();
+                // https://github.com/dotnet/corefx/issues/41034
+                throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(
+                    current.JsonPropertyInfo.DeclaredPropertyType,
+                    current.JsonPropertyInfo.ParentClassType,
+                    current.JsonPropertyInfo.PropertyInfo);
             }
 
             if (value == null)
@@ -161,8 +159,7 @@ namespace System.Text.Json
                     }
                 }
 
-                JsonEncodedText escapedKey = JsonEncodedText.Encode(key, options.Encoder);
-                writer.WritePropertyName(escapedKey);
+                writer.WritePropertyName(key);
                 converter.Write(writer, value, options);
             }
         }
index 707eab3..4aaeb18 100644 (file)
@@ -131,6 +131,46 @@ namespace System.Text.Json.Serialization.Tests
         }
 
         [Fact]
+        public static void CustomNameSerialize_NullableValue()
+        {
+            var options = new JsonSerializerOptions
+            {
+                DictionaryKeyPolicy = new UppercaseNamingPolicy() // e.g. myint -> MYINT.
+            };
+
+            Dictionary<string, int?> obj = new Dictionary<string, int?> { { "myint1", 1 }, { "myint2", 2 } };
+
+            const string Json = @"{""myint1"":1,""myint2"":2}";
+            const string JsonCustomKey = @"{""MYINT1"":1,""MYINT2"":2}";
+
+            // Without key policy option, serialize keys as they are.
+            string json = JsonSerializer.Serialize<object>(obj);
+            Assert.Equal(Json, json);
+
+            // With key policy option, serialize keys honoring the custom key policy.
+            json = JsonSerializer.Serialize<object>(obj, options);
+            Assert.Equal(JsonCustomKey, json);
+        }
+
+        [Fact]
+        public static void NullNamePolicy_NullableValue()
+        {
+            var options = new JsonSerializerOptions
+            {
+                DictionaryKeyPolicy = new NullNamingPolicy()
+            };
+
+            // A naming policy that returns null is not allowed.
+            Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new Dictionary<string, int?> { { "onlyKey", 1 } }, options));
+
+            // We don't use policy on deserialize, so we populate dictionary.
+            Dictionary<string, int?> obj = JsonSerializer.Deserialize<Dictionary<string, int?>>(@"{""onlyKey"": 1}", options);
+
+            Assert.Equal(1, obj.Count);
+            Assert.Equal(1, obj["onlyKey"]);
+        }
+
+        [Fact]
         public static void KeyConflict_Serialize_WriteAll()
         {
             var options = new JsonSerializerOptions
index 9beff45..d0614cf 100644 (file)
@@ -71,12 +71,15 @@ namespace System.Text.Json.Serialization.Tests
             TestClassWithInitializedProperties obj = JsonSerializer.Deserialize<TestClassWithInitializedProperties>(TestClassWithInitializedProperties.s_null_json);
             Assert.Null(obj.MyString);
             Assert.Null(obj.MyInt);
+            Assert.Null(obj.MyDateTime);
             Assert.Null(obj.MyIntArray);
             Assert.Null(obj.MyIntList);
+            Assert.Null(obj.MyNullableIntList);
             Assert.Null(obj.MyObjectList[0]);
             Assert.Null(obj.MyListList[0][0]);
             Assert.Null(obj.MyDictionaryList[0]["key"]);
             Assert.Null(obj.MyStringDictionary["key"]);
+            Assert.Null(obj.MyNullableDateTimeDictionary["key"]);
             Assert.Null(obj.MyObjectDictionary["key"]);
             Assert.Null(obj.MyStringDictionaryDictionary["key"]["key"]);
             Assert.Null(obj.MyListDictionary["key"][0]);
@@ -93,13 +96,16 @@ namespace System.Text.Json.Serialization.Tests
 
             Assert.Equal("Hello", obj.MyString);
             Assert.Equal(1, obj.MyInt);
+            Assert.Equal(new DateTime(1995, 4, 16), obj.MyDateTime);
             Assert.Equal(1, obj.MyIntArray[0]);
             Assert.Equal(1, obj.MyIntList[0]);
+            Assert.Equal(1, obj.MyNullableIntList[0]);
 
             Assert.Null(obj.MyObjectList[0]);
             Assert.Null(obj.MyObjectList[0]);
             Assert.Null(obj.MyListList[0][0]);
             Assert.Null(obj.MyDictionaryList[0]["key"]);
+            Assert.Null(obj.MyNullableDateTimeDictionary["key"]);
             Assert.Null(obj.MyStringDictionary["key"]);
             Assert.Null(obj.MyObjectDictionary["key"]);
             Assert.Null(obj.MyStringDictionaryDictionary["key"]["key"]);
index 7ae2aa7..c9ed945 100644 (file)
@@ -16,12 +16,15 @@ namespace System.Text.Json.Serialization.Tests
             {
                 MyString = null,
                 MyInt = null,
+                MyDateTime = null,
                 MyIntArray = null,
                 MyIntList = null,
+                MyNullableIntList = null,
                 MyObjectList = new List<object> { null },
                 MyListList = new List<List<object>> { new List<object> { null } },
                 MyDictionaryList = new List<Dictionary<string, string>> { new Dictionary<string, string>() { ["key"] = null } },
                 MyStringDictionary = new Dictionary<string, string>() { ["key"] = null },
+                MyNullableDateTimeDictionary = new Dictionary<string, DateTime?>() { ["key"] = null },
                 MyObjectDictionary = new Dictionary<string, object>() { ["key"] = null },
                 MyStringDictionaryDictionary = new Dictionary<string, Dictionary<string, string>>() { ["key"] = null },
                 MyListDictionary = new Dictionary<string, List<object>>() { ["key"] = null },
@@ -31,12 +34,15 @@ namespace System.Text.Json.Serialization.Tests
             string json = JsonSerializer.Serialize(obj);
             Assert.Contains(@"""MyString"":null", json);
             Assert.Contains(@"""MyInt"":null", json);
+            Assert.Contains(@"""MyDateTime"":null", json);
             Assert.Contains(@"""MyIntArray"":null", json);
             Assert.Contains(@"""MyIntList"":null", json);
+            Assert.Contains(@"""MyNullableIntList"":null", json);
             Assert.Contains(@"""MyObjectList"":[null],", json);
             Assert.Contains(@"""MyListList"":[[null]],", json);
             Assert.Contains(@"""MyDictionaryList"":[{""key"":null}],", json);
             Assert.Contains(@"""MyStringDictionary"":{""key"":null},", json);
+            Assert.Contains(@"""MyNullableDateTimeDictionary"":{""key"":null},", json);
             Assert.Contains(@"""MyObjectDictionary"":{""key"":null},", json);
             Assert.Contains(@"""MyStringDictionaryDictionary"":{""key"":null},", json);
             Assert.Contains(@"""MyListDictionary"":{""key"":null},", json);
@@ -53,12 +59,15 @@ namespace System.Text.Json.Serialization.Tests
             {
                 MyString = null,
                 MyInt = null,
+                MyDateTime = null,
                 MyIntArray = null,
                 MyIntList = null,
+                MyNullableIntList = null,
                 MyObjectList = new List<object> { null },
                 MyListList = new List<List<object>> { new List<object> { null } },
                 MyDictionaryList = new List<Dictionary<string, string>> { new Dictionary<string, string>() { ["key"] = null } },
                 MyStringDictionary = new Dictionary<string, string>() { ["key"] = null },
+                MyNullableDateTimeDictionary = new Dictionary<string, DateTime?>() { ["key"] = null },
                 MyObjectDictionary = new Dictionary<string, object>() { ["key"] = null },
                 MyStringDictionaryDictionary = new Dictionary<string, Dictionary<string, string>>() { ["key"] = new Dictionary<string, string>() { ["key"] = null } },
                 MyListDictionary = new Dictionary<string, List<object>>() { ["key"] = new List<object> { null } },
@@ -71,14 +80,17 @@ namespace System.Text.Json.Serialization.Tests
             TestClassWithInitializedProperties newObj = JsonSerializer.Deserialize<TestClassWithInitializedProperties>(json);
             Assert.Equal("Hello", newObj.MyString);
             Assert.Equal(1, newObj.MyInt);
+            Assert.Equal(new DateTime(1995, 4, 16), newObj.MyDateTime);
             Assert.Equal(1, newObj.MyIntArray[0]);
             Assert.Equal(1, newObj.MyIntList[0]);
+            Assert.Equal(1, newObj.MyNullableIntList[0]);
 
             Assert.Null(newObj.MyObjectList[0]);
             Assert.Null(newObj.MyObjectList[0]);
             Assert.Null(newObj.MyListList[0][0]);
             Assert.Null(newObj.MyDictionaryList[0]["key"]);
             Assert.Null(newObj.MyStringDictionary["key"]);
+            Assert.Null(newObj.MyNullableDateTimeDictionary["key"]);
             Assert.Null(newObj.MyObjectDictionary["key"]);
             Assert.Null(newObj.MyStringDictionaryDictionary["key"]["key"]);
             Assert.Null(newObj.MyListDictionary["key"][0]);
@@ -95,14 +107,17 @@ namespace System.Text.Json.Serialization.Tests
             TestClassWithInitializedProperties nestedObj = newParentObj.MyClass;
             Assert.Equal("Hello", nestedObj.MyString);
             Assert.Equal(1, nestedObj.MyInt);
+            Assert.Equal(new DateTime(1995, 4, 16), nestedObj.MyDateTime);
             Assert.Equal(1, nestedObj.MyIntArray[0]);
             Assert.Equal(1, nestedObj.MyIntList[0]);
+            Assert.Equal(1, nestedObj.MyNullableIntList[0]);
 
             Assert.Null(nestedObj.MyObjectList[0]);
             Assert.Null(nestedObj.MyObjectList[0]);
             Assert.Null(nestedObj.MyListList[0][0]);
             Assert.Null(nestedObj.MyDictionaryList[0]["key"]);
             Assert.Null(nestedObj.MyStringDictionary["key"]);
+            Assert.Null(nestedObj.MyNullableDateTimeDictionary["key"]);
             Assert.Null(nestedObj.MyObjectDictionary["key"]);
             Assert.Null(nestedObj.MyStringDictionaryDictionary["key"]["key"]);
             Assert.Null(nestedObj.MyListDictionary["key"][0]);
diff --git a/src/libraries/System.Text.Json/tests/Serialization/NullableTests.cs b/src/libraries/System.Text.Json/tests/Serialization/NullableTests.cs
new file mode 100644 (file)
index 0000000..cfc9942
--- /dev/null
@@ -0,0 +1,398 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+    public static partial class NullableTests
+    {
+        [Fact]
+        public static void DictionaryWithNullableValue()
+        {
+            Dictionary<string, float?> dictWithFloatValue = new Dictionary<string, float?> { { "key", 42.0f } };
+            Dictionary<string, float?> dictWithFloatNull = new Dictionary<string, float?> { { "key", null } };
+            TestDictionaryWithNullableValue<Dictionary<string, float?>, Dictionary<string, Dictionary<string, float?>>, float?>(
+                dictWithFloatValue,
+                dictWithFloatNull,
+                dictOfDictWithValue: new Dictionary<string, Dictionary<string, float?>> { { "key", dictWithFloatValue } },
+                dictOfDictWithNull: new Dictionary<string, Dictionary<string, float?>> { { "key", dictWithFloatNull } },
+                42.0f);
+
+            DateTime now = DateTime.Now;
+            Dictionary<string, DateTime?> dictWithDateTimeValue = new Dictionary<string, DateTime?> { { "key", now } };
+            Dictionary<string, DateTime?> dictWithDateTimeNull = new Dictionary<string, DateTime?> { { "key", null } };
+            TestDictionaryWithNullableValue<Dictionary<string, DateTime?>, Dictionary<string, Dictionary<string, DateTime?>>, DateTime?>(
+                dictWithDateTimeValue,
+                dictWithDateTimeNull,
+                dictOfDictWithValue: new Dictionary<string, Dictionary<string, DateTime?>> { { "key", dictWithDateTimeValue } },
+                dictOfDictWithNull: new Dictionary<string, Dictionary<string, DateTime?>> { { "key", dictWithDateTimeNull } },
+                now);
+
+            MyDictionaryWrapper<float?> dictWrapperWithFloatValue = new MyDictionaryWrapper<float?>() { { "key", 42.0f } };
+            MyDictionaryWrapper<float?> dictWrapperWithFloatNull = new MyDictionaryWrapper<float?>() { { "key", null } };
+            TestDictionaryWithNullableValue<MyDictionaryWrapper<float?>, MyDictionaryWrapper<MyDictionaryWrapper<float?>>, float?>(
+                dictWrapperWithFloatValue,
+                dictWrapperWithFloatNull,
+                dictOfDictWithValue: new MyDictionaryWrapper<MyDictionaryWrapper<float?>> { { "key", dictWrapperWithFloatValue } },
+                dictOfDictWithNull: new MyDictionaryWrapper<MyDictionaryWrapper<float?>> { { "key", dictWrapperWithFloatNull } },
+                42.0f);
+
+            MyIDictionaryWrapper<float?> idictWrapperWithFloatValue = new MyIDictionaryWrapper<float?>() { { "key", 42.0f } };
+            MyIDictionaryWrapper<float?> idictWrapperWithFloatNull = new MyIDictionaryWrapper<float?>() { { "key", null } };
+            TestDictionaryWithNullableValue<MyIDictionaryWrapper<float?>, MyIDictionaryWrapper<MyIDictionaryWrapper<float?>>, float?>(
+                idictWrapperWithFloatValue,
+                idictWrapperWithFloatNull,
+                dictOfDictWithValue: new MyIDictionaryWrapper<MyIDictionaryWrapper<float?>> { { "key", idictWrapperWithFloatValue } },
+                dictOfDictWithNull: new MyIDictionaryWrapper<MyIDictionaryWrapper<float?>> { { "key", idictWrapperWithFloatNull } },
+                42.0f);
+
+            IDictionary<string, DateTime?> idictWithDateTimeValue = new Dictionary<string, DateTime?> { { "key", now } };
+            IDictionary<string, DateTime?> idictWithDateTimeNull = new Dictionary<string, DateTime?> { { "key", null } };
+            TestDictionaryWithNullableValue<IDictionary<string, DateTime?>, IDictionary<string, IDictionary<string, DateTime?>>, DateTime?>(
+                idictWithDateTimeValue,
+                idictWithDateTimeNull,
+                dictOfDictWithValue: new Dictionary<string, IDictionary<string, DateTime?>> { { "key", idictWithDateTimeValue } },
+                dictOfDictWithNull: new Dictionary<string, IDictionary<string, DateTime?>> { { "key", idictWithDateTimeNull } },
+                now);
+
+            ImmutableDictionary<string, DateTime?> immutableDictWithDateTimeValue = ImmutableDictionary.CreateRange(new Dictionary<string, DateTime?> { { "key", now } });
+            ImmutableDictionary<string, DateTime?> immutableDictWithDateTimeNull = ImmutableDictionary.CreateRange(new Dictionary<string, DateTime?> { { "key", null } });
+            TestDictionaryWithNullableValue<ImmutableDictionary<string, DateTime?>, ImmutableDictionary<string, ImmutableDictionary<string, DateTime?>>, DateTime?>(
+                immutableDictWithDateTimeValue,
+                immutableDictWithDateTimeNull,
+                dictOfDictWithValue: ImmutableDictionary.CreateRange(new Dictionary<string, ImmutableDictionary<string, DateTime?>> { { "key", immutableDictWithDateTimeValue } }),
+                dictOfDictWithNull: ImmutableDictionary.CreateRange(new Dictionary<string, ImmutableDictionary<string, DateTime?>> { { "key", immutableDictWithDateTimeNull } }),
+                now);
+        }
+
+        public class MyOverflowWrapper
+        {
+            [JsonExtensionData]
+            public Dictionary<string, JsonElement?> MyOverflow { get; set; }
+        }
+
+        public class MyMultipleOverflowWrapper
+        {
+            [JsonExtensionData]
+            public Dictionary<string, JsonElement> MyValidOverflow { get; set; }
+
+            [JsonExtensionData]
+            public Dictionary<string, JsonElement?> MyInvalidOverflow { get; set; }
+        }
+
+        public class AnotherMultipleOverflowWrapper
+        {
+            [JsonExtensionData]
+            public Dictionary<string, JsonElement?> MyInvalidOverflow { get; set; }
+
+            [JsonExtensionData]
+            public Dictionary<string, JsonElement> MyValidOverflow { get; set; }
+        }
+
+        public class AnotherOverflowWrapper
+        {
+            public MyOverflowWrapper Wrapper { get; set; }
+        }
+
+        [Fact]
+        public static void ExtensionDataWithNullableJsonElement_Throws()
+        {
+            Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<MyOverflowWrapper>(@"{""key"":""value""}"));
+            Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<AnotherOverflowWrapper>(@"{""Wrapper"": {""key"":""value""}}"));
+
+            // Having multiple extension properties is not allowed.
+            Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<MyMultipleOverflowWrapper>(@"{""key"":""value""}"));
+            Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<AnotherMultipleOverflowWrapper>(@"{""key"":""value""}"));
+        }
+
+        private static void TestDictionaryWithNullableValue<TDict, TDictOfDict, TValue>(
+            TDict dictWithValue,
+            TDict dictWithNull,
+            TDictOfDict dictOfDictWithValue,
+            TDictOfDict dictOfDictWithNull,
+            TValue value)
+        {
+            string valueSerialized = JsonSerializer.Serialize(value);
+
+            static void ValidateDict(TDict dict, TValue expectedValue)
+            {
+                IDictionary<string, TValue> genericIDict = (IDictionary<string, TValue>)dict;
+                Assert.Equal(1, genericIDict.Count);
+                Assert.Equal(expectedValue, genericIDict["key"]);
+            }
+
+            static void ValidateDictOfDict(TDictOfDict dictOfDict, TValue expectedValue)
+            {
+                IDictionary<string, TDict> genericIDict = (IDictionary<string, TDict>)dictOfDict;
+                Assert.Equal(1, genericIDict.Count);
+
+                IDictionary<string, TValue> nestedDict = (IDictionary<string, TValue>)genericIDict["key"];
+                Assert.Equal(1, nestedDict.Count);
+                Assert.Equal(expectedValue, nestedDict["key"]);
+            }
+
+            string json = JsonSerializer.Serialize(dictWithValue);
+            Assert.Equal(@"{""key"":" + valueSerialized + "}", json);
+
+            TDict parsedDictWithValue = JsonSerializer.Deserialize<TDict>(json);
+            ValidateDict(parsedDictWithValue, value);
+
+            json = JsonSerializer.Serialize(dictWithNull);
+            Assert.Equal(@"{""key"":null}", json);
+
+            TDict parsedDictWithNull = JsonSerializer.Deserialize<TDict>(json);
+            ValidateDict(parsedDictWithNull, default);
+
+            // Test nested dicts with nullable values.
+            json = JsonSerializer.Serialize(dictOfDictWithValue);
+            Assert.Equal(@"{""key"":{""key"":" + valueSerialized + "}}", json);
+
+            TDictOfDict parsedDictOfDictWithValue = JsonSerializer.Deserialize<TDictOfDict>(json);
+            ValidateDictOfDict(parsedDictOfDictWithValue, value);
+
+            json = JsonSerializer.Serialize(dictOfDictWithNull);
+            Assert.Equal(@"{""key"":{""key"":null}}", json);
+
+            TDictOfDict parsedDictOfDictWithNull = JsonSerializer.Deserialize<TDictOfDict>(json);
+            ValidateDictOfDict(parsedDictOfDictWithNull, default);
+        }
+
+        public class SimpleClassWithDictionariesWithNullableValues
+        {
+            public Dictionary<string, DateTime?> Dict { get; set; }
+            public IDictionary<string, DateTime?> IDict { get; set; }
+            public ImmutableDictionary<string, DateTime?> ImmutableDict { get; set; }
+            public ImmutableSortedDictionary<string, DateTime?> ImmutableSortedDict { get; set; }
+        }
+
+        [Fact]
+        public static void ClassWithDictionariesWithNullableValues()
+        {
+            string json =
+                @"{
+                    ""Dict"": {""key"": ""1995-04-16""},
+                    ""IDict"": {""key"": null},
+                    ""ImmutableDict"": {""key"": ""1997-03-22""},
+                    ""ImmutableSortedDict"": { ""key"": null}
+                }";
+
+            SimpleClassWithDictionariesWithNullableValues obj = JsonSerializer.Deserialize<SimpleClassWithDictionariesWithNullableValues>(json);
+            Assert.Equal(new DateTime(1995, 4, 16), obj.Dict["key"]);
+            Assert.Null(obj.IDict["key"]);
+            Assert.Equal(new DateTime(1997, 3, 22), obj.ImmutableDict["key"]);
+            Assert.Null(obj.ImmutableSortedDict["key"]);
+
+            string serialized = JsonSerializer.Serialize(obj);
+            Assert.Contains(@"""Dict"":{""key"":""1995-04-16T00:00:00""}", serialized);
+            Assert.Contains(@"""IDict"":{""key"":null}", serialized);
+            Assert.Contains(@"""ImmutableDict"":{""key"":""1997-03-22T00:00:00""}", serialized);
+            Assert.Contains(@"""ImmutableSortedDict"":{""key"":null}", serialized);
+        }
+
+        [Fact]
+        public static void EnumerableWithNullableValue()
+        {
+            IEnumerable<float?> ieWithFloatValue = new List<float?> { 42.0f };
+            IEnumerable<float?> ieWithFloatNull = new List<float?> { null };
+            TestEnumerableWithNullableValue<IEnumerable<float?>, IEnumerable<IEnumerable<float?>>, float?>(
+                ieWithFloatValue,
+                ieWithFloatNull,
+                enumerableOfEnumerableWithValue: new List<IEnumerable<float?>> { ieWithFloatValue },
+                enumerableOfEnumerableWithNull: new List<IEnumerable<float?>> { ieWithFloatNull },
+                42.0f);
+
+            DateTime now = DateTime.Now;
+            IEnumerable<DateTime?> ieWithDateTimeValue = new List<DateTime?> { now };
+            IEnumerable<DateTime?> ieWithDateTimeNull = new List<DateTime?> { null };
+            TestEnumerableWithNullableValue<IEnumerable<DateTime?>, IEnumerable<IEnumerable<DateTime?>>, DateTime?>(
+                ieWithDateTimeValue,
+                ieWithDateTimeNull,
+                enumerableOfEnumerableWithValue: new List<IEnumerable<DateTime?>> { ieWithDateTimeValue },
+                enumerableOfEnumerableWithNull: new List<IEnumerable<DateTime?>> { ieWithDateTimeNull },
+                now);
+
+            IReadOnlyList<DateTime?> irlWithDateTimeValue = new List<DateTime?> { now };
+            IReadOnlyList<DateTime?> irlWithDateTimeNull = new List<DateTime?> { null };
+            TestEnumerableWithNullableValue<IReadOnlyList<DateTime?>, IReadOnlyList<IReadOnlyList<DateTime?>>, DateTime?>(
+                irlWithDateTimeValue,
+                irlWithDateTimeNull,
+                enumerableOfEnumerableWithValue: new List<IReadOnlyList<DateTime?>> { irlWithDateTimeValue },
+                enumerableOfEnumerableWithNull: new List<IReadOnlyList<DateTime?>> { irlWithDateTimeNull },
+                now);
+
+            Stack<DateTime?> stWithDateTimeValue = new Stack<DateTime?>();
+            stWithDateTimeValue.Push(now);
+
+            Stack<DateTime?> stWithDateTimeNull = new Stack<DateTime?>();
+            stWithDateTimeNull.Push(null);
+
+            Stack<Stack<DateTime?>> enumerableOfEnumerableWithValue = new Stack<Stack<DateTime?>>();
+            enumerableOfEnumerableWithValue.Push(stWithDateTimeValue);
+
+            Stack<Stack<DateTime?>> enumerableOfEnumerableWithNull = new Stack<Stack<DateTime?>>();
+            enumerableOfEnumerableWithNull.Push(stWithDateTimeNull);
+
+            TestEnumerableWithNullableValue<Stack<DateTime?>, Stack<Stack<DateTime?>>, DateTime?>(
+                stWithDateTimeValue,
+                stWithDateTimeNull,
+                enumerableOfEnumerableWithValue,
+                enumerableOfEnumerableWithNull,
+                now);
+
+            IImmutableList<DateTime?> imlWithDateTimeValue = ImmutableList.CreateRange(new List<DateTime?> { now });
+            IImmutableList<DateTime?> imlWithDateTimeNull = ImmutableList.CreateRange(new List<DateTime?> { null });
+            TestEnumerableWithNullableValue<IImmutableList<DateTime?>, IImmutableList<IImmutableList<DateTime?>>, DateTime?>(
+                imlWithDateTimeValue,
+                imlWithDateTimeNull,
+                enumerableOfEnumerableWithValue: ImmutableList.CreateRange(new List<IImmutableList<DateTime?>> { imlWithDateTimeValue }),
+                enumerableOfEnumerableWithNull: ImmutableList.CreateRange(new List<IImmutableList<DateTime?>> { imlWithDateTimeNull }),
+                now);
+        }
+
+        private static void TestEnumerableWithNullableValue<TEnumerable, TEnumerableOfEnumerable, TValue>(
+            TEnumerable enumerableWithValue,
+            TEnumerable enumerableWithNull,
+            TEnumerableOfEnumerable enumerableOfEnumerableWithValue,
+            TEnumerableOfEnumerable enumerableOfEnumerableWithNull,
+            TValue value)
+        {
+            string valueSerialized = JsonSerializer.Serialize(value);
+
+            static void ValidateEnumerable(TEnumerable enumerable, TValue expectedValue)
+            {
+                IEnumerable<TValue> ienumerable = (IEnumerable<TValue>)enumerable;
+                int count = 0;
+                foreach (TValue val in ienumerable)
+                {
+                    Assert.Equal(expectedValue, val);
+                    count += 1;
+                }
+                Assert.Equal(1, count);
+            }
+
+            static void ValidateEnumerableOfEnumerable(TEnumerableOfEnumerable dictOfDict, TValue expectedValue)
+            {
+                IEnumerable<TEnumerable> ienumerable = (IEnumerable<TEnumerable>)dictOfDict;
+                int ienumerableCount = 0;
+                int nestedIEnumerableCount = 0;
+
+                foreach (IEnumerable<TValue> nestedIEnumerable in ienumerable)
+                {
+                    foreach (TValue val in nestedIEnumerable)
+                    {
+                        Assert.Equal(expectedValue, val);
+                        nestedIEnumerableCount += 1;
+                    }
+                    ienumerableCount += 1;
+                }
+                Assert.Equal(1, ienumerableCount);
+                Assert.Equal(1, nestedIEnumerableCount);
+            }
+
+            string json = JsonSerializer.Serialize(enumerableWithValue);
+            Assert.Equal($"[{valueSerialized}]", json);
+
+            TEnumerable parsedEnumerableWithValue = JsonSerializer.Deserialize<TEnumerable>(json);
+            ValidateEnumerable(parsedEnumerableWithValue, value);
+
+            json = JsonSerializer.Serialize(enumerableWithNull);
+            Assert.Equal("[null]", json);
+
+            TEnumerable parsedEnumerableWithNull = JsonSerializer.Deserialize<TEnumerable>(json);
+            ValidateEnumerable(parsedEnumerableWithNull, default);
+
+            // Test nested dicts with nullable values.
+            json = JsonSerializer.Serialize(enumerableOfEnumerableWithValue);
+            Assert.Equal($"[[{valueSerialized}]]", json);
+
+            TEnumerableOfEnumerable parsedEnumerableOfEnumerableWithValue = JsonSerializer.Deserialize<TEnumerableOfEnumerable>(json);
+            ValidateEnumerableOfEnumerable(parsedEnumerableOfEnumerableWithValue, value);
+
+            json = JsonSerializer.Serialize(enumerableOfEnumerableWithNull);
+            Assert.Equal("[[null]]", json);
+
+            TEnumerableOfEnumerable parsedEnumerableOfEnumerableWithNull = JsonSerializer.Deserialize<TEnumerableOfEnumerable>(json);
+            ValidateEnumerableOfEnumerable(parsedEnumerableOfEnumerableWithNull, default);
+        }
+
+        public class MyDictionaryWrapper<TValue> : Dictionary<string, TValue> { }
+
+        public class MyIDictionaryWrapper<TValue> : IDictionary<string, TValue>
+        {
+            Dictionary<string, TValue> dict = new Dictionary<string, TValue>();
+
+            // Derived types need default constructors to be supported.
+            public MyIDictionaryWrapper() { }
+
+            public TValue this[string key] { get => ((IDictionary<string, TValue>)dict)[key]; set => ((IDictionary<string, TValue>)dict)[key] = value; }
+
+            public ICollection<string> Keys => ((IDictionary<string, TValue>)dict).Keys;
+
+            public ICollection<TValue> Values => ((IDictionary<string, TValue>)dict).Values;
+
+            public int Count => ((IDictionary<string, TValue>)dict).Count;
+
+            public bool IsReadOnly => ((IDictionary<string, TValue>)dict).IsReadOnly;
+
+            public void Add(string key, TValue value)
+            {
+                ((IDictionary<string, TValue>)dict).Add(key, value);
+            }
+
+            public void Add(KeyValuePair<string, TValue> item)
+            {
+                ((IDictionary<string, TValue>)dict).Add(item);
+            }
+
+            public void Clear()
+            {
+                ((IDictionary<string, TValue>)dict).Clear();
+            }
+
+            public bool Contains(KeyValuePair<string, TValue> item)
+            {
+                return ((IDictionary<string, TValue>)dict).Contains(item);
+            }
+
+            public bool ContainsKey(string key)
+            {
+                return ((IDictionary<string, TValue>)dict).ContainsKey(key);
+            }
+
+            public void CopyTo(KeyValuePair<string, TValue>[] array, int arrayIndex)
+            {
+                ((IDictionary<string, TValue>)dict).CopyTo(array, arrayIndex);
+            }
+
+            public IEnumerator<KeyValuePair<string, TValue>> GetEnumerator()
+            {
+                return ((IDictionary<string, TValue>)dict).GetEnumerator();
+            }
+
+            public bool Remove(string key)
+            {
+                return ((IDictionary<string, TValue>)dict).Remove(key);
+            }
+
+            public bool Remove(KeyValuePair<string, TValue> item)
+            {
+                return ((IDictionary<string, TValue>)dict).Remove(item);
+            }
+
+            public bool TryGetValue(string key, out TValue value)
+            {
+                return ((IDictionary<string, TValue>)dict).TryGetValue(key, out value);
+            }
+
+            IEnumerator IEnumerable.GetEnumerator()
+            {
+                return ((IDictionary<string, TValue>)dict).GetEnumerator();
+            }
+        }
+    }
+}
index c188565..7d64e12 100644 (file)
@@ -145,14 +145,17 @@ namespace System.Text.Json.Serialization.Tests
     {
         public string MyString { get; set; } = "Hello";
         public int? MyInt { get; set; } = 1;
+        public DateTime? MyDateTime { get; set; } = new DateTime(1995, 4, 16);
         public int[] MyIntArray { get; set; } = new int[] { 1 };
         public List<int> MyIntList { get; set; } = new List<int> { 1 };
+        public List<int?> MyNullableIntList { get; set; } = new List<int?> { 1 };
         public List<object> MyObjectList { get; set; } = new List<object> { 1 };
         public List<List<object>> MyListList { get; set; } = new List<List<object>> { new List<object> { 1 } };
         public List<Dictionary<string, string>> MyDictionaryList { get; set; } = new List<Dictionary<string, string>> {
             new Dictionary<string, string> { ["key"] = "value" }
         };
         public Dictionary<string, string> MyStringDictionary { get; set; } = new Dictionary<string, string> { ["key"] = "value" };
+        public Dictionary<string, DateTime?> MyNullableDateTimeDictionary { get; set; } = new Dictionary<string, DateTime?> { ["key"] = new DateTime(1995, 04, 16) };
         public Dictionary<string, object> MyObjectDictionary { get; set; } = new Dictionary<string, object> { ["key"] = "value" };
         public Dictionary<string, Dictionary<string, string>> MyStringDictionaryDictionary { get; set; } = new Dictionary<string, Dictionary<string, string>>
         {
@@ -176,12 +179,15 @@ namespace System.Text.Json.Serialization.Tests
                 @"{" +
                     @"""MyString"" : null," +
                     @"""MyInt"" : null," +
+                    @"""MyDateTime"" : null," +
                     @"""MyIntArray"" : null," +
                     @"""MyIntList"" : null," +
+                    @"""MyNullableIntList"" : null," +
                     @"""MyObjectList"" : [null]," +
                     @"""MyListList"" : [[null]]," +
                     @"""MyDictionaryList"" : [{""key"" : null}]," +
                     @"""MyStringDictionary"" : {""key"" : null}," +
+                    @"""MyNullableDateTimeDictionary"" : {""key"" : null}," +
                     @"""MyObjectDictionary"" : {""key"" : null}," +
                     @"""MyStringDictionaryDictionary"" : {""key"" : {""key"" : null}}," +
                     @"""MyListDictionary"" : {""key"" : [null]}," +
index b1d02c5..a2b5510 100644 (file)
@@ -61,6 +61,7 @@
     <Compile Include="Serialization\JsonElementTests.cs" />
     <Compile Include="Serialization\Null.ReadTests.cs" />
     <Compile Include="Serialization\Null.WriteTests.cs" />
+    <Compile Include="Serialization\NullableTests.cs" />
     <Compile Include="Serialization\Object.ReadTests.cs" />
     <Compile Include="Serialization\Object.WriteTests.cs" />
     <Compile Include="Serialization\OptionsTests.cs" />