Make IgnoreNullValues apply only to reference types (#39147)
authorLayomi Akinrinade <laakinri@microsoft.com>
Wed, 15 Jul 2020 18:36:59 +0000 (11:36 -0700)
committerGitHub <noreply@github.com>
Wed, 15 Jul 2020 18:36:59 +0000 (11:36 -0700)
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfo.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs
src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs

index 1fe65b9..38b3735 100644 (file)
@@ -19,7 +19,7 @@ namespace System.Text.Json.Serialization.Converters
 
             bool success = jsonParameterInfo.ConverterBase.TryReadAsObject(ref reader, jsonParameterInfo.Options!, ref state, out object? arg);
 
-            if (success)
+            if (success && !(arg == null && jsonParameterInfo.IgnoreDefaultValuesOnRead))
             {
                 ((object[])state.Current.CtorArgumentState!.Arguments)[jsonParameterInfo.Position] = arg!;
             }
index 12c3090..be414b6 100644 (file)
@@ -2,7 +2,6 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Diagnostics;
-using System.Runtime.CompilerServices;
 
 namespace System.Text.Json.Serialization.Converters
 {
@@ -63,7 +62,14 @@ namespace System.Text.Json.Serialization.Converters
 
             var info = (JsonParameterInfo<TArg>)jsonParameterInfo;
             var converter = (JsonConverter<TArg>)jsonParameterInfo.ConverterBase;
-            return converter.TryRead(ref reader, info.RuntimePropertyType, info.Options!, ref state, out arg!);
+
+            bool success = converter.TryRead(ref reader, info.RuntimePropertyType, info.Options!, ref state, out TArg value);
+
+            arg = value == null && jsonParameterInfo.IgnoreDefaultValuesOnRead
+                ? (TArg)info.DefaultValue! // Use default value specified on parameter, if any.
+                : value!;
+
+            return success;
         }
 
         protected override void InitializeConstructorArgumentCaches(ref ReadStack state, JsonSerializerOptions options)
index b3b9208..a893b7d 100644 (file)
@@ -419,7 +419,7 @@ namespace System.Text.Json
         {
             if (jsonPropertyInfo.IsIgnored)
             {
-                return JsonParameterInfo.CreateIgnoredParameterPlaceholder(jsonPropertyInfo, options);
+                return JsonParameterInfo.CreateIgnoredParameterPlaceholder(jsonPropertyInfo);
             }
 
             JsonConverter converter = jsonPropertyInfo.ConverterBase;
index c9b767f..0ccdbee 100644 (file)
@@ -19,6 +19,8 @@ namespace System.Text.Json
         // The default value of the parameter. This is `DefaultValue` of the `ParameterInfo`, if specified, or the CLR `default` for the `ParameterType`.
         public object? DefaultValue { get; protected set; }
 
+        public bool IgnoreDefaultValuesOnRead { get; private set; }
+
         // Options can be referenced here since all JsonPropertyInfos originate from a JsonClassInfo that is cached on JsonSerializerOptions.
         public JsonSerializerOptions? Options { get; set; } // initialized in Init method
 
@@ -60,13 +62,12 @@ namespace System.Text.Json
             Options = options;
             ShouldDeserialize = true;
             ConverterBase = matchingProperty.ConverterBase;
+            IgnoreDefaultValuesOnRead = matchingProperty.IgnoreDefaultValuesOnRead;
         }
 
         // Create a parameter that is ignored at run-time. It uses the same type (typeof(sbyte)) to help
         // prevent issues with unsupported types and helps ensure we don't accidently (de)serialize it.
-        public static JsonParameterInfo CreateIgnoredParameterPlaceholder(
-            JsonPropertyInfo matchingProperty,
-            JsonSerializerOptions options)
+        public static JsonParameterInfo CreateIgnoredParameterPlaceholder(JsonPropertyInfo matchingProperty)
         {
             return new JsonParameterInfo<sbyte>
             {
index 5061879..ae982a0 100644 (file)
@@ -4,7 +4,6 @@
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Reflection;
-using System.Runtime.CompilerServices;
 using System.Text.Json.Serialization;
 
 namespace System.Text.Json
@@ -126,7 +125,7 @@ namespace System.Text.Json
             }
         }
 
-        private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition)
+        private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition, bool isReferenceType)
         {
             if (ignoreCondition != null)
             {
@@ -143,8 +142,11 @@ namespace System.Text.Json
             else if (Options.IgnoreNullValues)
             {
                 Debug.Assert(Options.DefaultIgnoreCondition == JsonIgnoreCondition.Never);
-                IgnoreDefaultValuesOnRead = true;
-                IgnoreDefaultValuesOnWrite = true;
+                if (isReferenceType)
+                {
+                    IgnoreDefaultValuesOnRead = true;
+                    IgnoreDefaultValuesOnWrite = true;
+                }
             }
             else if (Options.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingDefault)
             {
@@ -162,11 +164,11 @@ namespace System.Text.Json
         public abstract bool GetMemberAndWriteJson(object obj, ref WriteStack state, Utf8JsonWriter writer);
         public abstract bool GetMemberAndWriteJsonExtensionData(object obj, ref WriteStack state, Utf8JsonWriter writer);
 
-        public virtual void GetPolicies(JsonIgnoreCondition? ignoreCondition)
+        public virtual void GetPolicies(JsonIgnoreCondition? ignoreCondition, bool isReferenceType)
         {
             DetermineSerializationCapabilities(ignoreCondition);
             DeterminePropertyName();
-            DetermineIgnoreCondition(ignoreCondition);
+            DetermineIgnoreCondition(ignoreCondition, isReferenceType);
         }
 
         public abstract object? GetValueAsObject(object obj);
index 9869949..e5c50ba 100644 (file)
@@ -89,7 +89,7 @@ namespace System.Text.Json
                     }
             }
 
-            GetPolicies(ignoreCondition);
+            GetPolicies(ignoreCondition, isReferenceType: default(T) == null);
         }
 
         public override JsonConverter ConverterBase
@@ -121,7 +121,7 @@ namespace System.Text.Json
             T value = Get!(obj);
 
             // Since devirtualization only works in non-shared generics,
-            // the default comparer is uded only for value types for now.
+            // the default comparer is used only for value types for now.
             // For reference types there is a quick check for null.
             if (IgnoreDefaultValuesOnWrite && (
                 default(T) == null ? value == null : EqualityComparer<T>.Default.Equals(default, value)))
index a63a074..b6676c3 100644 (file)
@@ -2031,5 +2031,150 @@ namespace System.Text.Json.Serialization.Tests
             public int MyInt { get; set; } = -1;
             public Point_2D_Struct MyPoint { get; set; } = new Point_2D_Struct(-1, -1);
         }
+
+        [Fact]
+        public static void ValueType_Properties_NotIgnoredWhen_IgnoreNullValues_Active_ClassTest()
+        {
+            var options = new JsonSerializerOptions { IgnoreNullValues = true };
+
+            // Deserialization.
+            string json = @"{""MyString"":null,""MyInt"":0,""MyPointClass"":null,""MyPointStruct"":{""X"":0,""Y"":0}}";
+
+            ClassWithValueAndReferenceTypes obj = JsonSerializer.Deserialize<ClassWithValueAndReferenceTypes>(json, options);
+
+            // Null values ignored for reference types.
+            Assert.Equal("Default", obj.MyString);
+            Assert.NotNull(obj.MyPointClass);
+
+            // Default values not ignored for value types.
+            Assert.Equal(0, obj.MyInt);
+            Assert.Equal(0, obj.MyPointStruct.X);
+            Assert.Equal(0, obj.MyPointStruct.Y);
+
+            // Serialization.
+
+            // Make all members their default CLR value.
+            obj.MyString = null;
+            obj.MyPointClass = null;
+
+            json = JsonSerializer.Serialize(obj, options);
+
+            // Null values not serialized, default values for value types serialized.
+            JsonTestHelper.AssertJsonEqual(@"{""MyInt"":0,""MyPointStruct"":{""X"":0,""Y"":0}}", json);
+        }
+
+        [Fact]
+        public static void ValueType_Properties_NotIgnoredWhen_IgnoreNullValues_Active_LargeStructTest()
+        {
+            var options = new JsonSerializerOptions { IgnoreNullValues = true };
+
+            // Deserialization.
+            string json = @"{""MyString"":null,""MyInt"":0,""MyBool"":false,""MyPointClass"":null,""MyPointStruct"":{""X"":0,""Y"":0}}";
+
+            LargeStructWithValueAndReferenceTypes obj = JsonSerializer.Deserialize<LargeStructWithValueAndReferenceTypes>(json, options);
+
+            // Null values ignored for reference types.
+
+            Assert.Equal("Default", obj.MyString);
+            // No way to specify a non-constant default before construction with ctor, so this remains null.
+            Assert.Null(obj.MyPointClass);
+
+            // Default values not ignored for value types.
+            Assert.Equal(0, obj.MyInt);
+            Assert.False(obj.MyBool);
+            Assert.Equal(0, obj.MyPointStruct.X);
+            Assert.Equal(0, obj.MyPointStruct.Y);
+
+            // Serialization.
+
+            // Make all members their default CLR value.
+            obj = new LargeStructWithValueAndReferenceTypes(null, new Point_2D_Struct(0, 0), null, 0, false);
+
+            json = JsonSerializer.Serialize(obj, options);
+
+            // Null values not serialized, default values for value types serialized.
+            JsonTestHelper.AssertJsonEqual(@"{""MyInt"":0,""MyBool"":false,""MyPointStruct"":{""X"":0,""Y"":0}}", json);
+        }
+
+        [Fact]
+        public static void ValueType_Properties_NotIgnoredWhen_IgnoreNullValues_Active_SmallStructTest()
+        {
+            var options = new JsonSerializerOptions { IgnoreNullValues = true };
+
+            // Deserialization.
+            string json = @"{""MyString"":null,""MyInt"":0,""MyPointStruct"":{""X"":0,""Y"":0}}";
+
+            SmallStructWithValueAndReferenceTypes obj = JsonSerializer.Deserialize<SmallStructWithValueAndReferenceTypes>(json, options);
+
+            // Null values ignored for reference types.
+            Assert.Equal("Default", obj.MyString);
+
+            // Default values not ignored for value types.
+            Assert.Equal(0, obj.MyInt);
+            Assert.Equal(0, obj.MyPointStruct.X);
+            Assert.Equal(0, obj.MyPointStruct.Y);
+
+            // Serialization.
+
+            // Make all members their default CLR value.
+            obj = new SmallStructWithValueAndReferenceTypes(new Point_2D_Struct(0, 0), null, 0);
+
+            json = JsonSerializer.Serialize(obj, options);
+
+            // Null values not serialized, default values for value types serialized.
+            JsonTestHelper.AssertJsonEqual(@"{""MyInt"":0,""MyPointStruct"":{""X"":0,""Y"":0}}", json);
+        }
+
+        private class ClassWithValueAndReferenceTypes
+        {
+            public string MyString { get; set; } = "Default";
+            public int MyInt { get; set; } = -1;
+            public PointClass MyPointClass { get; set; } = new PointClass();
+            public Point_2D_Struct MyPointStruct { get; set; } = new Point_2D_Struct(1, 2);
+        }
+
+        private struct LargeStructWithValueAndReferenceTypes
+        {
+            public string MyString { get; }
+            public int MyInt { get; set; }
+            public bool MyBool { get; set; }
+            public PointClass MyPointClass { get; set; }
+            public Point_2D_Struct MyPointStruct { get; set; }
+
+            [JsonConstructor]
+            public LargeStructWithValueAndReferenceTypes(
+                PointClass myPointClass,
+                Point_2D_Struct myPointStruct,
+                string myString = "Default",
+                int myInt = -1,
+                bool myBool = true)
+            {
+                MyString = myString;
+                MyInt = myInt;
+                MyBool = myBool;
+                MyPointClass = myPointClass;
+                MyPointStruct = myPointStruct;
+            }
+        }
+
+        private struct SmallStructWithValueAndReferenceTypes
+        {
+            public string MyString { get; }
+            public int MyInt { get; set; }
+            public Point_2D_Struct MyPointStruct { get; set; }
+
+            [JsonConstructor]
+            public SmallStructWithValueAndReferenceTypes(
+                Point_2D_Struct myPointStruct,
+                string myString = "Default",
+                int myInt = -1)
+            {
+                MyString = myString;
+                MyInt = myInt;
+                MyPointStruct = myPointStruct;
+            }
+        }
+
+        private class PointClass { }
     }
 }