Skip to content

Commit

Permalink
Add JsonIgnoreCondition & per-property ignore logic (dotnet#34049)
Browse files Browse the repository at this point in the history
* Add JsonIgnoreCondition & per-property ignore logic

* Address review feedback
  • Loading branch information
layomia authored Mar 26, 2020
1 parent d8f763e commit c653291
Show file tree
Hide file tree
Showing 12 changed files with 413 additions and 36 deletions.
7 changes: 7 additions & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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() { }
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
<Compile Include="System\Text\Json\Serialization\JsonDictionaryConverter.cs" />
<Compile Include="System\Text\Json\Serialization\JsonExtensionDataAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\JsonIgnoreAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\JsonIgnoreCondition.cs" />
<Compile Include="System\Text\Json\Serialization\JsonNamingPolicy.cs" />
<Compile Include="System\Text\Json\Serialization\JsonObjectConverter.cs" />
<Compile Include="System\Text\Json\Serialization\JsonParameterInfo.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ public Dictionary<string, JsonParameterInfo> CreateParameterCache(int capacity,

public static JsonPropertyInfo AddProperty(Type propertyType, PropertyInfo propertyInfo, Type parentClassType, JsonSerializerOptions options)
{
bool hasIgnoreAttribute = (JsonPropertyInfo.GetAttribute<JsonIgnoreAttribute>(propertyInfo) != null);
if (hasIgnoreAttribute)
JsonIgnoreAttribute? ignoreAttribute = JsonPropertyInfo.GetAttribute<JsonIgnoreAttribute>(propertyInfo);
if (ignoreAttribute?.Condition == JsonIgnoreCondition.Always)
{
return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(propertyInfo, options);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ namespace System.Text.Json.Serialization
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class JsonIgnoreAttribute : JsonAttribute
{
/// <summary>
/// Specifies the condition that must be met before a property will be ignored.
/// </summary>
/// <remarks>The default value is <see cref="JsonIgnoreCondition.Always"/>.</remarks>
public JsonIgnoreCondition Condition { get; set; } = JsonIgnoreCondition.Always;

/// <summary>
/// Initializes a new instance of <see cref="JsonIgnoreAttribute"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Controls how the <see cref="JsonIgnoreAttribute"/> ignores properties on serialization and deserialization.
/// </summary>
public enum JsonIgnoreCondition
{
/// <summary>
/// Property will always be ignored.
/// </summary>
Always = 0,
/// <summary>
/// Property will only be ignored if it is null.
/// </summary>
WhenNull = 1,
/// <summary>
/// Property will always be serialized and deserialized, regardless of <see cref="JsonSerializerOptions.IgnoreNullValues"/> configuration.
/// </summary>
Never = 2
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -89,16 +88,16 @@ 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<sbyte>();
jsonParameterInfo.Options = options;
jsonParameterInfo.ParameterInfo = parameterInfo;
jsonParameterInfo.ShouldDeserialize = false;

jsonParameterInfo.DetermineParameterName(matchingPropertyName);
jsonParameterInfo.DetermineParameterName(matchingProperty);

return jsonParameterInfo;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,29 @@ namespace System.Text.Json
internal class JsonParameterInfo<T> : JsonParameterInfo
{
private JsonConverter<T> _converter = null!;
private bool _ignoreNullValues;
private Type _runtimePropertyType = null!;

public override JsonConverter ConverterBase => _converter;

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<T>)converter;
_converter = (JsonConverter<T>)matchingProperty.ConverterBase;
_ignoreNullValues = matchingProperty.IgnoreNullValues;
_runtimePropertyType = runtimePropertyType;

if (parameterInfo.HasDefaultValue)
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,28 @@ private void DetermineSerializationCapabilities()
}
}

private void DetermineIgnoreCondition()
{
JsonIgnoreAttribute? ignoreAttribute;
if (PropertyInfo != null && (ignoreAttribute = GetAttribute<JsonIgnoreAttribute>(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;
Expand All @@ -134,7 +156,7 @@ public virtual void GetPolicies()
{
DetermineSerializationCapabilities();
DeterminePropertyName();
IgnoreNullValues = Options.IgnoreNullValues;
DetermineIgnoreCondition();
}

public abstract object? GetValueAsObject(object obj);
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.
// 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""Point3DStruct"":null,""Int"":null,""ImmutableArray"":null}"));
Assert.Throws<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""Point3DStruct"":null}"));
Assert.Throws<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""Int"":null}"));
Assert.Throws<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""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<NullArgTester>(@"{""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<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""Point3DStruct"":null,""Int"":null,""ImmutableArray"":null}", options));
Assert.Throws<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""Point3DStruct"":null}", options));
Assert.Throws<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""Int"":null}", options));
Assert.Throws<JsonException>(() => Serializer.Deserialize<NullArgTester>(@"{""ImmutableArray"":null}", options));
}

[Fact]
Expand Down
Loading

0 comments on commit c653291

Please sign in to comment.