diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index e7ef98216e31c..3072fd4522cb3 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -724,6 +724,12 @@ public void WriteStringValue(System.Text.Json.JsonEncodedText value) { } } namespace System.Text.Json.Serialization { + public enum JsonIgnoreCondition + { + Always = 0, + WhenNull = 1, + Never = 2, + } public abstract partial class JsonAttribute : System.Attribute { protected JsonAttribute() { } @@ -767,6 +773,7 @@ public JsonExtensionDataAttribute() { } public sealed partial class JsonIgnoreAttribute : System.Text.Json.Serialization.JsonAttribute { public JsonIgnoreAttribute() { } + public System.Text.Json.Serialization.JsonIgnoreCondition Condition { get { throw null; } set { } } } [System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false)] public sealed partial class JsonPropertyNameAttribute : System.Text.Json.Serialization.JsonAttribute diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 39786bdc4a4b1..57999db56637e 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -131,6 +131,7 @@ + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.Cache.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.Cache.cs index 2f8dd30dc0483..330e3052f19c0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.Cache.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.Cache.cs @@ -79,8 +79,8 @@ public Dictionary CreateParameterCache(int capacity, public static JsonPropertyInfo AddProperty(Type propertyType, PropertyInfo propertyInfo, Type parentClassType, JsonSerializerOptions options) { - bool hasIgnoreAttribute = (JsonPropertyInfo.GetAttribute(propertyInfo) != null); - if (hasIgnoreAttribute) + JsonIgnoreAttribute? ignoreAttribute = JsonPropertyInfo.GetAttribute(propertyInfo); + if (ignoreAttribute?.Condition == JsonIgnoreCondition.Always) { return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(propertyInfo, 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 8f35740b459a2..3068699eac2f2 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 @@ -304,22 +304,19 @@ private static JsonParameterInfo AddConstructorParameter( JsonPropertyInfo jsonPropertyInfo, JsonSerializerOptions options) { - string matchingPropertyName = jsonPropertyInfo.NameAsString!; - if (jsonPropertyInfo.IsIgnored) { - return JsonParameterInfo.CreateIgnoredParameterPlaceholder(matchingPropertyName, parameterInfo, options); + return JsonParameterInfo.CreateIgnoredParameterPlaceholder(parameterInfo, jsonPropertyInfo, options); } JsonConverter converter = jsonPropertyInfo.ConverterBase; JsonParameterInfo jsonParameterInfo = converter.CreateJsonParameterInfo(); jsonParameterInfo.Initialize( - matchingPropertyName, jsonPropertyInfo.DeclaredPropertyType, jsonPropertyInfo.RuntimePropertyType!, parameterInfo, - converter, + jsonPropertyInfo, options); return jsonParameterInfo; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIgnoreAttribute.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIgnoreAttribute.cs index 26fac42a90d59..2b081d4b4b5b7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIgnoreAttribute.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIgnoreAttribute.cs @@ -10,6 +10,12 @@ namespace System.Text.Json.Serialization [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public sealed class JsonIgnoreAttribute : JsonAttribute { + /// + /// Specifies the condition that must be met before a property will be ignored. + /// + /// The default value is . + public JsonIgnoreCondition Condition { get; set; } = JsonIgnoreCondition.Always; + /// /// Initializes a new instance of . /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIgnoreCondition.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIgnoreCondition.cs new file mode 100644 index 0000000000000..70a8bca3ad378 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIgnoreCondition.cs @@ -0,0 +1,25 @@ +// 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. + +namespace System.Text.Json.Serialization +{ + /// + /// Controls how the ignores properties on serialization and deserialization. + /// + public enum JsonIgnoreCondition + { + /// + /// Property will always be ignored. + /// + Always = 0, + /// + /// Property will only be ignored if it is null. + /// + WhenNull = 1, + /// + /// Property will always be serialized and deserialized, regardless of configuration. + /// + Never = 2 + } +} 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 7f5076277c9b1..d531d9f9c2d27 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 @@ -59,11 +59,10 @@ public JsonClassInfo RuntimeClassInfo public bool ShouldDeserialize { get; private set; } public virtual void Initialize( - string matchingPropertyName, Type declaredPropertyType, Type runtimePropertyType, ParameterInfo parameterInfo, - JsonConverter converter, + JsonPropertyInfo matchingProperty, JsonSerializerOptions options) { _runtimePropertyType = runtimePropertyType; @@ -73,12 +72,12 @@ public virtual void Initialize( Position = parameterInfo.Position; ShouldDeserialize = true; - DetermineParameterName(matchingPropertyName); + DetermineParameterName(matchingProperty); } - private void DetermineParameterName(string matchingPropertyName) + private void DetermineParameterName(JsonPropertyInfo matchingProperty) { - NameAsString = matchingPropertyName; + NameAsString = matchingProperty.NameAsString!; // `NameAsString` is valid UTF16, so just call the simple UTF16->UTF8 encoder. ParameterName = Encoding.UTF8.GetBytes(NameAsString); @@ -89,8 +88,8 @@ private void DetermineParameterName(string matchingPropertyName) // 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( - string matchingPropertyName, ParameterInfo parameterInfo, + JsonPropertyInfo matchingProperty, JsonSerializerOptions options) { JsonParameterInfo jsonParameterInfo = new JsonParameterInfo(); @@ -98,7 +97,7 @@ public static JsonParameterInfo CreateIgnoredParameterPlaceholder( jsonParameterInfo.ParameterInfo = parameterInfo; jsonParameterInfo.ShouldDeserialize = false; - jsonParameterInfo.DetermineParameterName(matchingPropertyName); + jsonParameterInfo.DetermineParameterName(matchingProperty); return jsonParameterInfo; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfoOfT.cs index b2444779146c1..398e0dedc3a0c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfoOfT.cs @@ -14,6 +14,7 @@ namespace System.Text.Json internal class JsonParameterInfo : JsonParameterInfo { private JsonConverter _converter = null!; + private bool _ignoreNullValues; private Type _runtimePropertyType = null!; public override JsonConverter ConverterBase => _converter; @@ -21,22 +22,21 @@ internal class JsonParameterInfo : JsonParameterInfo public T TypedDefaultValue { get; private set; } = default!; public override void Initialize( - string matchingPropertyName, Type declaredPropertyType, Type runtimePropertyType, ParameterInfo parameterInfo, - JsonConverter converter, + JsonPropertyInfo matchingProperty, JsonSerializerOptions options) { base.Initialize( - matchingPropertyName, declaredPropertyType, runtimePropertyType, parameterInfo, - converter, + matchingProperty, options); - _converter = (JsonConverter)converter; + _converter = (JsonConverter)matchingProperty.ConverterBase; + _ignoreNullValues = matchingProperty.IgnoreNullValues; _runtimePropertyType = runtimePropertyType; if (parameterInfo.HasDefaultValue) @@ -55,8 +55,7 @@ public override bool ReadJson(ref ReadStack state, ref Utf8JsonReader reader, ou bool success; bool isNullToken = reader.TokenType == JsonTokenType.Null; - if (isNullToken && - ((!_converter.HandleNullValue && !state.IsContinuation) || Options.IgnoreNullValues)) + if (isNullToken && !_converter.HandleNullValue && !state.IsContinuation) { // Don't have to check for IgnoreNullValue option here because we set the default value (likely null) regardless value = DefaultValue; @@ -85,10 +84,8 @@ public bool ReadJsonTyped(ref ReadStack state, ref Utf8JsonReader reader, out T bool success; bool isNullToken = reader.TokenType == JsonTokenType.Null; - if (isNullToken && - ((!_converter.HandleNullValue && !state.IsContinuation) || Options.IgnoreNullValues)) + if (isNullToken && !_converter.HandleNullValue && !state.IsContinuation) { - // Don't have to check for IgnoreNullValue option here because we set the default value (likely null) regardless value = TypedDefaultValue; return true; } 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 c519d62471105..0a0d33a2c84aa 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 @@ -118,6 +118,28 @@ private void DetermineSerializationCapabilities() } } + private void DetermineIgnoreCondition() + { + JsonIgnoreAttribute? ignoreAttribute; + if (PropertyInfo != null && (ignoreAttribute = GetAttribute(PropertyInfo)) != null) + { + JsonIgnoreCondition condition = ignoreAttribute.Condition; + + // We should have created a placeholder property for this upstream and shouldn't be down this code-path. + Debug.Assert(condition != JsonIgnoreCondition.Always); + + if (condition != JsonIgnoreCondition.Never) + { + Debug.Assert(condition == JsonIgnoreCondition.WhenNull); + IgnoreNullValues = true; + } + } + else + { + IgnoreNullValues = Options.IgnoreNullValues; + } + } + // The escaped name passed to the writer. // Use a field here (not a property) to avoid value semantics. public JsonEncodedText? EscapedName; @@ -134,7 +156,7 @@ public virtual void GetPolicies() { DetermineSerializationCapabilities(); DeterminePropertyName(); - IgnoreNullValues = Options.IgnoreNullValues; + DetermineIgnoreCondition(); } public abstract object? GetValueAsObject(object obj); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs index fda63d5879751..3def5052f8191 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs @@ -2,7 +2,6 @@ // 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.Runtime.CompilerServices; diff --git a/src/libraries/System.Text.Json/tests/Serialization/ConstructorTests.ParameterMatching.cs b/src/libraries/System.Text.Json/tests/Serialization/ConstructorTests.ParameterMatching.cs index edb7090b8e08e..c019f74a555c2 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ConstructorTests.ParameterMatching.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ConstructorTests.ParameterMatching.cs @@ -339,20 +339,19 @@ public void PropertiesNotSet_WhenJSON_MapsToConstructorParameters() [Fact] public void IgnoreNullValues_DontSetNull_ToConstructorArguments_ThatCantBeNull() { - // Default is to throw JsonException when null applied to types that can't be null. + // Throw JsonException when null applied to types that can't be null. Assert.Throws(() => Serializer.Deserialize(@"{""Point3DStruct"":null,""Int"":null,""ImmutableArray"":null}")); Assert.Throws(() => Serializer.Deserialize(@"{""Point3DStruct"":null}")); Assert.Throws(() => Serializer.Deserialize(@"{""Int"":null}")); Assert.Throws(() => Serializer.Deserialize(@"{""ImmutableArray"":null}")); - // Set arguments to default values when IgnoreNullValues is on. + // Throw even when IgnoreNullValues is true for symmetry with property deserialization, + // until https://github.com/dotnet/runtime/issues/30795 is addressed. var options = new JsonSerializerOptions { IgnoreNullValues = true }; - var obj = Serializer.Deserialize(@"{""Int"":null,""Point3DStruct"":null,""ImmutableArray"":null}", options); - Assert.Equal(0, obj.Point3DStruct.X); - Assert.Equal(0, obj.Point3DStruct.Y); - Assert.Equal(0, obj.Point3DStruct.Z); - Assert.True(obj.ImmutableArray.IsDefault); - Assert.Equal(50, obj.Int); + Assert.Throws(() => Serializer.Deserialize(@"{""Point3DStruct"":null,""Int"":null,""ImmutableArray"":null}", options)); + Assert.Throws(() => Serializer.Deserialize(@"{""Point3DStruct"":null}", options)); + Assert.Throws(() => Serializer.Deserialize(@"{""Int"":null}", options)); + Assert.Throws(() => Serializer.Deserialize(@"{""ImmutableArray"":null}", options)); } [Fact] diff --git a/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs index 74567651b234a..d7b4791b5ad75 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs @@ -156,7 +156,7 @@ public static void JsonIgnoreAttribute_UnsupportedCollection() // Unsupported collections will throw on deserialize by default. Assert.Throws(() => JsonSerializer.Deserialize(json)); - + // Using new options instance to prevent using previously cached metadata. JsonSerializerOptions options = new JsonSerializerOptions(); @@ -166,7 +166,7 @@ public static void JsonIgnoreAttribute_UnsupportedCollection() // Unsupported collections will throw on deserialize by default. options = new JsonSerializerOptions(); Assert.Throws(() => JsonSerializer.Deserialize(wrapperJson, options)); - + options = new JsonSerializerOptions(); // Unsupported collections will throw on serialize by default. Assert.Throws(() => JsonSerializer.Serialize(new WrapperForClassWithUnsupportedDictionary(), options)); @@ -456,5 +456,330 @@ public static void OverrideJsonIgnorePropertyUsingJsonPropertyNameReversed() string jsonSerialized = JsonSerializer.Serialize(obj); Assert.Equal(json, jsonSerialized); } + + [Theory] + [InlineData(typeof(ClassWithProperty_IgnoreConditionAlways))] + [InlineData(typeof(ClassWithProperty_IgnoreConditionAlways_Ctor))] + public static void JsonIgnoreConditionSetToAlwaysWorks(Type type) + { + string json = @"{""MyString"":""Random"",""MyDateTime"":""2020-03-23"",""MyInt"":4}"; + + object obj = JsonSerializer.Deserialize(json, type); + Assert.Equal("Random", (string)type.GetProperty("MyString").GetValue(obj)); + Assert.Equal(default, (DateTime)type.GetProperty("MyDateTime").GetValue(obj)); + Assert.Equal(4, (int)type.GetProperty("MyInt").GetValue(obj)); + + string serialized = JsonSerializer.Serialize(obj); + Assert.Contains(@"""MyString"":""Random""", serialized); + Assert.Contains(@"""MyInt"":4", serialized); + Assert.DoesNotContain(@"""MyDateTime"":", serialized); + } + + private class ClassWithProperty_IgnoreConditionAlways + { + public string MyString { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public DateTime MyDateTime { get; set; } + public int MyInt { get; set; } + } + + private class ClassWithProperty_IgnoreConditionAlways_Ctor + { + public string MyString { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public DateTime MyDateTime { get; } + public int MyInt { get; } + + public ClassWithProperty_IgnoreConditionAlways_Ctor(DateTime myDateTime, int myInt) + { + MyDateTime = myDateTime; + MyInt = myInt; + } + } + + [Theory] + [MemberData(nameof(JsonIgnoreConditionWhenNull_ClassProperty_TestData))] + public static void JsonIgnoreConditionWhenNull_ClassProperty(Type type, JsonSerializerOptions options) + { + // Property shouldn't be ignored if it isn't null. + string json = @"{""Int1"":1,""MyString"":""Random"",""Int2"":2}"; + + object obj = JsonSerializer.Deserialize(json, type, options); + Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); + Assert.Equal("Random", (string)type.GetProperty("MyString").GetValue(obj)); + Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); + + string serialized = JsonSerializer.Serialize(obj, options); + Assert.Contains(@"""Int1"":1", serialized); + Assert.Contains(@"""MyString"":""Random""", serialized); + Assert.Contains(@"""Int2"":2", serialized); + + // Property should be ignored when null. + json = @"{""Int1"":1,""MyString"":null,""Int2"":2}"; + + obj = JsonSerializer.Deserialize(json, type, options); + Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); + Assert.Equal("DefaultString", (string)type.GetProperty("MyString").GetValue(obj)); + Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); + + // Set property to be ignored to null. + type.GetProperty("MyString").SetValue(obj, null); + + serialized = JsonSerializer.Serialize(obj, options); + Assert.Contains(@"""Int1"":1", serialized); + Assert.Contains(@"""Int2"":2", serialized); + Assert.DoesNotContain(@"""MyString"":", serialized); + } + + private class ClassWithClassProperty_IgnoreConditionWhenNull + { + public int Int1 { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenNull)] + public string MyString { get; set; } = "DefaultString"; + public int Int2 { get; set; } + } + + private class ClassWithClassProperty_IgnoreConditionWhenNull_Ctor + { + public int Int1 { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenNull)] + public string MyString { get; set; } = "DefaultString"; + public int Int2 { get; set; } + + public ClassWithClassProperty_IgnoreConditionWhenNull_Ctor(string myString) + { + if (myString != null) + { + MyString = myString; + } + } + } + + private static IEnumerable JsonIgnoreConditionWhenNull_ClassProperty_TestData() + { + yield return new object[] { typeof(ClassWithClassProperty_IgnoreConditionWhenNull), new JsonSerializerOptions() }; + yield return new object[] { typeof(ClassWithClassProperty_IgnoreConditionWhenNull_Ctor), new JsonSerializerOptions { IgnoreNullValues = true } }; + } + + [Theory] + [MemberData(nameof(JsonIgnoreConditionWhenNull_StructProperty_TestData))] + public static void JsonIgnoreConditionWhenNull_StructProperty(Type type, JsonSerializerOptions options) + { + // Property shouldn't be ignored if it isn't null. + string json = @"{""Int1"":1,""MyInt"":3,""Int2"":2}"; + + object obj = JsonSerializer.Deserialize(json, type, options); + Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); + Assert.Equal(3, (int)type.GetProperty("MyInt").GetValue(obj)); + Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); + + string serialized = JsonSerializer.Serialize(obj, options); + Assert.Contains(@"""Int1"":1", serialized); + Assert.Contains(@"""MyInt"":3", serialized); + Assert.Contains(@"""Int2"":2", serialized); + + // Null being assigned to non-nullable types is invalid. + json = @"{""Int1"":1,""MyInt"":null,""Int2"":2}"; + Assert.Throws(() => JsonSerializer.Deserialize(json, type, options)); + } + + private class ClassWithStructProperty_IgnoreConditionWhenNull + { + public int Int1 { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenNull)] + public int MyInt { get; set; } + public int Int2 { get; set; } + } + + private struct StructWithStructProperty_IgnoreConditionWhenNull_Ctor + { + public int Int1 { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenNull)] + public int MyInt { get; } + public int Int2 { get; set; } + + [JsonConstructor] + public StructWithStructProperty_IgnoreConditionWhenNull_Ctor(int myInt) + { + Int1 = 0; + MyInt = myInt; + Int2 = 0; + } + } + + private static IEnumerable JsonIgnoreConditionWhenNull_StructProperty_TestData() + { + yield return new object[] { typeof(ClassWithStructProperty_IgnoreConditionWhenNull), new JsonSerializerOptions() }; + yield return new object[] { typeof(StructWithStructProperty_IgnoreConditionWhenNull_Ctor), new JsonSerializerOptions { IgnoreNullValues = true } }; + } + + [Theory] + [MemberData(nameof(JsonIgnoreConditionNever_TestData))] + public static void JsonIgnoreConditionNever(Type type) + { + // Property should always be (de)serialized, even when null. + string json = @"{""Int1"":1,""MyString"":""Random"",""Int2"":2}"; + + object obj = JsonSerializer.Deserialize(json, type); + Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); + Assert.Equal("Random", (string)type.GetProperty("MyString").GetValue(obj)); + Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); + + string serialized = JsonSerializer.Serialize(obj); + Assert.Contains(@"""Int1"":1", serialized); + Assert.Contains(@"""MyString"":""Random""", serialized); + Assert.Contains(@"""Int2"":2", serialized); + + // Property should always be (de)serialized, even when null. + json = @"{""Int1"":1,""MyString"":null,""Int2"":2}"; + + obj = JsonSerializer.Deserialize(json, type); + Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); + Assert.Null((string)type.GetProperty("MyString").GetValue(obj)); + Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); + + serialized = JsonSerializer.Serialize(obj); + Assert.Contains(@"""Int1"":1", serialized); + Assert.Contains(@"""MyString"":null", serialized); + Assert.Contains(@"""Int2"":2", serialized); + } + + [Theory] + [MemberData(nameof(JsonIgnoreConditionNever_TestData))] + public static void JsonIgnoreConditionNever_IgnoreNullValues_True(Type type) + { + // Property should always be (de)serialized. + string json = @"{""Int1"":1,""MyString"":""Random"",""Int2"":2}"; + var options = new JsonSerializerOptions { IgnoreNullValues = true }; + + object obj = JsonSerializer.Deserialize(json, type, options); + Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); + Assert.Equal("Random", (string)type.GetProperty("MyString").GetValue(obj)); + Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); + + string serialized = JsonSerializer.Serialize(obj, options); + Assert.Contains(@"""Int1"":1", serialized); + Assert.Contains(@"""MyString"":""Random""", serialized); + Assert.Contains(@"""Int2"":2", serialized); + + // Property should always be (de)serialized, even when null. + json = @"{""Int1"":1,""MyString"":null,""Int2"":2}"; + + obj = JsonSerializer.Deserialize(json, type, options); + Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); + Assert.Null((string)type.GetProperty("MyString").GetValue(obj)); + Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); + + serialized = JsonSerializer.Serialize(obj, options); + Assert.Contains(@"""Int1"":1", serialized); + Assert.Contains(@"""MyString"":null", serialized); + Assert.Contains(@"""Int2"":2", serialized); + } + + private class ClassWithStructProperty_IgnoreConditionNever + { + public int Int1 { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string MyString { get; set; } + public int Int2 { get; set; } + } + + private class ClassWithStructProperty_IgnoreConditionNever_Ctor + { + public int Int1 { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string MyString { get; } + public int Int2 { get; set; } + + public ClassWithStructProperty_IgnoreConditionNever_Ctor(string myString) + { + MyString = myString; + } + } + + private static IEnumerable JsonIgnoreConditionNever_TestData() + { + yield return new object[] { typeof(ClassWithStructProperty_IgnoreConditionNever) }; + yield return new object[] { typeof(ClassWithStructProperty_IgnoreConditionNever_Ctor) }; + } + + [Fact] + public static void JsonIgnoreCondition_LastOneWins() + { + string json = @"{""MyString"":""Random"",""MYSTRING"":null}"; + + var options = new JsonSerializerOptions { + IgnoreNullValues = true, + PropertyNameCaseInsensitive = true + }; + var obj = JsonSerializer.Deserialize(json, options); + + Assert.Null(obj.MyString); + } + + [Fact] + public static void ClassWithComplexObjectsUsingIgnoreWhenNullAttribute() + { + string json = @"{""Class"":{""MyInt16"":18}, ""Dictionary"":null}"; + + ClassUsingIgnoreWhenNullAttribute obj = JsonSerializer.Deserialize(json); + + // Class is deserialized because it is not null in json. + Assert.NotNull(obj.Class); + Assert.Equal(18, obj.Class.MyInt16); + + // Dictionary is left alone because it is null in json. + Assert.NotNull(obj.Dictionary); + Assert.Equal(1, obj.Dictionary.Count); + Assert.Equal("Value", obj.Dictionary["Key"]); + + + obj = new ClassUsingIgnoreWhenNullAttribute(); + json = JsonSerializer.Serialize(obj); + Assert.Equal(@"{""Dictionary"":{""Key"":""Value""}}", json); + } + + public class ClassUsingIgnoreWhenNullAttribute + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenNull)] + public SimpleTestClass Class { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenNull)] + public Dictionary Dictionary { get; set; } = new Dictionary { ["Key"] = "Value" }; + } + + [Fact] + public static void ClassWithComplexObjectUsingIgnoreNeverAttribute() + { + string json = @"{""Class"":null, ""Dictionary"":null}"; + var options = new JsonSerializerOptions { IgnoreNullValues = true }; + + var obj = JsonSerializer.Deserialize(json, options); + + // Class is not deserialized because it is null in json. + Assert.NotNull(obj.Class); + Assert.Equal(18, obj.Class.MyInt16); + + // Dictionary is deserialized regardless of being null in json. + Assert.Null(obj.Dictionary); + + // Serialize when values are null. + obj = new ClassUsingIgnoreNeverAttribute(); + obj.Class = null; + obj.Dictionary = null; + + json = JsonSerializer.Serialize(obj, options); + + // Class is not included in json because it was null, Dictionary is included regardless of being null. + Assert.Equal(@"{""Dictionary"":null}", json); + } + + public class ClassUsingIgnoreNeverAttribute + { + public SimpleTestClass Class { get; set; } = new SimpleTestClass { MyInt16 = 18 }; + + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public Dictionary Dictionary { get; set; } = new Dictionary { ["Key"] = "Value" }; + } } }