Skip to content

Commit

Permalink
Make IgnoreNullValues apply only to reference types (#39147)
Browse files Browse the repository at this point in the history
  • Loading branch information
layomia authored Jul 15, 2020
1 parent fa8de89 commit 60cd838
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ protected override bool ReadAndCacheConstructorArgument(ref ReadStack state, ref

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!;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -63,7 +62,14 @@ private bool TryRead<TArg>(

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ private static JsonParameterInfo AddConstructorParameter(
{
if (jsonPropertyInfo.IsIgnored)
{
return JsonParameterInfo.CreateIgnoredParameterPlaceholder(jsonPropertyInfo, options);
return JsonParameterInfo.CreateIgnoredParameterPlaceholder(jsonPropertyInfo);
}

JsonConverter converter = jsonPropertyInfo.ConverterBase;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ internal abstract class JsonParameterInfo
// 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

Expand Down Expand Up @@ -60,13 +62,12 @@ public virtual void Initialize(
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>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -126,7 +125,7 @@ private void DetermineSerializationCapabilities(JsonIgnoreCondition? ignoreCondi
}
}

private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition)
private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition, bool isReferenceType)
{
if (ignoreCondition != null)
{
Expand All @@ -143,8 +142,11 @@ private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition)
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)
{
Expand All @@ -162,11 +164,11 @@ private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition)
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public override void Initialize(
}
}

GetPolicies(ignoreCondition);
GetPolicies(ignoreCondition, isReferenceType: default(T) == null);
}

public override JsonConverter ConverterBase
Expand Down Expand Up @@ -121,7 +121,7 @@ public override bool GetMemberAndWriteJson(object obj, ref WriteStack state, Utf
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)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2031,5 +2031,150 @@ private class ClassWithInitializedProps
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 { }
}
}

0 comments on commit 60cd838

Please sign in to comment.