System.Text.Json AOT Polymorphic serialization #115303
-
DescriptionI'm making an AOT compatible library. This library contains a base type that users inherit from for their own custom classes. This type needs serialized, but considering the base type and the derived types are in different assemblies, the base type doesn't have knowledge of the derived types at compile time. I want it to be easy for the user to register types, avoiding requiring users to have a deep understanding of the System.Text.Json API so I've provided a static method they can use to register their custom classes through. When serializing, I have added a modifier to the BaseTypeContext's default resolver, which adds the registered derived type to the base type's PolymorphismOptions, but it's never being called. This works in a non-source gen context, but for source gen, it appears to need to know about all derived types to call the Modifier, and it needs the modifier to be called to know about any derived type. I saw this old issue, but it looks like the user stopped replying and the last suggestion seemed to still require references to the user's custom classes in the base assembly, which isn't feasible. Is the only currently supported way to achieve AOT-compatible cross-assembly polymorphic serialization to put the burden mainly on the user, or is there another way I'm missing? Reproduction Stepsusing System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
BaseType.Register<DerivedType>();
JsonSerializerOptions options = new()
{
WriteIndented = true,
TypeInfoResolver = BaseTypeContext.Default
.WithAddedModifier(static ti =>
{
// Never reached
if (ti.Type.IsAssignableTo(typeof(BaseType)))
{
ti.PolymorphismOptions = new JsonPolymorphismOptions
{
TypeDiscriminatorPropertyName = "DerivedType",
IgnoreUnrecognizedTypeDiscriminators = true,
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
};
foreach (var type in BaseType.DerivedTypes)
ti.PolymorphismOptions.DerivedTypes.Add(type);
}
}
)
};
DerivedType derivedType = new(4, "Foo");
// Exception thrown, says 'JsonTypeInfo metadata for type 'DerivedType' was not provided'
string ser = JsonSerializer.Serialize(derivedType, options);
Console.WriteLine(ser);
Console.ReadLine();
// Base Assembly
public class BaseType(int myInt)
{
private static List<JsonDerivedType> _derivedTypes = [];
public static IReadOnlyList<JsonDerivedType> DerivedTypes => _derivedTypes;
public static void Register<T>()
{
Type type = typeof(T);
_derivedTypes.Add(new JsonDerivedType(type, type.Name));
}
public int MyInt { get; set; } = myInt;
}
[JsonSerializable(typeof(BaseType))]
public partial class BaseTypeContext : JsonSerializerContext
{
}
// User Assembly
public class DerivedType(int myInt, string myString) : BaseType(myInt)
{
public string MyString { get; set; } = myString;
} Expected behaviorCalls the modifier and serializes the type. Actual behaviorNever calls the modifier and throws exception when serializing. Regression?No. Known WorkaroundsYou can change the source gen context to a The other work around is to put the burden on the user and assume they have a deep understanding of System.Text.Json and are okay with splitting registration of each type across all of their custom classes via attributes. Of course, this is less than ideal though. ConfigurationThis is a standard .NET 9 console app. Attempted on Linux and Windows x64. Other informationN/A |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 2 replies
-
Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis |
Beta Was this translation helpful? Give feedback.
-
In the context of AOT, apart from declaring a derived type you also need to tell the serializer how that derived type is meant to be serialized. This involves having the user define their own context for |
Beta Was this translation helpful? Give feedback.
-
Just an FYI: Since you don't have control over how the users of your library implement their derived types and what property names they will come up with, using a TypeDiscriminatorPropertyName that is a valid identifier in C#/F# could possibly lead to conflicts if for whatever reason some user chooses to implement a derived type with a property of exactly the same name. I don't know how likely that scenario would be, but unless you are required to use exactly this TypeDiscriminatorPropertyName, why leave it to chance (and needing to make your users aware of this) if you could avoid this outright by choosing one that is not a valid C#/F# identifier. |
Beta Was this translation helpful? Give feedback.
In the context of AOT, apart from declaring a derived type you also need to tell the serializer how that derived type is meant to be serialized. This involves having the user define their own context for
DerivedType
and pass that intoJsonSerializerOptions
as well. In your example, this would involve having theRegister<T>()
method additionally accept aJsonSerializerContext
argument which is then appended to theTypeInfoResolverChain
property in yourJsonSerializerOptions
.