Fix bugs in null serialization (dotnet/corefx#40077)
authorLayomi Akinrinade <laakinri@microsoft.com>
Sat, 10 Aug 2019 13:35:20 +0000 (09:35 -0400)
committerGitHub <noreply@github.com>
Sat, 10 Aug 2019 13:35:20 +0000 (09:35 -0400)
* Support null serialization for objects and dictionaries as values in Dictionary and KeyValuePair

* Address reviw comments

* Fix nested enumerable null serialization and add dictionaries to null test suite

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

12 files changed:
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterKeyValuePair.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.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/tests/Serialization/DictionaryTests.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/TestClasses.cs
src/libraries/System.Text.Json/tests/Serialization/Value.ReadTests.GenericCollections.cs
src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.GenericCollections.cs
src/libraries/System.Text.Json/tests/Utf8JsonReaderTests.TryGet.Date.cs

index 2c777b2..e516fa1 100644 (file)
@@ -37,12 +37,12 @@ namespace System.Text.Json.Serialization.Converters
             string propertyName = reader.GetString();
             if (propertyName == KeyName)
             {
-                k = ReadProperty<TKey>(ref reader, options);
+                k = ReadProperty<TKey>(ref reader, typeToConvert, options);
                 keySet = true;
             }
             else if (propertyName == ValueName)
             {
-                v = ReadProperty<TValue>(ref reader, options);
+                v = ReadProperty<TValue>(ref reader, typeToConvert, options);
                 valueSet = true;
             }
             else
@@ -60,12 +60,12 @@ namespace System.Text.Json.Serialization.Converters
             propertyName = reader.GetString();
             if (propertyName == ValueName)
             {
-                v = ReadProperty<TValue>(ref reader, options);
+                v = ReadProperty<TValue>(ref reader, typeToConvert, options);
                 valueSet = true;
             }
             else if (propertyName == KeyName)
             {
-                k = ReadProperty<TKey>(ref reader, options);
+                k = ReadProperty<TKey>(ref reader, typeToConvert, options);
                 keySet = true;
             }
             else
@@ -88,21 +88,20 @@ namespace System.Text.Json.Serialization.Converters
             return new KeyValuePair<TKey, TValue>(k, v);
         }
 
-        private T ReadProperty<T>(ref Utf8JsonReader reader, JsonSerializerOptions options)
+        private T ReadProperty<T>(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
         {
             T k;
-            Type typeToConvert = typeof(T);
 
-            // Attempt to use existing converter first before re-entering through JsonSerializer.Read().
-            JsonConverter<T> keyConverter = options.GetConverter(typeToConvert) as JsonConverter<T>;
-            if (keyConverter == null)
+            // Attempt to use existing converter first before re-entering through JsonSerializer.Deserialize().
+            // The default converter for objects does not parse null objects as null, so it is not used here.
+            if (typeToConvert != typeof(object) && (options.GetConverter(typeToConvert) is JsonConverter<T> keyConverter))
             {
-                k = JsonSerializer.Deserialize<T>(ref reader, options);
+                reader.Read();
+                k = keyConverter.Read(ref reader, typeToConvert, options);
             }
             else
             {
-                reader.Read();
-                k = keyConverter.Read(ref reader, typeToConvert, options);
+                k = JsonSerializer.Deserialize<T>(ref reader, options);
             }
 
             return k;
@@ -110,17 +109,19 @@ namespace System.Text.Json.Serialization.Converters
 
         private void WriteProperty<T>(Utf8JsonWriter writer, T value, JsonEncodedText name, JsonSerializerOptions options)
         {
+            Type typeToConvert = typeof(T);
+
             writer.WritePropertyName(name);
 
-            // Attempt to use existing converter first before re-entering through JsonSerializer.Write().
-            JsonConverter<T> keyConverter = options.GetConverter(typeof(T)) as JsonConverter<T>;
-            if (keyConverter == null)
+            // Attempt to use existing converter first before re-entering through JsonSerializer.Serialize().
+            // The default converter for object does not support writing.
+            if (typeToConvert != typeof(object) && (options.GetConverter(typeToConvert) is JsonConverter<T> keyConverter))
             {
-                JsonSerializer.Serialize<T>(writer, value, options);
+                keyConverter.Write(writer, value, options);
             }
             else
             {
-                keyConverter.Write(writer, value, options);
+                JsonSerializer.Serialize<T>(writer, value, options);
             }
         }
 
index 0280fbe..27e8339 100644 (file)
@@ -5,7 +5,6 @@
 using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
-using System.Linq;
 using System.Reflection;
 using System.Runtime.InteropServices;
 using System.Text.Json.Serialization;
index 266a6cc..75e9e64 100644 (file)
@@ -25,10 +25,17 @@ namespace System.Text.Json
                 enumerable = (IEnumerable)jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue);
                 if (enumerable == null)
                 {
-                    if (!state.Current.JsonPropertyInfo.IgnoreNullValues)
+                    // If applicable, we only want to ignore object properties.
+                    if (state.Current.JsonClassInfo.ClassType != ClassType.Object ||
+                        !state.Current.JsonPropertyInfo.IgnoreNullValues)
                     {
                         // Write a null object or enumerable.
-                        state.Current.WriteObjectOrArrayStart(ClassType.Enumerable, writer, writeNull: true);
+                        state.Current.WriteObjectOrArrayStart(ClassType.Dictionary, writer, writeNull: true);
+                    }
+
+                    if (state.Current.PopStackOnEndCollection)
+                    {
+                        state.Pop();
                     }
 
                     return true;
index dfcfe98..bb659bd 100644 (file)
@@ -23,12 +23,19 @@ namespace System.Text.Json
 
                 if (enumerable == null)
                 {
-                    if (!state.Current.JsonPropertyInfo.IgnoreNullValues)
+                    // If applicable, we only want to ignore object properties.
+                    if (state.Current.JsonClassInfo.ClassType != ClassType.Object ||
+                        !state.Current.JsonPropertyInfo.IgnoreNullValues)
                     {
                         // Write a null object or enumerable.
                         state.Current.WriteObjectOrArrayStart(ClassType.Enumerable, writer, writeNull: true);
                     }
 
+                    if (state.Current.PopStackOnEndCollection)
+                    {
+                        state.Pop();
+                    }
+
                     return true;
                 }
 
index f8c87bc..2ca7c97 100644 (file)
@@ -17,6 +17,14 @@ namespace System.Text.Json
             // Write the start.
             if (!state.Current.StartObjectWritten)
             {
+                // If true, we are writing a root object or a value that doesn't belong
+                // to an object e.g. a dictionary value.
+                if (state.Current.CurrentValue == null)
+                {
+                    state.Current.WriteObjectOrArrayStart(ClassType.Object, writer, writeNull: true);
+                    return WriteEndObject(ref state);
+                }
+
                 state.Current.WriteObjectOrArrayStart(ClassType.Object, writer);
                 state.Current.PropertyEnumerator = state.Current.JsonClassInfo.PropertyCache.GetEnumerator();
                 state.Current.PropertyEnumeratorActive = true;
@@ -49,7 +57,11 @@ namespace System.Text.Json
             }
 
             writer.WriteEndObject();
+            return WriteEndObject(ref state);
+        }
 
+        private static bool WriteEndObject(ref WriteStack state)
+        {
             if (state.Current.PopStackOnEndObject)
             {
                 state.Pop();
@@ -122,7 +134,8 @@ namespace System.Text.Json
                 return endOfEnumerable;
             }
 
-            // A property that returns an immutable dictionary keeps the same stack frame.
+            // A property that returns a type that is deserialized by passing an
+            // IDictionary to its constructor keeps the same stack frame.
             if (jsonPropertyInfo.ClassType == ClassType.IDictionaryConstructible)
             {
                 state.Current.IsIDictionaryConstructibleProperty = true;
index 79fd270..be7adef 100644 (file)
@@ -1158,6 +1158,36 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Null(dictionaryLast.Dict);
         }
 
+        [Fact]
+        public static void NullDictionaryValuesShouldDeserializeAsNull()
+        {
+            const string json =
+                    @"{" +
+                        @"""StringVals"":{" +
+                            @"""key"":null" +
+                        @"}," +
+                        @"""ObjectVals"":{" +
+                            @"""key"":null" +
+                        @"}," +
+                        @"""StringDictVals"":{" +
+                            @"""key"":null" +
+                        @"}," +
+                        @"""ObjectDictVals"":{" +
+                            @"""key"":null" +
+                        @"}," +
+                        @"""ClassVals"":{" +
+                            @"""key"":null" +
+                        @"}" +
+                    @"}";
+
+            SimpleClassWithDictionaries obj = JsonSerializer.Deserialize<SimpleClassWithDictionaries>(json);
+            Assert.Null(obj.StringVals["key"]);
+            Assert.Null(obj.ObjectVals["key"]);
+            Assert.Null(obj.StringDictVals["key"]);
+            Assert.Null(obj.ObjectDictVals["key"]);
+            Assert.Null(obj.ClassVals["key"]);
+        }
+
         public class ClassWithNotSupportedDictionary
         {
             public Dictionary<int, int> MyDictionary { get; set; }
@@ -1212,5 +1242,14 @@ namespace System.Text.Json.Serialization.Tests
             public Dictionary<string, string> Dict { get; set; }
             public string Test { get; set; }
         }
+
+        public class SimpleClassWithDictionaries
+        {
+            public Dictionary<string, string> StringVals { get; set; }
+            public Dictionary<string, object> ObjectVals { get; set; }
+            public Dictionary<string, Dictionary<string, string>> StringDictVals { get; set; }
+            public Dictionary<string, Dictionary<string, object>> ObjectDictVals { get; set; }
+            public Dictionary<string, SimpleClassWithDictionaries> ClassVals { get; set; }
+        }
     }
 }
index a92a192..9beff45 100644 (file)
@@ -73,6 +73,14 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Null(obj.MyInt);
             Assert.Null(obj.MyIntArray);
             Assert.Null(obj.MyIntList);
+            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.MyObjectDictionary["key"]);
+            Assert.Null(obj.MyStringDictionaryDictionary["key"]["key"]);
+            Assert.Null(obj.MyListDictionary["key"][0]);
+            Assert.Null(obj.MyObjectDictionaryDictionary["key"]["key"]);
         }
 
         [Fact]
@@ -82,10 +90,21 @@ namespace System.Text.Json.Serialization.Tests
             options.IgnoreNullValues = true;
 
             TestClassWithInitializedProperties obj = JsonSerializer.Deserialize<TestClassWithInitializedProperties>(TestClassWithInitializedProperties.s_null_json, options);
+
             Assert.Equal("Hello", obj.MyString);
             Assert.Equal(1, obj.MyInt);
             Assert.Equal(1, obj.MyIntArray[0]);
             Assert.Equal(1, obj.MyIntList[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.MyStringDictionary["key"]);
+            Assert.Null(obj.MyObjectDictionary["key"]);
+            Assert.Null(obj.MyStringDictionaryDictionary["key"]["key"]);
+            Assert.Null(obj.MyListDictionary["key"][0]);
+            Assert.Null(obj.MyObjectDictionaryDictionary["key"]["key"]);
         }
 
         [Fact]
index ab3988d..1544465 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
@@ -11,17 +12,35 @@ namespace System.Text.Json.Serialization.Tests
         [Fact]
         public static void DefaultIgnoreNullValuesOnWrite()
         {
-            var obj = new TestClassWithInitializedProperties();
-            obj.MyString = null;
-            obj.MyInt = null;
-            obj.MyIntArray = null;
-            obj.MyIntList = null;
+            var obj = new TestClassWithInitializedProperties
+            {
+                MyString = null,
+                MyInt = null,
+                MyIntArray = null,
+                MyIntList = 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 },
+                MyObjectDictionary = new Dictionary<string, object>() { ["key"] = null },
+                MyStringDictionaryDictionary = new Dictionary<string, Dictionary<string, string>>() { ["key"] = null },
+                MyListDictionary = new Dictionary<string, List<object>>() { ["key"] = null },
+                MyObjectDictionaryDictionary = new Dictionary<string, Dictionary<string, object>>() { ["key"] = null }
+            };
 
             string json = JsonSerializer.Serialize(obj);
             Assert.Contains(@"""MyString"":null", json);
             Assert.Contains(@"""MyInt"":null", json);
             Assert.Contains(@"""MyIntArray"":null", json);
             Assert.Contains(@"""MyIntList"":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(@"""MyObjectDictionary"":{""key"":null},", json);
+            Assert.Contains(@"""MyStringDictionaryDictionary"":{""key"":null},", json);
+            Assert.Contains(@"""MyListDictionary"":{""key"":null},", json);
+            Assert.Contains(@"""MyObjectDictionaryDictionary"":{""key"":null}", json);
         }
 
         [Fact]
@@ -30,14 +49,57 @@ namespace System.Text.Json.Serialization.Tests
             JsonSerializerOptions options = new JsonSerializerOptions();
             options.IgnoreNullValues = true;
 
-            var obj = new TestClassWithInitializedProperties();
-            obj.MyString = null;
-            obj.MyInt = null;
-            obj.MyIntArray = null;
-            obj.MyIntList = null;
+            var obj = new TestClassWithInitializedProperties
+            {
+                MyString = null,
+                MyInt = null,
+                MyIntArray = null,
+                MyIntList = 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 },
+                MyObjectDictionary = new Dictionary<string, object>() { ["key"] = null },
+                MyStringDictionaryDictionary = new Dictionary<string, Dictionary<string, string>>() { ["key"] = null },
+                MyListDictionary = new Dictionary<string, List<object>>() { ["key"] = null },
+                MyObjectDictionaryDictionary = new Dictionary<string, Dictionary<string, object>>() { ["key"] = null }
+            };
+
+            string expectedJson =
+                    @"{" +
+                        @"""MyObjectList"":[null]," +
+                        @"""MyListList"":[[null]]," +
+                        @"""MyDictionaryList"":[" +
+                            @"{" +
+                                @"""key"":null" +
+                            @"}" +
+                        @"]," +
+                        @"""MyStringDictionary"":{" +
+                            @"""key"":null" +
+                        @"}," +
+                        @"""MyObjectDictionary"":{" +
+                            @"""key"":null" +
+                        @"}," +
+                        @"""MyStringDictionaryDictionary"":{" +
+                            @"""key"":null" +
+                        @"}," +
+                        @"""MyListDictionary"":{" +
+                            @"""key"":null" +
+                        @"}," +
+                        @"""MyObjectDictionaryDictionary"":{" +
+                            @"""key"":null" +
+                        @"}" +
+                    @"}";
 
-            string json = JsonSerializer.Serialize(obj, options);
-            Assert.Equal(@"{}", json);
+            Assert.Equal(expectedJson, JsonSerializer.Serialize(obj, options));
+
+            var parentObj = new WrapperForTestClassWithInitializedProperties
+            {
+                MyClass = obj
+            };
+
+            expectedJson = @"{""MyClass"":" + expectedJson + "}";
+            Assert.Equal(expectedJson, JsonSerializer.Serialize(parentObj, options));
         }
 
         [Fact]
@@ -106,5 +168,62 @@ namespace System.Text.Json.Serialization.Tests
                 Assert.Equal("null", output);
             }
         }
+
+        class WrapperForTestClassWithInitializedProperties
+        {
+            public TestClassWithInitializedProperties MyClass { get; set; }
+        }
+
+        [Fact]
+        public static void SerializeDictionaryWithNullValues()
+        {
+            Dictionary<string, string> StringVals = new Dictionary<string, string>()
+            {
+                ["key"] = null,
+            };
+            Assert.Equal(@"{""key"":null}", JsonSerializer.Serialize(StringVals));
+
+            Dictionary<string, object> ObjVals = new Dictionary<string, object>()
+            {
+                ["key"] = null,
+            };
+            Assert.Equal(@"{""key"":null}", JsonSerializer.Serialize(ObjVals));
+
+            Dictionary<string, Dictionary<string, string>> StringDictVals = new Dictionary<string, Dictionary<string, string>>()
+            {
+                ["key"] = null,
+            };
+            Assert.Equal(@"{""key"":null}", JsonSerializer.Serialize(StringDictVals));
+
+            Dictionary<string, Dictionary<string, object>> ObjectDictVals = new Dictionary<string, Dictionary<string, object>>()
+            {
+                ["key"] = null,
+            };
+            Assert.Equal(@"{""key"":null}", JsonSerializer.Serialize(ObjectDictVals));
+        }
+
+        [Fact]
+        public static void DeserializeDictionaryWithNullValues()
+        {
+            {
+                Dictionary<string, string> dict = JsonSerializer.Deserialize<Dictionary<string, string>>(@"{""key"":null}");
+                Assert.Null(dict["key"]);
+            }
+
+            {
+                Dictionary<string, object> dict = JsonSerializer.Deserialize<Dictionary<string, object>>(@"{""key"":null}");
+                Assert.Null(dict["key"]);
+            }
+
+            {
+                Dictionary<string, Dictionary<string, string>> dict = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(@"{""key"":null}");
+                Assert.Null(dict["key"]);
+            }
+
+            {
+                Dictionary<string, Dictionary<string, object>> dict = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, object>>>(@"{""key"":null}");
+                Assert.Null(dict["key"]);
+            }
+        }
     }
 }
index b063916..e969dc0 100644 (file)
@@ -147,12 +147,45 @@ namespace System.Text.Json.Serialization.Tests
         public int? MyInt { get; set; } = 1;
         public int[] MyIntArray { get; set; } = new int[] { 1 };
         public List<int> MyIntList { 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, 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>>
+        {
+            ["key"] = new Dictionary<string, string>
+            {
+                ["key"] = "value"
+            }
+        };
+        public Dictionary<string, List<object>> MyListDictionary { get; set; } = new Dictionary<string, List<object>> {
+            ["key"] = new List<object> { "value" }
+        };
+        public Dictionary<string, Dictionary<string, object>> MyObjectDictionaryDictionary { get; set; } = new Dictionary<string, Dictionary<string, object>>
+        {
+            ["key"] = new Dictionary<string, object>
+            {
+                ["key"] = "value"
+            }
+        };
+
         public static readonly string s_null_json =
                 @"{" +
-                @"""MyString"" : null," +
-                @"""MyInt"" : null," +
-                @"""MyIntArray"" : null," +
-                @"""MyIntList"" : null" +
+                    @"""MyString"" : null," +
+                    @"""MyInt"" : null," +
+                    @"""MyIntArray"" : null," +
+                    @"""MyIntList"" : null," +
+                    @"""MyObjectList"" : [null]," +
+                    @"""MyListList"" : [[null]]," +
+                    @"""MyDictionaryList"" : [{""key"" : null}]," +
+                    @"""MyStringDictionary"" : {""key"" : null}," +
+                    @"""MyObjectDictionary"" : {""key"" : null}," +
+                    @"""MyStringDictionaryDictionary"" : {""key"" : {""key"" : null}}," +
+                    @"""MyListDictionary"" : {""key"" : [null]}," +
+                    @"""MyObjectDictionaryDictionary"" : {""key"" : {""key"" : null}}" +
                 @"}";
 
         public static readonly byte[] s_data = Encoding.UTF8.GetBytes(s_null_json);
index bbdcab8..f28a7b6 100644 (file)
@@ -1023,6 +1023,108 @@ namespace System.Text.Json.Serialization.Tests
         }
 
         [Fact]
+        public static void ReadKeyValuePairWithNullValues()
+        {
+            {
+                KeyValuePair<string, string> kvp = JsonSerializer.Deserialize<KeyValuePair<string, string>>(@"{""Key"":""key"",""Value"":null}");
+                Assert.Equal("key", kvp.Key);
+                Assert.Null(kvp.Value);
+            }
+
+            {
+                KeyValuePair<string, object> kvp = JsonSerializer.Deserialize<KeyValuePair<string, object>>(@"{""Key"":""key"",""Value"":null}");
+                Assert.Equal("key", kvp.Key);
+                Assert.Null(kvp.Value);
+            }
+
+            {
+                KeyValuePair<string, SimpleClassWithKeyValuePairs> kvp = JsonSerializer.Deserialize<KeyValuePair<string, SimpleClassWithKeyValuePairs>>(@"{""Key"":""key"",""Value"":null}");
+                Assert.Equal("key", kvp.Key);
+                Assert.Null(kvp.Value);
+            }
+
+            {
+                KeyValuePair<string, KeyValuePair<string, string>> kvp = JsonSerializer.Deserialize<KeyValuePair<string, KeyValuePair<string, string>>>(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}");
+                Assert.Equal("key", kvp.Key);
+                Assert.Equal("key", kvp.Value.Key);
+                Assert.Null(kvp.Value.Value);
+            }
+
+            {
+                KeyValuePair<string, KeyValuePair<string, object>> kvp = JsonSerializer.Deserialize<KeyValuePair<string, KeyValuePair<string, object>>>(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}");
+                Assert.Equal("key", kvp.Key);
+                Assert.Equal("key", kvp.Value.Key);
+                Assert.Null(kvp.Value.Value);
+            }
+
+            {
+                KeyValuePair<string, KeyValuePair<string, SimpleClassWithKeyValuePairs>> kvp = JsonSerializer.Deserialize<KeyValuePair<string, KeyValuePair<string, SimpleClassWithKeyValuePairs>>>(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}");
+                Assert.Equal("key", kvp.Key);
+                Assert.Equal("key", kvp.Value.Key);
+                Assert.Null(kvp.Value.Value);
+            }
+        }
+
+        [Fact]
+        public static void ReadClassWithNullKeyValuePairValues()
+        {
+            string json =
+                    @"{" +
+                        @"""KvpWStrVal"":{" +
+                            @"""Key"":""key""," +
+                            @"""Value"":null" +
+                        @"}," +
+                        @"""KvpWObjVal"":{" +
+                            @"""Key"":""key""," +
+                            @"""Value"":null" +
+                        @"}," +
+                        @"""KvpWClassVal"":{" +
+                            @"""Key"":""key""," +
+                            @"""Value"":null" +
+                        @"}," +
+                        @"""KvpWStrKvpVal"":{" +
+                            @"""Key"":""key""," +
+                            @"""Value"":{" +
+                                @"""Key"":""key""," +
+                                @"""Value"":null" +
+                            @"}" +
+                        @"}," +
+                        @"""KvpWObjKvpVal"":{" +
+                            @"""Key"":""key""," +
+                            @"""Value"":{" +
+                                @"""Key"":""key""," +
+                                @"""Value"":null" +
+                            @"}" +
+                        @"}," +
+                        @"""KvpWClassKvpVal"":{" +
+                            @"""Key"":""key""," +
+                            @"""Value"":{" +
+                                @"""Key"":""key""," +
+                                @"""Value"":null" +
+                            @"}" +
+                        @"}" +
+                    @"}";
+            SimpleClassWithKeyValuePairs obj = JsonSerializer.Deserialize<SimpleClassWithKeyValuePairs>(json);
+
+            Assert.Equal("key", obj.KvpWStrVal.Key);
+            Assert.Equal("key", obj.KvpWObjVal.Key);
+            Assert.Equal("key", obj.KvpWClassVal.Key);
+            Assert.Equal("key", obj.KvpWStrKvpVal.Key);
+            Assert.Equal("key", obj.KvpWObjKvpVal.Key);
+            Assert.Equal("key", obj.KvpWClassKvpVal.Key);
+            Assert.Equal("key", obj.KvpWStrKvpVal.Value.Key);
+            Assert.Equal("key", obj.KvpWObjKvpVal.Value.Key);
+            Assert.Equal("key", obj.KvpWClassKvpVal.Value.Key);
+
+            Assert.Null(obj.KvpWStrVal.Value);
+            Assert.Null(obj.KvpWObjVal.Value);
+            Assert.Null(obj.KvpWClassVal.Value);
+            Assert.Null(obj.KvpWStrKvpVal.Value.Value);
+            Assert.Null(obj.KvpWObjKvpVal.Value.Value);
+            Assert.Null(obj.KvpWClassKvpVal.Value.Value);
+        }
+
+        [Fact]
         public static void ReadSimpleTestClass_GenericCollectionWrappers()
         {
             SimpleTestClassWithGenericCollectionWrappers obj = JsonSerializer.Deserialize<SimpleTestClassWithGenericCollectionWrappers>(SimpleTestClassWithGenericCollectionWrappers.s_json);
index 7f0319b..bab668c 100644 (file)
@@ -791,6 +791,59 @@ namespace System.Text.Json.Serialization.Tests
         }
 
         [Fact]
+        public static void WriteKeyValuePairWithNullValues()
+        {
+            {
+                KeyValuePair<string, string> kvp = new KeyValuePair<string, string>("key", null);
+                Assert.Equal(@"{""Key"":""key"",""Value"":null}", JsonSerializer.Serialize(kvp));
+            }
+
+            {
+                KeyValuePair<string, object> kvp = new KeyValuePair<string, object>("key", null);
+                Assert.Equal(@"{""Key"":""key"",""Value"":null}", JsonSerializer.Serialize(kvp));
+            }
+
+            {
+                KeyValuePair<string, SimpleClassWithKeyValuePairs> kvp = new KeyValuePair<string, SimpleClassWithKeyValuePairs>("key", null);
+                Assert.Equal(@"{""Key"":""key"",""Value"":null}", JsonSerializer.Serialize(kvp));
+            }
+
+            {
+                KeyValuePair<string, KeyValuePair<string, string>> kvp = new KeyValuePair<string, KeyValuePair<string, string>>("key", new KeyValuePair<string, string>("key", null));
+                Assert.Equal(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}", JsonSerializer.Serialize(kvp));
+            }
+
+            {
+                KeyValuePair<string, KeyValuePair<string, object>> kvp = new KeyValuePair<string, KeyValuePair<string, object>>("key", new KeyValuePair<string, object>("key", null));
+                Assert.Equal(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}", JsonSerializer.Serialize(kvp));
+            }
+
+            {
+                KeyValuePair<string, KeyValuePair<string, SimpleClassWithKeyValuePairs>> kvp = new KeyValuePair<string, KeyValuePair<string, SimpleClassWithKeyValuePairs>>("key", new KeyValuePair<string, SimpleClassWithKeyValuePairs>("key", null));
+                Assert.Equal(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}", JsonSerializer.Serialize(kvp));
+            }
+        }
+
+        // https://github.com/dotnet/corefx/issues/39808
+        [Fact]
+        public static void WriteClassWithNullKeyValuePairValues_Regression39808()
+        {
+            var value = new SimpleClassWithKeyValuePairs()
+            {
+                KvpWStrVal = new KeyValuePair<string, string>("key", null),
+                KvpWObjVal = new KeyValuePair<string, object>("key", null),
+                KvpWClassVal = new KeyValuePair<string, SimpleClassWithKeyValuePairs>("key", null),
+                KvpWStrKvpVal = new KeyValuePair<string, KeyValuePair<string, string>>("key", new KeyValuePair<string, string>("key", null)),
+                KvpWObjKvpVal = new KeyValuePair<string, KeyValuePair<string, object>>("key", new KeyValuePair<string, object>("key", null)),
+                KvpWClassKvpVal = new KeyValuePair<string, KeyValuePair<string, SimpleClassWithKeyValuePairs>>("key", new KeyValuePair<string, SimpleClassWithKeyValuePairs>("key", null)),
+            };
+
+            string expectedJson = @"{""KvpWStrVal"":{""Key"":""key"",""Value"":null},""KvpWObjVal"":{""Key"":""key"",""Value"":null},""KvpWClassVal"":{""Key"":""key"",""Value"":null},""KvpWStrKvpVal"":{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}},""KvpWObjKvpVal"":{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}},""KvpWClassKvpVal"":{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}}";
+            string result = JsonSerializer.Serialize(value);
+            Assert.Equal(expectedJson, result);
+        }
+
+        [Fact]
         public static void WriteGenericCollectionWrappers()
         {
             SimpleTestClassWithGenericCollectionWrappers obj1 = new SimpleTestClassWithGenericCollectionWrappers();
@@ -820,5 +873,15 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Equal(SimpleTestClassWithStringToStringIReadOnlyDictionaryWrapper.s_json.StripWhitespace(), JsonSerializer.Serialize(obj5));
             Assert.Equal(SimpleTestClassWithStringToStringIReadOnlyDictionaryWrapper.s_json.StripWhitespace(), JsonSerializer.Serialize<object>(obj5));
         }
+
+        public class SimpleClassWithKeyValuePairs
+        {
+            public KeyValuePair<string, string> KvpWStrVal { get; set; }
+            public KeyValuePair<string, object> KvpWObjVal { get; set; }
+            public KeyValuePair<string, SimpleClassWithKeyValuePairs> KvpWClassVal { get; set; }
+            public KeyValuePair<string, KeyValuePair<string, string>> KvpWStrKvpVal { get; set; }
+            public KeyValuePair<string, KeyValuePair<string, object>> KvpWObjKvpVal { get; set; }
+            public KeyValuePair<string, KeyValuePair<string, SimpleClassWithKeyValuePairs>> KvpWClassKvpVal { get; set; }
+        }
     }
 }