Skip to content

Ensure ReadWithNumberHandling() not called for custom converters of numeric-typed collection elements - #42239

Closed
layomia wants to merge 2 commits into
dotnet:mainfrom
layomia:number_handling
Closed

Ensure ReadWithNumberHandling() not called for custom converters of numeric-typed collection elements#42239
layomia wants to merge 2 commits into
dotnet:mainfrom
layomia:number_handling

Conversation

@layomia

Copy link
Copy Markdown
Contributor

Fixes issue in .NET 6.0/master following #41679. The issue is not present in .NET 5.

@layomialayomia added this to the 5.0.0 milestone Sep 15, 2020
@layomialayomia self-assigned this Sep 15, 2020
@layomialayomia changed the title Ensure Read/WriteWithNumberHandling() not called for collection number-elements with custom convertersEnsure ReadWithNumberHandling() not called for collection number-elements with custom convertersSep 15, 2020
@layomialayomia changed the title Ensure ReadWithNumberHandling() not called for collection number-elements with custom convertersEnsure ReadWithNumberHandling() not called for custom converters of numeric-typed collection elementsSep 15, 2020
@jozkee

Copy link
Copy Markdown
Member

Instead of throwing, why not just normally forward the reader into the converter in case that it is not built-in? This implies not calling ReadNumberWithCustomHandling for such case.

Comment threadsrc/libraries/System.Text.Json/src/Resources/Strings.resx Outdated
JsonConverter converter = Options.GetConverter(elementType);
return converter.IsInternalConverterForNumberType;
}
catch (NotSupportedException)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we detect this without having to catch the exception?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm inclined to just call the custom converter and if it doesn't know about quoted numbers, then let it throw what it would have otherwise (likely a reader exception re-thrown as JsonException).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Calling the custom converter is fine with me.

@ahsonkhanahsonkhanSep 22, 2020

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm inclined to just call the custom converter and if it doesn't know about quoted numbers, then let it throw what it would have otherwise (likely a reader exception re-thrown as JsonException).

Why? I haven't fully thought through the scenario or use case, so definitely not pushing back, but just curious what is the benefit to the caller? Could such a behavior be confusing or would it, in fact, match their expectation?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Generally, the expectation when users provide custom converters is that they are called. The exception is when JsonIgnoreCondition.WhenWritingNull/Default/IgnoreNullValues is active and the value is null/default.

The initial concern was that it didn't make sense to specify both custom handling and a custom converter for a type/property since the custom handling would not be honored (by the serializer). Thus, logic was added to throw when both custom handling and a custom handling were provided. The fix would be to remove either the custom number handling or the custom converter. The problem with this is highlighted in #42239 (comment).

The current restriction makes it impossible to have a custom converter for a property on a non-owned type which has number handling specified

The downside to removing the restriction on custom converters is that users can now specify an option which will not be honored. The upside is that we maintain the behavior that a custom converter can take over the (de)serialization of any type, even if parts of the object graph is non-owned.

@ahsonkhanahsonkhanSep 24, 2020

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That makes sense.

The current restriction makes it impossible to have a custom converter for a property on a non-owned type which has number handling specified

This is a great point, which helps motivate the change you are suggesting, for sure. However, doesn't a local attribute have precedence over a global converter?

Have we gotten any customer feedback around this, to validate that the current restriction is problematic?

What is the principles behind when an option is allowed to coincide with converters, and overrides its behavior, and when is it not allowed or ignored, with the converter being prioritized? Can we come up with and articulate a design rationale for why we have certain exceptions (like the ones you listed)? What is common/special about those?

I think it will be valuable to enumerate the design and precedence more broadly, across all options and permutations with converters. Should a local (or maybe even global) custom converter always take precedence, regardless of the other options, aside from converters in higher order of precedence?

Comment threadsrc/libraries/System.Text.Json/tests/Serialization/NumberHandlingTests.cs Outdated
Comment threadsrc/libraries/System.Text.Json/src/Resources/Strings.resx Outdated
@steveharter

steveharter commented Sep 15, 2020

Copy link
Copy Markdown
Contributor

If someone has a custom converter for a number and wants to manually support quoted numbers (by reading or writing JsonTokenType.String) is this possible (with the new number handling option set), or is this now prevented with InvalidOperationException? Since it was possible in the past to do this (AFAIK), we shouldn't close that option IMO just because the new number handling feature is turned on for non-custom converters.

This means that a custom converter for an Int32, for example, would throw if it doesn't support or know about quoted. Likely the exception would be thrown by the reader when the custom converter calls reader.GetInt32() which will be re-thrown as JsonException.

@ahsonkhan

Copy link
Copy Markdown
Contributor

The issue is not present in .NET 5.

So, is this PR for the 6.0 milestone?

@layomialayomia modified the milestones: 5.0.0, 6.0.0Sep 17, 2020
@layomia

Copy link
Copy Markdown
ContributorAuthor

Yes @ahsonkhan, for 6.0.

@layomia

layomia commented Sep 20, 2020

Copy link
Copy Markdown
ContributorAuthor

If someone has a custom converter for a number and wants to manually support quoted numbers (by reading or writing JsonTokenType.String) is this possible (with the new number handling option set), or is this now prevented with InvalidOperationException? Since it was possible in the past to do this (AFAIK), we shouldn't close that option IMO just because the new number handling feature is turned on for non-custom converters.

This means that a custom converter for an Int32, for example, would throw if it doesn't support or know about quoted. Likely the exception would be thrown by the reader when the custom converter calls reader.GetInt32() which will be re-thrown as JsonException.

We were throwing InvalidOperationException when a property or type had JsonNumberHandlingAttribute and there was also a custom converter. The exception would not be thrown when there was no `JsonNumberHandlingAttribute`` on the type/property but there was a converter and number handling specified globally on the options instance.

The current restriction makes it impossible to have a custom converter for a property on a non-owned type which has number handling specified:

publicclassNonOwnedType{[JsonNumberHandling(JsonNumberHandling.WriteAsString)]publicintNum{get;set;}}JsonSerializer.Serialize(newNonOwnedType());// Works okay: {"Num":"0"}varoptions=newJsonSerializerOptions{Converters=newCustomIntConverter()};// InvalidOperationException thrown at warm up without workaround except type owner removing attribute on `Num` property,// or using custom converter for `NonOwnedType` itself.JsonSerializer.Serialize(newNonOwnedType(),options);

I'm not sure that this will be a popular scenario that we have to fix for 5.0.

I agree we should remove restrictions with custom converters and this feature. If both custom number handling and a custom converter are provided, we'll just call the converters regular Read and Write method, and the converter is free to do whatever. In the future we can provide a way to inspect per-property handling in custom converters, alongside other per-property features. The new commit updates the logic and tests appropriately.

@stevehartersteveharter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM with the converter restrictions removed.

@ericstj

Copy link
Copy Markdown
Member

@layomia is this PR still relevant?

@layomia

Copy link
Copy Markdown
ContributorAuthor

@ericstj yes - I'll finish this PR and merge it.

Base automatically changed from master to mainMarch 1, 2021 09:07
@layomia

Copy link
Copy Markdown
ContributorAuthor

Will circle back to this and merge it.

@layomialayomia closed this Mar 10, 2021
@ghostghost locked as resolved and limited conversation to collaborators Apr 9, 2021
@layomia
layomia deleted the number_handling branch May 18, 2021 07:03
@layomia
layomia restored the number_handling branch July 27, 2021 03:28
@layomialayomia reopened this Jul 27, 2021
@layomialayomia closed this Aug 3, 2021
Sign up for freeto 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.

6 participants

@layomia@jozkee@steveharter@ahsonkhan@ericstj@eiriktsarpalis