From: Layomi Akinrinade Date: Wed, 15 Jul 2020 18:36:59 +0000 (-0700) Subject: Make IgnoreNullValues apply only to reference types (#39147) X-Git-Tag: submit/tizen/20210909.063632~6660 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=60cd838b4c6d5736a514dfe00d2d417e7c89b22b;p=platform%2Fupstream%2Fdotnet%2Fruntime.git Make IgnoreNullValues apply only to reference types (#39147) --- diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs index 1fe65b9..38b3735 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs @@ -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!; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs index 12c3090..be414b6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs @@ -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)jsonParameterInfo; var converter = (JsonConverter)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) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs index b3b9208..a893b7d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs @@ -419,7 +419,7 @@ namespace System.Text.Json { if (jsonPropertyInfo.IsIgnored) { - return JsonParameterInfo.CreateIgnoredParameterPlaceholder(jsonPropertyInfo, options); + return JsonParameterInfo.CreateIgnoredParameterPlaceholder(jsonPropertyInfo); } JsonConverter converter = jsonPropertyInfo.ConverterBase; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfo.cs index c9b767f..0ccdbee 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfo.cs @@ -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 { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs index 5061879..ae982a0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs @@ -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); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs index 9869949..e5c50ba 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs @@ -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.Default.Equals(default, value))) diff --git a/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs index a63a074..b6676c3 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs @@ -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(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(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(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 { } } }