Skip to content

[API Proposal]: A pattern for AOT-friendly source-generated JsonConverterFactory-based converters. #73124

@teo-tsirpanis

Description

@teo-tsirpanis

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 to JsonConverterFactory but I realized mid-typing that EnumConverter<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.GetEnumConverter that allows specifying whether we want to handle enums as strings, and teaching the source generator to emit a call to this when JsonStringEnumConverter is applied.

Risks

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-System.Text.Jsonlinkable-frameworkIssues associated with delivering a linker friendly framework

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions