Skip to content

Commit c653291

Browse files
authored
Add JsonIgnoreCondition & per-property ignore logic (#34049)
* Add JsonIgnoreCondition & per-property ignore logic * Address review feedback
1 parent d8f763e commit c653291

12 files changed

+413
-36
lines changed

src/libraries/System.Text.Json/ref/System.Text.Json.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,12 @@ public void WriteStringValue(System.Text.Json.JsonEncodedText value) { }
456456
}
457457
namespace System.Text.Json.Serialization
458458
{
459+
public enum JsonIgnoreCondition
460+
{
461+
Always = 0,
462+
WhenNull = 1,
463+
Never = 2,
464+
}
459465
public abstract partial class JsonAttribute : System.Attribute
460466
{
461467
protected JsonAttribute() { }
@@ -499,6 +505,7 @@ public JsonExtensionDataAttribute() { }
499505
public sealed partial class JsonIgnoreAttribute : System.Text.Json.Serialization.JsonAttribute
500506
{
501507
public JsonIgnoreAttribute() { }
508+
public System.Text.Json.Serialization.JsonIgnoreCondition Condition { get { throw null; } set { } }
502509
}
503510
[System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false)]
504511
public sealed partial class JsonPropertyNameAttribute : System.Text.Json.Serialization.JsonAttribute

src/libraries/System.Text.Json/src/System.Text.Json.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
<Compile Include="System\Text\Json\Serialization\JsonDictionaryConverter.cs" />
132132
<Compile Include="System\Text\Json\Serialization\JsonExtensionDataAttribute.cs" />
133133
<Compile Include="System\Text\Json\Serialization\JsonIgnoreAttribute.cs" />
134+
<Compile Include="System\Text\Json\Serialization\JsonIgnoreCondition.cs" />
134135
<Compile Include="System\Text\Json\Serialization\JsonNamingPolicy.cs" />
135136
<Compile Include="System\Text\Json\Serialization\JsonObjectConverter.cs" />
136137
<Compile Include="System\Text\Json\Serialization\JsonParameterInfo.cs" />

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.Cache.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ public Dictionary<string, JsonParameterInfo> CreateParameterCache(int capacity,
7979

8080
public static JsonPropertyInfo AddProperty(Type propertyType, PropertyInfo propertyInfo, Type parentClassType, JsonSerializerOptions options)
8181
{
82-
bool hasIgnoreAttribute = (JsonPropertyInfo.GetAttribute<JsonIgnoreAttribute>(propertyInfo) != null);
83-
if (hasIgnoreAttribute)
82+
JsonIgnoreAttribute? ignoreAttribute = JsonPropertyInfo.GetAttribute<JsonIgnoreAttribute>(propertyInfo);
83+
if (ignoreAttribute?.Condition == JsonIgnoreCondition.Always)
8484
{
8585
return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(propertyInfo, options);
8686
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,22 +304,19 @@ private static JsonParameterInfo AddConstructorParameter(
304304
JsonPropertyInfo jsonPropertyInfo,
305305
JsonSerializerOptions options)
306306
{
307-
string matchingPropertyName = jsonPropertyInfo.NameAsString!;
308-
309307
if (jsonPropertyInfo.IsIgnored)
310308
{
311-
return JsonParameterInfo.CreateIgnoredParameterPlaceholder(matchingPropertyName, parameterInfo, options);
309+
return JsonParameterInfo.CreateIgnoredParameterPlaceholder(parameterInfo, jsonPropertyInfo, options);
312310
}
313311

314312
JsonConverter converter = jsonPropertyInfo.ConverterBase;
315313

316314
JsonParameterInfo jsonParameterInfo = converter.CreateJsonParameterInfo();
317315
jsonParameterInfo.Initialize(
318-
matchingPropertyName,
319316
jsonPropertyInfo.DeclaredPropertyType,
320317
jsonPropertyInfo.RuntimePropertyType!,
321318
parameterInfo,
322-
converter,
319+
jsonPropertyInfo,
323320
options);
324321

325322
return jsonParameterInfo;

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIgnoreAttribute.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ namespace System.Text.Json.Serialization
1010
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
1111
public sealed class JsonIgnoreAttribute : JsonAttribute
1212
{
13+
/// <summary>
14+
/// Specifies the condition that must be met before a property will be ignored.
15+
/// </summary>
16+
/// <remarks>The default value is <see cref="JsonIgnoreCondition.Always"/>.</remarks>
17+
public JsonIgnoreCondition Condition { get; set; } = JsonIgnoreCondition.Always;
18+
1319
/// <summary>
1420
/// Initializes a new instance of <see cref="JsonIgnoreAttribute"/>.
1521
/// </summary>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace System.Text.Json.Serialization
6+
{
7+
/// <summary>
8+
/// Controls how the <see cref="JsonIgnoreAttribute"/> ignores properties on serialization and deserialization.
9+
/// </summary>
10+
public enum JsonIgnoreCondition
11+
{
12+
/// <summary>
13+
/// Property will always be ignored.
14+
/// </summary>
15+
Always = 0,
16+
/// <summary>
17+
/// Property will only be ignored if it is null.
18+
/// </summary>
19+
WhenNull = 1,
20+
/// <summary>
21+
/// Property will always be serialized and deserialized, regardless of <see cref="JsonSerializerOptions.IgnoreNullValues"/> configuration.
22+
/// </summary>
23+
Never = 2
24+
}
25+
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfo.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,10 @@ public JsonClassInfo RuntimeClassInfo
5959
public bool ShouldDeserialize { get; private set; }
6060

6161
public virtual void Initialize(
62-
string matchingPropertyName,
6362
Type declaredPropertyType,
6463
Type runtimePropertyType,
6564
ParameterInfo parameterInfo,
66-
JsonConverter converter,
65+
JsonPropertyInfo matchingProperty,
6766
JsonSerializerOptions options)
6867
{
6968
_runtimePropertyType = runtimePropertyType;
@@ -73,12 +72,12 @@ public virtual void Initialize(
7372
Position = parameterInfo.Position;
7473
ShouldDeserialize = true;
7574

76-
DetermineParameterName(matchingPropertyName);
75+
DetermineParameterName(matchingProperty);
7776
}
7877

79-
private void DetermineParameterName(string matchingPropertyName)
78+
private void DetermineParameterName(JsonPropertyInfo matchingProperty)
8079
{
81-
NameAsString = matchingPropertyName;
80+
NameAsString = matchingProperty.NameAsString!;
8281

8382
// `NameAsString` is valid UTF16, so just call the simple UTF16->UTF8 encoder.
8483
ParameterName = Encoding.UTF8.GetBytes(NameAsString);
@@ -89,16 +88,16 @@ private void DetermineParameterName(string matchingPropertyName)
8988
// Create a parameter that is ignored at run-time. It uses the same type (typeof(sbyte)) to help
9089
// prevent issues with unsupported types and helps ensure we don't accidently (de)serialize it.
9190
public static JsonParameterInfo CreateIgnoredParameterPlaceholder(
92-
string matchingPropertyName,
9391
ParameterInfo parameterInfo,
92+
JsonPropertyInfo matchingProperty,
9493
JsonSerializerOptions options)
9594
{
9695
JsonParameterInfo jsonParameterInfo = new JsonParameterInfo<sbyte>();
9796
jsonParameterInfo.Options = options;
9897
jsonParameterInfo.ParameterInfo = parameterInfo;
9998
jsonParameterInfo.ShouldDeserialize = false;
10099

101-
jsonParameterInfo.DetermineParameterName(matchingPropertyName);
100+
jsonParameterInfo.DetermineParameterName(matchingProperty);
102101

103102
return jsonParameterInfo;
104103
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfoOfT.cs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,29 @@ namespace System.Text.Json
1414
internal class JsonParameterInfo<T> : JsonParameterInfo
1515
{
1616
private JsonConverter<T> _converter = null!;
17+
private bool _ignoreNullValues;
1718
private Type _runtimePropertyType = null!;
1819

1920
public override JsonConverter ConverterBase => _converter;
2021

2122
public T TypedDefaultValue { get; private set; } = default!;
2223

2324
public override void Initialize(
24-
string matchingPropertyName,
2525
Type declaredPropertyType,
2626
Type runtimePropertyType,
2727
ParameterInfo parameterInfo,
28-
JsonConverter converter,
28+
JsonPropertyInfo matchingProperty,
2929
JsonSerializerOptions options)
3030
{
3131
base.Initialize(
32-
matchingPropertyName,
3332
declaredPropertyType,
3433
runtimePropertyType,
3534
parameterInfo,
36-
converter,
35+
matchingProperty,
3736
options);
3837

39-
_converter = (JsonConverter<T>)converter;
38+
_converter = (JsonConverter<T>)matchingProperty.ConverterBase;
39+
_ignoreNullValues = matchingProperty.IgnoreNullValues;
4040
_runtimePropertyType = runtimePropertyType;
4141

4242
if (parameterInfo.HasDefaultValue)
@@ -55,8 +55,7 @@ public override bool ReadJson(ref ReadStack state, ref Utf8JsonReader reader, ou
5555
bool success;
5656
bool isNullToken = reader.TokenType == JsonTokenType.Null;
5757

58-
if (isNullToken &&
59-
((!_converter.HandleNullValue && !state.IsContinuation) || Options.IgnoreNullValues))
58+
if (isNullToken && !_converter.HandleNullValue && !state.IsContinuation)
6059
{
6160
// Don't have to check for IgnoreNullValue option here because we set the default value (likely null) regardless
6261
value = DefaultValue;
@@ -85,10 +84,8 @@ public bool ReadJsonTyped(ref ReadStack state, ref Utf8JsonReader reader, out T
8584
bool success;
8685
bool isNullToken = reader.TokenType == JsonTokenType.Null;
8786

88-
if (isNullToken &&
89-
((!_converter.HandleNullValue && !state.IsContinuation) || Options.IgnoreNullValues))
87+
if (isNullToken && !_converter.HandleNullValue && !state.IsContinuation)
9088
{
91-
// Don't have to check for IgnoreNullValue option here because we set the default value (likely null) regardless
9289
value = TypedDefaultValue;
9390
return true;
9491
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,28 @@ private void DetermineSerializationCapabilities()
118118
}
119119
}
120120

121+
private void DetermineIgnoreCondition()
122+
{
123+
JsonIgnoreAttribute? ignoreAttribute;
124+
if (PropertyInfo != null && (ignoreAttribute = GetAttribute<JsonIgnoreAttribute>(PropertyInfo)) != null)
125+
{
126+
JsonIgnoreCondition condition = ignoreAttribute.Condition;
127+
128+
// We should have created a placeholder property for this upstream and shouldn't be down this code-path.
129+
Debug.Assert(condition != JsonIgnoreCondition.Always);
130+
131+
if (condition != JsonIgnoreCondition.Never)
132+
{
133+
Debug.Assert(condition == JsonIgnoreCondition.WhenNull);
134+
IgnoreNullValues = true;
135+
}
136+
}
137+
else
138+
{
139+
IgnoreNullValues = Options.IgnoreNullValues;
140+
}
141+
}
142+
121143
// The escaped name passed to the writer.
122144
// Use a field here (not a property) to avoid value semantics.
123145
public JsonEncodedText? EscapedName;
@@ -134,7 +156,7 @@ public virtual void GetPolicies()
134156
{
135157
DetermineSerializationCapabilities();
136158
DeterminePropertyName();
137-
IgnoreNullValues = Options.IgnoreNullValues;
159+
DetermineIgnoreCondition();
138160
}
139161

140162
public abstract object? GetValueAsObject(object obj);

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using System.Collections;
65
using System.Collections.Generic;
76
using System.Diagnostics;
87
using System.Runtime.CompilerServices;

src/libraries/System.Text.Json/tests/Serialization/ConstructorTests.ParameterMatching.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -340,20 +340,19 @@ public void PropertiesNotSet_WhenJSON_MapsToConstructorParameters()
340340
[Fact]
341341
public void IgnoreNullValues_DontSetNull_ToConstructorArguments_ThatCantBeNull()
342342
{
343-
// Default is to throw JsonException when null applied to types that can't be null.
343+
// Throw JsonException when null applied to types that can't be null.
344344
Assert.Throws<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""Point3DStruct"":null,""Int"":null,""ImmutableArray"":null}"));
345345
Assert.Throws<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""Point3DStruct"":null}"));
346346
Assert.Throws<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""Int"":null}"));
347347
Assert.Throws<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""ImmutableArray"":null}"));
348348

349-
// Set arguments to default values when IgnoreNullValues is on.
349+
// Throw even when IgnoreNullValues is true for symmetry with property deserialization,
350+
// until https://github.com/dotnet/runtime/issues/30795 is addressed.
350351
var options = new JsonSerializerOptions { IgnoreNullValues = true };
351-
var obj = Serializer.Deserialize<NullArgTester>(@"{""Int"":null,""Point3DStruct"":null,""ImmutableArray"":null}", options);
352-
Assert.Equal(0, obj.Point3DStruct.X);
353-
Assert.Equal(0, obj.Point3DStruct.Y);
354-
Assert.Equal(0, obj.Point3DStruct.Z);
355-
Assert.True(obj.ImmutableArray.IsDefault);
356-
Assert.Equal(50, obj.Int);
352+
Assert.Throws<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""Point3DStruct"":null,""Int"":null,""ImmutableArray"":null}", options));
353+
Assert.Throws<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""Point3DStruct"":null}", options));
354+
Assert.Throws<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""Int"":null}", options));
355+
Assert.Throws<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""ImmutableArray"":null}", options));
357356
}
358357

359358
[Fact]

0 commit comments

Comments
 (0)