Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider not throwing exceptions when deserializing unknown enum values #90

Closed
martincostello opened this issue Jul 7, 2022 · 5 comments · Fixed by #98
Closed

Consider not throwing exceptions when deserializing unknown enum values #90

martincostello opened this issue Jul 7, 2022 · 5 comments · Fixed by #98
Labels
Type: Feature New feature or request

Comments

@martincostello
Copy link
Contributor

Describe the feature

There have been a number of times where drift between the schema definitions and/or this library and the actual runtime behaviour of github.com/GitHub Enterprise Server cause runtime failures in applications consuming the library to process webhooks. Examples include #52, #76 and #89.

To make consuming applications more resilient to natural evolution of the service over time, it would be preferable if enum deserialization is more tolerant to undefined values (this could be opt-in).

This is particularly impactful with github.com as the payloads could change at any time without advance notice to the owner of an integration due to a new deployment/feature on the server. In contrast, upgrades to GitHub Enterprise Server are more predictable and can be planned for in advance as the appliance is typically upgraded through a specific decision on the part of its operators.

@JamieMagee
Copy link
Contributor

JamieMagee commented Jul 7, 2022

I'd split those issues into two categories:

  1. GitHub has started sending a new event
  2. I've tried to tighten up the types and got it wrong

Do you know of a way to handle this more gracefully in System.Text.Json? I know in Newtonsoft.Json there the OnErrorAttribute1. There's currently an open issue2 for better error handling in System.Text.Json. Is the best thing to do just wrap deserialization in a try/catch?

Footnotes

  1. https://www.newtonsoft.com/json/help/html/serializationerrorhandling.htm#OnErrorAttribute

  2. https://github.com/dotnet/runtime/issues/38049

@martincostello
Copy link
Contributor Author

Having a dig around, it looks like the JsonStringEnumMemberConverter converter that's brought in from Macross.Json.Extensions has some support for this built in a few different ways.

You can specify a fallback value on each enum using [JsonStringEnumMemberConverterOptions].

[PublicAPI]
[JsonConverter(typeof(JsonStringEnumMemberConverter))]
[JsonStringEnumMemberConverterOptions(deserializationFailureFallbackValue: -1)]
public enum HookType
{
    [EnumMember(Value = "Repository")]
    Repository,
    [EnumMember(Value = "Organization")]
    Organization,
    [EnumMember(Value = "App")]
    App,
}

Another way is to create a derived JsonStringEnumMemberConverter that specifies the fallback in code .

[PublicAPI]
[JsonConverter(typeof(JsonStringEnumMemberConverterWithFallback))]
public enum HookType
{
    [EnumMember(Value = "Repository")]
    Repository,
    [EnumMember(Value = "Organization")]
    Organization,
    [EnumMember(Value = "App")]
    App,
}

public sealed class JsonStringEnumMemberConverterWithFallback : JsonStringEnumMemberConverter
{
    private static readonly JsonStringEnumMemberConverterOptions Options = new()
    {
        DeserializationFailureFallbackValue = -1,
    };

    public JsonStringEnumMemberConverterWithFallback()
        : base(Options)
    {
    }
}

Given that, one approach could be to use a custom implementation of it that uses -1 to signify an invalid value, then unknown values would only affect code that tried to use something that was affected. It would stop the serializer failing, but anything reading the specific webhook event property that received that invalid value would do whatever the user code does with it. The upside to that is the user's code doesn't use a given event property, the changes remain invisible to them. The downside is that the user's code might not handle an undefined value in a nice way if it's started to run once it's been deserialized.

Another approach entirely (but a big breaking change) could be to type enums like octokit.net does by wrapping them with the StringEnum<TEnum> struct. That would preserve the raw string values so the serializer wouldn't complain but would throw an exception when the value was read if parsing it fails. This approach would still break apps if they get new values in the payload if they access .Value, but an app would have the option of using .StringValue or TryParse() to make a conscious decision per usage on what to do if an unknown value is encountered.

Option 1 seems more light-touch, but isn't really customisable. Option 2 is more flexible for users, but would likely need code fix-ups to adopt in an existing application due to the binary-breaking changes.

@JamieMagee
Copy link
Contributor

I think that option 1 would be good for a short-term fix. We can do that in a 1.x release. I agree that option 2 is the better long term solution. We can do that in a 2.x release. It might be a good place to release the Azure Functions package as well.

What do you think?

@martincostello
Copy link
Contributor Author

That sounds reasonable to me.

I'll have a test of approach 1 and see how it handles things. If it looks viable I'll push up a draft PR for you to take a look at.

martincostello added a commit to martincostello/webhooks.net that referenced this issue Jul 8, 2022
Add fallback value for unknown enumeration values.
Relates to octokit#90.
@JamieMagee
Copy link
Contributor

I've started working on option 2 in the feat/string-enum branch

martincostello added a commit to martincostello/webhooks.net that referenced this issue Jul 20, 2022
- Add an `Unknown` value to the `ReviewState` enum as it wasn't accounted for in octokit#90 before it was merged.
- Add unit tests to catch future missing `Unknown` values.
JamieMagee pushed a commit that referenced this issue Jul 20, 2022
- Add an `Unknown` value to the `ReviewState` enum as it wasn't accounted for in #90 before it was merged.
- Add unit tests to catch future missing `Unknown` values.
@nickfloyd nickfloyd added Priority: High Type: Feature New feature or request and removed type:feature labels Oct 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Feature New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants