-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Description
Background and motivation
Using custom JSON converters that derive from JsonConverterFactory such as JsonStringEnumConverter does not always play well with AOT technologies even if we use the source generator. Consider the following code:
Console.WriteLine(JsonSerializer.Deserialize<MyEnum>("\"A\"", MyJsonContext.Default.MyEnum));
[JsonSerializable(typeof(MyEnum))]
internal partial class MyJsonContext : JsonSerializerContext { }
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum MyEnum { A, B }When compiled under NativeAOT, it will warn that Using member 'System.Text.Json.Serialization.JsonStringEnumConverter.JsonStringEnumConverter()' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications..
When ran under NativeAOT it will throw a System.Reflection.MissingMetadataException: 'System.Text.Json.Serialization.Converters.EnumConverter<MyEnum>' is missing metadata.. Fixing it needs fiddling with rd.xml files.
The reason for these failures is that there is not a continuous sequence of generic method calls from the source generator up until the creation of the EnumConverter, making the NativeAOT compiler's life difficult. The source-generated code calls new JsonStringEnumConverter().CreateConverter(typeof(MyEnum), …), which calls EnumConverterFactory.Create(typeof(MyEnum), …), which uses the infamous Type.MakeGenericType to instantiate EnumConverter<MyEnum>.
We can avoid the dynamic type instantiation if we had a way to pass the type in the converter factory through a generic, which is what I am proposing.
API Proposal
The JSON source generator will be taught so that if it sees a [JsonConverterAttribute(typeof(TConverter))] on a type T or a member of type T where TConverter has a static method with one generic parameter that accepts a JsonSerializerOptions and returns a JsonConverter, instead of emitting the equivalent of new TConverter().CreateConverter(typeof(T), Options), it would emit TConverter.CreateConverter<T>(Options).
Here's how JsonStringEnumConverter would be updated:
namespace System.Text.Json;
public class JsonStringEnumConverter
{
public static JsonConverter CreateConverter<T>(JsonSerializerOptions options) where T : struct, Enum;
}API Usage
The API is intended to be used by source-generated code as described above.
Alternative Designs
- My first thought was to add an instance
JsonConverter CreateConverter<T>(JsonSerializerOptions options)method toJsonConverterFactorybut I realized mid-typing thatEnumConverter<T>has generic constraints which cannot be bridged until Expose a way to bridge generic constraints csharplang#6308 is implemented. - If the scenario is not widely applicable, we could make a solution specific to enums by adding an overload to
JsonMetadataServices.GetEnumConverterthat allows specifying whether we want to handle enums as strings, and teaching the source generator to emit a call to this whenJsonStringEnumConverteris applied.
Risks
No response