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

Support for JsonSerializerContext inheritance in source generation with JsonTypeInfo reuse #101663

Open
brantburnett opened this issue Apr 28, 2024 · 1 comment
Labels
area-System.Text.Json untriaged New issue has not been triaged by the area owner

Comments

@brantburnett
Copy link
Contributor

Problem

Using JsonSerializerContext and source generation is a great tool for improving performance and supporting trimming and NativeAOT. However, producing shared libraries where support for trimming and NativeAOT is desired can be problematic in more complex scenarios. By definition, each JsonSerializerContext generated does two things that are important in the context of this issue. It roots all of the serializable types in the type hierarchy so they are not trimmed and it produces serialization/deserialization logic for each of them.

The difficulty arises when producing a large library with many various "areas". In this case, a library consumer may only use some areas of the library and not others, and when trimming the "areas" that are unused should also be trimmed to reduce size. If these areas each use JSON serialization, that leaves the library author with the choice to either create one very large source-generated JsonSerializerContext or to create a separate source-generated JsonSerializerContext for each area. The first option effectively can't be trimmed, since it will root every JSON-serializable class from every area. The second option, however, can result in massive duplication if there are also many shared classes between the various generated serializer contexts. This can bloat the library in terms of size, memory usage, JIT time, etc.

Proposed Solution

The proposed solution is to support inheriting source-generated contexts from one another. This is almost supported today, as this contrived example does compile:

public class CommonModel
{
    public string? Name { get; set; }
}

public class SpecificModelA
{
    public Guid Id { get; set; }

    public CommonModel? Child { get; set; }
}

public class SpecificModelB
{
    public int Id { get; set; }

    public CommonModel? Child { get; set; }
}

[JsonSerializable(typeof(CommonModel))]
internal partial class CommonSerializerContext : JsonSerializerContext;

[JsonSerializable(typeof(SpecificModelA))]
internal partial class SpecificSerializerContextA : CommonSerializerContext;

[JsonSerializable(typeof(SpecificModelB))]
internal partial class SpecificSerializerContextB : CommonSerializerContext;

However, the CommonModel has its JsonTypeInfo and all related serialization logic regenerated in both the specific serializer contexts. Instead, the source generator could recognize the inheritance pattern and not generate code for JsonTypeInfo<CommonModel>. The JsonSerializerOptions used by the context could use a combined ITypeInfoResolver to pull types from both contexts, or GetTypeInfo could call the parent base implementation, or some similar method could be used to resolve the JsonTypeInfo<CommonModel>.

Under this pattern, all types used by CommonSerializerContext would be rooted in all cases, but yet there would only be one instance of their serialization logic. Types related to the two specific serializer contexts would then be rooted based on where the code that uses those contexts is used. Library authors could then divide their serializer contexts logically based on how various consumers may use their library and subsequently get better support for trimming.

Concerns

One concern is handling the various options that are applied using the JsonSourceGenerationOptions attribute. If the base context and the derived context both include options, what is the desired behavior? The GenerationMode property would almost certainly need to apply only to models specifically included in that class. For other properties, the settings on the derived context may "win".

Another concern is handling a class explicitly included via JsonSerializable on the derived class that's also included in the base class. Should it be completely regenerated in the derived class? Or should a simple forwarder property be created for the TypeInfoPropertyName that still uses the base implementation?

Finally, what about inheritance between assemblies? The source generator may not have sufficient information about a base context from another assembly (especially if it's a PackageReference and not a ProjectReference) to make the necessary decisions.

@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Apr 28, 2024
@eiriktsarpalis
Copy link
Member

I find it unlikely that we would pursue such functionality, primarily because we would have to contend with separate context types in the hierarchy being generated by different versions of the source generator and potential incompatibilities arising from different generation strategies.

The other issue is that we generally want to move away from using context classes altogether, because it is a rather clunky way for registering source generated types. At some point in the future we would to replace it with better language tools (static abstract interface methods, extension interfaces, interceptors), this discussion is tracked by #90787.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.Text.Json untriaged New issue has not been triaged by the area owner
Projects
None yet
Development

No branches or pull requests

2 participants