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 deb79317200c1..5c3e9d81519b8 100644
--- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs
+++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs
@@ -456,6 +456,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() { }
@@ -499,6 +505,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 346209bb19622..ee9488ed2c0cb 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 70555efcc74d6..3f18845236380 100644
--- a/src/libraries/System.Text.Json/tests/Serialization/ConstructorTests.ParameterMatching.cs
+++ b/src/libraries/System.Text.Json/tests/Serialization/ConstructorTests.ParameterMatching.cs
@@ -340,20 +340,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