Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Set values of array and immutable collection properties directly #42420

Merged
merged 2 commits into from
Nov 13, 2019

Conversation

layomia
Copy link
Contributor

@layomia layomia commented Nov 6, 2019

When deserializing, the serializer is throwing a NotSupportedException whenever we see an array or immutable collection (non-dictionary) property which isn't initally null. See https://dotnetfiddle.net/hdhEDG.

Arrays and immutable collections are deserialized using internal converters that create new instances populated the JSON array.
This issue doesn't apply to dictionaries, or ClassType.Enumerable types where we don't use converters:

else if (state.Current.IsProcessingProperty(ClassType.Enumerable))
{
// We added the items to the list already.
state.Current.EndProperty();
return false;
}
.

@layomia layomia added this to the 5.0 milestone Nov 6, 2019
@layomia layomia self-assigned this Nov 6, 2019
if (currentEnumerable == null ||
// ImmutableArray<T> is a struct, so default value won't be null.
jsonPropertyInfo.IsImmutableArray)
if (currentEnumerable == null)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

ImmutableArray<T> will no longer come down this path.

Choose a reason for hiding this comment

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

Can you please explain why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because it's immutable, we use a converter then we set the property directly as highlighted in https://github.com/dotnet/corefx/pull/42420/files#r345507426.

if (!jsonPropertyInfo.IsPropertyPolicy &&
!state.Current.JsonPropertyInfo.IsImmutableArray)
// Clear the value if present to ensure we don't confuse TempEnumerableValues with the collection.
if (!jsonPropertyInfo.IsPropertyPolicy && jsonPropertyInfo.CanBeNull)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We shouldn't attempt to set any value-type enumerable to null, not just ImmutableArray. Same below.

Given the intention to set the collection to null, there might be an issue with this check since value-type collections are excluded and this isn't accounted for anywhere. I'll dig into this as part of https://github.com/dotnet/corefx/issues/42399.

Copy link
Member

Choose a reason for hiding this comment

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

I assume we want to throw when we see a null literal in a JSON array and the corresponding CLR element type is a value type. Wouldn't we just throw JsonException if CanBeNull=true here?

Copy link
Contributor Author

@layomia layomia Nov 12, 2019

Choose a reason for hiding this comment

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

I assume we want to throw when we see a null literal in a JSON array and the corresponding CLR element type is a value type.

Yes, that's what we should (and currently) do.

In this area of the code, we are attempting to clear the collection property by setting it to null, so that we don't get confused in our null checks to state.Current.Temp[Dictionary|Enumerable]Values (are we using a collection converter?) later on. The jsonPropertyInfo.CanBeNull clause of this check guards against attempting to set a value type collection (e.g. ImmutableArray which is a struct) to null which I believe will give an InvalidCastException.
This means that we don't clear value type collections (these collections seem edge-casey and one-off in the BCL - I only know of ImmutableArray).

With the inital comment, I was noting that this may be a bug since the intention was to clear all collections, and that I plan to look into this in a future PR for https://github.com/dotnet/corefx/issues/42399. Maybe we don't actually need to clear the collection here and should just make sure our state.Current.Temp[Dictionary|Enumerable]Values checks are correct/vigilant without this "clearing" dependency.

The issue for https://github.com/dotnet/corefx/issues/42399 is that we should throw JsonException when we see null where we expect a JSON array for a value type collection, e.g. here.

Copy link

@ahsonkhan ahsonkhan Nov 12, 2019

Choose a reason for hiding this comment

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

corresponding CLR element type is a value type.

I would imagine this shouldn't be true if it's a nullable value type (int?, DateTime?, etc.). What's the behavior for that case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What's the behavior for that case?

Those work as expected. We execute tests for this via:

Copy link

@ahsonkhan ahsonkhan Nov 13, 2019

Choose a reason for hiding this comment

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

I was thinking about nullable value types as the T of the collections like immutable array, regular array. So DateTime?[] or ImmutableArray<DateTime?>. Do these work as expected too?

As an aside, looks like nullable ImmutableArray breaks (https://dotnetfiddle.net/14qZfX):

Console.WriteLine(JsonSerializer.Deserialize<ImmutableArray<DateTime>?>("null") == null);
Unhandled exception. System.ArgumentNullException: Value cannot be null.
   at System.RuntimeType.MakeGenericType(Type[] instantiation)

Copy link
Contributor Author

@layomia layomia Nov 13, 2019

Choose a reason for hiding this comment

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

I was thinking about nullable value types as the T of the collections like immutable array, regular array. So DateTime?[] or `ImmutableArray<DateTime?>. Do these work as expected too?

Yes we have tests for these:

public static void DictionaryWithNullableValue()

public static void EnumerableWithNullableValue()

Some for array of T?:

public short?[] MyInt16Array { get; set; }
public int?[] MyInt32Array { get; set; }
public long?[] MyInt64Array { get; set; }
public ushort?[] MyUInt16Array { get; set; }
public uint?[] MyUInt32Array { get; set; }
public ulong?[] MyUInt64Array { get; set; }
public byte?[] MyByteArray { get; set; }
public sbyte?[] MySByteArray { get; set; }
public char?[] MyCharArray { get; set; }
public decimal?[] MyDecimalArray { get; set; }
public bool?[] MyBooleanTrueArray { get; set; }
public bool?[] MyBooleanFalseArray { get; set; }
public float?[] MySingleArray { get; set; }
public double?[] MyDoubleArray { get; set; }
public DateTime?[] MyDateTimeArray { get; set; }
public DateTimeOffset?[] MyDateTimeOffsetArray { get; set; }
public Guid?[] MyGuidArray { get; set; }
public SampleEnum?[] MyEnumArray { get; set; }

As an aside, looks like nullable ImmutableArray breaks (https://dotnetfiddle.net/14qZfX)

Yes, this is an extension of the known issue with assigning JSON null to value-type collections: https://github.com/dotnet/corefx/issues/42399#issuecomment-553190419.

Copy link
Member

@ericstj ericstj left a comment

Choose a reason for hiding this comment

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

LGTM but you should get either @steveharter or @ahsonkhan to sign off.

@@ -102,19 +102,16 @@ public static void RegisterImmutableCollection(Type immutableCollectionType, Typ
options.TryAddCreateRangeDelegate(delegateKey, createRangeDelegate);
}

public static bool IsImmutableEnumerable(Type type, out bool IsImmutableArray)

Choose a reason for hiding this comment

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

Why did we have to special case ImmutableArray initially?

Copy link
Contributor Author

@layomia layomia Nov 13, 2019

Choose a reason for hiding this comment

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

There are places in the code where we perform null checks on collections. Since value-types can't be null and the only "known" value type collection was ImmutableArray, it was special cased. The changes in this PR use the more appropriate and general JsonPropertyInfo.CanBeNull property which makes sure "unknown" value collection types are accounted for as well.

@@ -132,7 +140,7 @@ private static void HandleStartArray(JsonSerializerOptions options, ref ReadStac
state.Pop();
}

ApplyObjectToEnumerable(value, ref state);
ApplyObjectToEnumerable(value, ref state, setPropertyDirectly);

Choose a reason for hiding this comment

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

With this change, if state.Current.IsProcessingProperty(ClassType.Enumerable) is true, i.e. setPropertyDirectly is true, will we jump to this code path in ApplyObjectToEnumerable?

else
{
Debug.Assert(state.Current.JsonPropertyInfo != null);
state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, value);
}

Why do we need setPropertyDirectly in ApplyObjectToEnumerable but not in ApplyValueToEnumerable?

Copy link
Contributor Author

@layomia layomia Nov 13, 2019

Choose a reason for hiding this comment

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

With this change, if state.Current.IsProcessingProperty(ClassType.Enumerable) is true, i.e. setPropertyDirectly is true, will we jump to this code path in ApplyObjectToEnumerable?

Yes.

Why do we need setPropertyDirectly in ApplyObjectToEnumerable but not in ApplyValueToEnumerable?

We probably need such semantics in ApplyValueToEnumerable to fix issues such as https://github.com/dotnet/corefx/issues/41598 and https://github.com/dotnet/corefx/issues/42399.
Also, the two methods both add elements to collections, and set populated collections to class properties. There should be a separation of these responsibilities, and I'll dig deeper into this to fix the afforementioned issues.

if (currentEnumerable == null ||
// ImmutableArray<T> is a struct, so default value won't be null.
jsonPropertyInfo.IsImmutableArray)
if (currentEnumerable == null)

Choose a reason for hiding this comment

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

Can you please explain why?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants