Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults defaults) {
public bool AllowTrailingCommas { get { throw null; } set { } }
public System.Collections.Generic.IList<System.Text.Json.Serialization.JsonConverter> Converters { get { throw null; } }
public int DefaultBufferSize { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonIgnoreCondition DefaultIgnoreCondition { get { throw null; } set { } }
public System.Text.Json.JsonNamingPolicy? DictionaryKeyPolicy { get { throw null; } set { } }
public System.Text.Encodings.Web.JavaScriptEncoder? Encoder { get { throw null; } set { } }
public bool IgnoreNullValues { get { throw null; } set { } }
Expand Down Expand Up @@ -465,9 +466,9 @@ namespace System.Text.Json.Serialization
{
public enum JsonIgnoreCondition
{
Always = 0,
WhenNull = 1,
Never = 2,
Never = 0,
Always = 1,
WhenWritingDefault = 2,
}
public abstract partial class JsonAttribute : System.Attribute
{
Expand Down
6 changes: 6 additions & 0 deletions src/libraries/System.Text.Json/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -521,4 +521,10 @@
<data name="CannotPopulateCollection" xml:space="preserve">
<value>The collection type '{0}' is abstract, an interface, or is read only, and could not be instantiated and populated.</value>
</data>
<data name="DefaultIgnoreConditionAlreadySpecified" xml:space="preserve">
<value>'IgnoreNullValues' and 'DefaultIgnoreCondition' cannot both be set to non-default values.</value>
</data>
<data name="DefaultIgnoreConditionInvalid" xml:space="preserve">
<value>The value cannot be 'JsonIgnoreCondition.Always'.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo

if (dataExtKey == null)
{
jsonPropertyInfo.SetValueAsObject(obj, propValue);
jsonPropertyInfo.SetExtensionDictionaryAsObject(obj, propValue);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// 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.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

Expand Down Expand Up @@ -74,6 +75,8 @@ internal override sealed JsonParameterInfo CreateJsonParameterInfo()
/// </summary>
internal bool IsInternalConverter { get; set; }

internal readonly EqualityComparer<T> _defaultComparer = EqualityComparer<T>.Default;

// This non-generic API is sealed as it just forwards to the generic version.
internal sealed override bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,24 @@
namespace System.Text.Json.Serialization
{
/// <summary>
/// Controls how the <see cref="JsonIgnoreAttribute"/> ignores properties on serialization and deserialization.
/// When specified on <see cref="JsonSerializerOptions.DefaultIgnoreCondition"/>,
/// <see cref="WhenWritingDefault"/> specifies that properties with default values are ignored during serialization.
/// When specified on <see cref="JsonIgnoreAttribute.Condition"/>, controls whether
/// a property is ignored during serialization and deserialization.
/// </summary>
public enum JsonIgnoreCondition
{
/// <summary>
/// Property will always be ignored.
/// Property is never ignored during serialization or deserialization.
/// </summary>
Always = 0,
Never = 0,
/// <summary>
/// Property will only be ignored if it is null.
/// Property is always ignored during serialization and deserialization.
/// </summary>
WhenNull = 1,
Always = 1,
/// <summary>
/// Property will always be serialized and deserialized, regardless of <see cref="JsonSerializerOptions.IgnoreNullValues"/> configuration.
/// If the value is the default, the property is ignored during serialization.
/// </summary>
Never = 2
WhenWritingDefault = 2,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,23 @@ private void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition)

if (ignoreCondition != JsonIgnoreCondition.Never)
{
Debug.Assert(ignoreCondition == JsonIgnoreCondition.WhenNull);
IgnoreNullValues = true;
Debug.Assert(ignoreCondition == JsonIgnoreCondition.WhenWritingDefault);
IgnoreDefaultValuesOnWrite = true;
}
}
else
#pragma warning disable CS0618 // IgnoreNullValues is obsolete
else if (Options.IgnoreNullValues)
{
Debug.Assert(Options.DefaultIgnoreCondition == JsonIgnoreCondition.Never);
IgnoreDefaultValuesOnRead = true;
IgnoreDefaultValuesOnWrite = true;
}
else if (Options.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingDefault)
{
IgnoreNullValues = Options.IgnoreNullValues;
Debug.Assert(!Options.IgnoreNullValues);
IgnoreDefaultValuesOnWrite = true;
}
#pragma warning restore CS0618 // IgnoreNullValues is obsolete
}

public static TAttribute? GetAttribute<TAttribute>(PropertyInfo propertyInfo) where TAttribute : Attribute
Expand Down Expand Up @@ -183,7 +192,8 @@ public virtual void Initialize(
Options = options;
}

public bool IgnoreNullValues { get; private set; }
public bool IgnoreDefaultValuesOnRead { get; private set; }
public bool IgnoreDefaultValuesOnWrite { get; private set; }

public bool IsPropertyPolicy { get; protected set; }

Expand Down Expand Up @@ -307,7 +317,7 @@ public JsonClassInfo RuntimeClassInfo

public Type? RuntimePropertyType { get; private set; } = null;

public abstract void SetValueAsObject(object obj, object? value);
public abstract void SetExtensionDictionaryAsObject(object obj, object? extensionDict);

public bool ShouldSerialize { get; private set; }
public bool ShouldDeserialize { get; private set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;

namespace System.Text.Json
Expand All @@ -15,6 +16,8 @@ namespace System.Text.Json
/// or a type's converter, if the current instance is a <see cref="JsonClassInfo.PropertyInfoForClassInfo"/>.
internal sealed class JsonPropertyInfo<T> : JsonPropertyInfo
{
private static readonly T s_defaultValue = default!;

public Func<object, T>? Get { get; private set; }
public Action<object, T>? Set { get; private set; }

Expand Down Expand Up @@ -98,16 +101,23 @@ public override bool GetMemberAndWriteJson(object obj, ref WriteStack state, Utf

bool success;
T value = Get!(obj);

if (value == null)
{
if (!IgnoreNullValues)
Debug.Assert(s_defaultValue == null && Converter.CanBeNull);

success = true;
if (!IgnoreDefaultValuesOnWrite)
{
if (!Converter.HandleNull)
{
writer.WriteNull(EscapedName.Value);
}
else
{
// No object, collection, or re-entrancy converter handles null.
Debug.Assert(Converter.ClassType == ClassType.Value);

if (state.Current.PropertyState < StackFramePropertyState.Name)
{
state.Current.PropertyState = StackFramePropertyState.Name;
Expand All @@ -122,7 +132,10 @@ public override bool GetMemberAndWriteJson(object obj, ref WriteStack state, Utf
}
}
}

}
else if (IgnoreDefaultValuesOnWrite && Converter._defaultComparer.Equals(s_defaultValue, value))
{
Debug.Assert(s_defaultValue != null && !Converter.CanBeNull);
success = true;
}
else
Expand Down Expand Up @@ -159,45 +172,46 @@ public override bool GetMemberAndWriteJsonExtensionData(object obj, ref WriteSta
public override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref Utf8JsonReader reader)
{
bool success;

bool isNullToken = reader.TokenType == JsonTokenType.Null;
if (isNullToken && !Converter.HandleNull && !state.IsContinuation)
{
if (!IgnoreNullValues)
if (!Converter.CanBeNull)
{
if (!Converter.CanBeNull)
{
ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Converter.TypeToConvert);
}
ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Converter.TypeToConvert);
}

Debug.Assert(s_defaultValue == null);

if (!IgnoreDefaultValuesOnRead)
{
T value = default;
Set!(obj, value!);
}

success = true;
}
else
else if (Converter.CanUseDirectReadOrWrite)
{
// Get the value from the converter and set the property.
if (Converter.CanUseDirectReadOrWrite)
if (!(isNullToken && IgnoreDefaultValuesOnRead && Converter.CanBeNull))
{
// Optimize for internal converters by avoiding the extra call to TryRead.
T fastvalue = Converter.Read(ref reader, RuntimePropertyType!, Options);
if (!IgnoreNullValues || (!isNullToken && fastvalue != null))
{
Set!(obj, fastvalue!);
}

return true;
Set!(obj, fastvalue!);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semantics here changed when using IgnoreNullValues. Previously, the setter wasn't called when the value is null.

If we keep this, we should document it as a breaking change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep this and document the change. The only way for null to be set here is if the token type was not JsonTokenType.Null and the converter returned null for some reason. I've added a test to express this behavior.

AFAIK, IgnoreNullValues means if the token type for the property is JsonTokenType.Null when deserializing, and the type to deserialize can be null, then the serializer should skip it. We still honor this contract.

}
else

success = true;
}
else
{
success = true;

if (!(isNullToken && IgnoreDefaultValuesOnRead && Converter.CanBeNull))
{
success = Converter.TryRead(ref reader, RuntimePropertyType!, Options, ref state, out T value);
if (success)
{
if (!IgnoreNullValues || (!isNullToken && value != null))
{
Set!(obj, value!);
}
Set!(obj, value!);
}
}
}
Expand Down Expand Up @@ -225,7 +239,7 @@ public override bool ReadJsonAsObject(ref ReadStack state, ref Utf8JsonReader re
if (Converter.CanUseDirectReadOrWrite)
{
value = Converter.Read(ref reader, RuntimePropertyType!, Options);
return true;
success = true;
}
else
{
Expand All @@ -237,15 +251,11 @@ public override bool ReadJsonAsObject(ref ReadStack state, ref Utf8JsonReader re
return success;
}

public override void SetValueAsObject(object obj, object? value)
public override void SetExtensionDictionaryAsObject(object obj, object? extensionDict)
{
Debug.Assert(HasSetter);
T typedValue = (T)value!;

if (typedValue != null || !IgnoreNullValues)
{
Set!(obj, typedValue);
}
T typedValue = (T)extensionDict!;
Set!(obj, typedValue);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializer
TryWrite(writer, value, options, ref state);
}

public override bool HandleNull => false;
public sealed override bool HandleNull => false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ internal static void CreateDataExtensionProperty(
}

extensionData = jsonPropertyInfo.RuntimeClassInfo.CreateObject();
jsonPropertyInfo.SetValueAsObject(obj, extensionData);
jsonPropertyInfo.SetExtensionDictionaryAsObject(obj, extensionData);
}

// We don't add the value to the dictionary here because we need to support the read-ahead functionality for Streams.
Expand Down
Loading