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

Unseal JsonStringEnumConverter #30486

Closed
josundt opened this issue Aug 5, 2019 · 13 comments · Fixed by #54917
Closed

Unseal JsonStringEnumConverter #30486

josundt opened this issue Aug 5, 2019 · 13 comments · Fixed by #54917
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Text.Json enhancement Product code improvement that does NOT require public API changes/additions
Milestone

Comments

@josundt
Copy link

josundt commented Aug 5, 2019

Background and Motivation

JsonStringEnumConverter can be configured using a couple of parameters. But since the type is marked sealed, it can be awkward to configure when applied via the JsonConverter attribute, leading to awkward workarounds like this one.

API Proposal

namespace System.Text.Json.Serialization
{
-    public sealed class JsonStringEnumConverter : JsonConverterFactory
+    public class JsonStringEnumConverter : JsonConverterFactory
    {
        public JsonStringEnumConverter() { }
        public JsonStringEnumConverter(JsonNamingPolicy? namingPolicy = null, bool allowIntegerValues = true) { }
-        public override bool CanConvert(Type typeToConvert) { throw null; }
+        public sealed override bool CanConvert(Type typeToConvert) { throw null; }
-        public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { throw null; }
+        public sealed override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { throw null; }
    }
}

Risks

Unsealing public types is generally not considered a breaking change. It might break reflection in the hypothetical scenario where a user depends on the result of typeof(JsonStringEnumConverter).IsSealed.

Original proposal by @josundt (click to view) In our APIs, we serialize most enums as strings (no case conversion) since string enums make APIs more self-explanatory, but for **flag enums** we serialize with the numeric value since strings would prevent bitwise flag enum operations.

With NewtonSoft.Json, we accomplished the described serialization behavior by creating our own converter that inherited from NewtonSoft's StringEnumConverter.

This would have been possible and even easier with System.Text.Json, if only the JsonStringEnumConverter class was not sealed.

If it was not sealed, I could have done this simply by overriding the CanConvert method like below:

using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace MyNamespace
{
    public class JsonStringEnumNumericFlagConverter : JsonStringEnumConverter
    {
        public JsonStringEnumNumericFlagConverter()
            : base() {}

        public JsonStringEnumNumericFlagConverter(JsonNamingPolicy namingPolicy = null, bool allowIntegerValues = true)
            : base(namingPolicy, allowIntegerValues) {}

        public override bool CanConvert(Type typeToConvert)
        {
            return typeToConvert.IsEnum
                && typeToConvert.GetCustomAttributes(typeof(FlagsAttribute), false).Length == 0;
        }
    }
}

Can you please make this extensibility point available ("un-sealing" the class)?

@ahsonkhan
Copy link
Member

ahsonkhan commented Aug 5, 2019

cc @steveharter, @JeremyKuhne - why was this type sealed? Is the scenario with enum flags not meant to be supported via JsonStringEnumConverter?

@JeremyKuhne
Copy link
Member

This was done for performance (and future implementation flexibility). We changed the APIs explicitly so you can compose it if you want to for this exact scenario. dotnet/corefx#39438 There is a specific example in the tests that shows you how to compose.

@ahsonkhan
Copy link
Member

ahsonkhan commented Aug 5, 2019

Thanks @JeremyKuhne, since there is a viable workaround, marking this as future.

https://github.com/dotnet/corefx/blob/7d905389f94e52c284d985e7bf3158459b30bccb/src/System.Text.Json/tests/Serialization/EnumConverterTests.cs#L158-L167

@josundt, does composition like this work for your scenario (rather than inheritance)?

@josundt
Copy link
Author

josundt commented Aug 6, 2019

Seems to be good enough for my requirements.
A limitation with the example above: Since the private JsonStringEnumConverter is static, it uses the default options that comes with the default constructor.
Would it be bad to make it non static and initialize like this instead?

public class NoFlagsStringEnumConverter : JsonConverterFactory
 {
     private JsonStringEnumConverter _stringEnumConverter;

     public NoFlagsStringEnumConverter()
        : this(namingPolicy: null, allowIntegerValues: true)
     {
        // An empty constructor is needed for construction via attributes
     }

     public NoFlagsStringEnumConverter(JsonNamingPolicy namingPolicy = null, bool allowIntegerValues = true)
     {
         this._stringEnumConverter = new JsonStringEnumConverter(namingPolicy, allowIntegerValues);
     }

     public override bool CanConvert(Type typeToConvert)
         => typeToConvert.IsEnum && !typeToConvert.IsDefined(typeof(FlagsAttribute), inherit: false);

     public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
         => s_stringEnumConverter.CreateConverter(typeToConvert, options);
 }

@JeremyKuhne
Copy link
Member

Would it be bad to make it non static and initialize like this instead?

That would be fine.

@layomia
Copy link
Contributor

layomia commented Nov 23, 2019

@msftgits msftgits transferred this issue from dotnet/corefx Feb 1, 2020
@msftgits msftgits added this to the Future milestone Feb 1, 2020
@layomia
Copy link
Contributor

layomia commented Feb 21, 2020

Closing - there's a workaround.

@layomia layomia closed this as completed Feb 21, 2020
@dotnet dotnet locked as resolved and limited conversation to collaborators Dec 12, 2020
@eiriktsarpalis eiriktsarpalis self-assigned this Jun 28, 2021
@eiriktsarpalis eiriktsarpalis modified the milestones: Future, 6.0.0 Jun 28, 2021
@eiriktsarpalis eiriktsarpalis changed the title Make JsonStringEnumConverter non-sealed Unseal JsonStringEnumConverter Jun 28, 2021
@eiriktsarpalis eiriktsarpalis added the api-ready-for-review API is ready for review, it is NOT ready for implementation label Jun 28, 2021
@eiriktsarpalis
Copy link
Member

Reopening following conversation from #54187.

@eiriktsarpalis
Copy link
Member

@steveharter, @JeremyKuhne why was this type sealed?

This was done for performance

How so? The public type itself is a converter factory so any perf impact would probably only impact the initial converter generation.

@JeremyKuhne
Copy link
Member

How so? The public type itself is a converter factory so any perf impact would probably only impact the initial converter generation.

It was a general API best practice. Virtuals make it harder to change internal implementation details- when something can't be derived we can make assumptions and/or take shortcuts we can't otherwise do when we're sealed. FileStream comes to mind as a concrete example of where we suffered for it not being sealed (being forced to call methods such as ReadByte() that weren't strictly necessary as derived classes might depend on it).

Your factory point is valid, of course, and the team can certainly decide to unseal this if it is felt to give significant value.

@eiriktsarpalis eiriktsarpalis added the blocking Marks issues that we want to fast track in order to unblock other important work label Jun 29, 2021
@eiriktsarpalis
Copy link
Member

Good point about changing virtuals; I have updated the OP so that virtual members are explicitly sealed.

@layomia
Copy link
Contributor

layomia commented Jun 29, 2021

Since the motivating scenario behind unsealing this type is to effectively pass state through it's constructor when using JsonConverterAttribute, I just want to point out that JsonConstructorAttribute is unsealed and exposes a virtual CreateConverter method which allows users to create converter instances using parameterized ctors. Here's an example in our tests. It is an imperative approach, whereas attribute usages are usually declarative, but I thought it was worth mentioning.

@terrajobst
Copy link
Member

  • Looks good as proposed
namespace System.Text.Json.Serialization
{
    // Unsealed
    public class JsonStringEnumConverter : JsonConverterFactory
    {
        // Existing:
        // public JsonStringEnumConverter();
        // public JsonStringEnumConverter(JsonNamingPolicy? namingPolicy = null, bool allowIntegerValues = true);

        // Existing methods, now sealed
        public sealed override bool CanConvert(Type typeToConvert);
        public sealed override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options);
    }
}

@terrajobst terrajobst added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation blocking Marks issues that we want to fast track in order to unblock other important work labels Jun 29, 2021
eiriktsarpalis added a commit to eiriktsarpalis/runtime that referenced this issue Jun 29, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Text.Json enhancement Product code improvement that does NOT require public API changes/additions
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants