1

Consider these model classes:

[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(A), typeDiscriminator: nameof(A))]
[JsonDerivedType(typeof(B), typeDiscriminator: nameof(B))]
interface IMyType
{
}

class A(DependencyA dependency) : IMyType
{
    public int X { get; set; } = dependency.GetValue();
}

class B(DependencyB dependency) : IMyType
{
    public int Y { get; set; } = dependency.GetValue();
}

class DependencyA
{
    public int GetValue() => 42;
}

class DependencyB
{
    public int GetValue() => 123;
}

DI container used is Microsoft.DependencyInjection.

The example:

static void Main(string[] args)
{
    IServiceCollection services = new ServiceCollection()
        .AddTransient<DependencyA>()
        .AddTransient<DependencyB>()
        .AddTransient<A>()
        .AddTransient<B>();

    IServiceProvider provider = services.BuildServiceProvider();

    IMyType[] items =
    [
        provider.GetRequiredService<A>(),
        provider.GetRequiredService<B>()
    ];

    Print(items);

    string json = JsonSerializer.Serialize(items);
    Console.WriteLine(json);

    // This line is invalid but states my intension:
    IMyType[]? deserialized = JsonSerializer.Deserialize<IMyType[]>(json);
    // ^^^^^^^^
    // System.InvalidOperationException: 'Each parameter in the deserialization constructor
    // on type 'ConsoleAppExample.A' must bind to an object property or field on 
    // deserialization. Each parameter name must match with a property or field on the object.
    // Fields are only considered when 'JsonSerializerOptions.IncludeFields' is enabled.
    // The match can be case-insensitive.'

    Print(deserialized);
}

static void Print(IMyType[]? items)
{
    foreach (IMyType instance in items ?? [])
    {
        switch (instance)
        {
            case A aInstance:
                Console.WriteLine($"A.X = {aInstance.X}");
                break;

            case B bInstance:
                Console.WriteLine($"B.Y = {bInstance.Y}");
                break;
        }
    }
}

Beginning of output (before exception):

A.X = 42
B.Y = 123
[{"$type":"A","X":42},{"$type":"B","Y":123}]

The serialized JSON output is as expected.

My question: how to make the JsonSerializer aware of ServiceProvider to create the instances using DI instead of calling constructor?

I've tried to create some PolymorphicTypeResolver but failed because I didn't find a place to inject IServiceProvider instance where it can get the derived Type and return the object for deserializer.

OK, here's my attempt:

class PolymorphicTypeResolver(IServiceProvider serviceProvider) : DefaultJsonTypeInfoResolver
{
    static Type[] RegisteredTypes = [typeof(A), typeof(B)];

    public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
    {
        var jsonTypeInfo = base.GetTypeInfo(type, options);

        if (jsonTypeInfo.Type == typeof(IMyType))
        {
            // WRONG LINE because here `type` is always `IMyType`
            // But where can I get the derived one?
            jsonTypeInfo.CreateObject = () => serviceProvider.GetRequiredService(type);

            jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
            {
                TypeDiscriminatorPropertyName = "$type",
                UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
            };

            foreach (var derived in RegisteredTypes)
            {
                jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(derived, derived.Name));
            }
        }

        return jsonTypeInfo;
    }
}

A native-AOT-friendly solution is preferred.

1 Answer 1

2

Since you are looking for a solution that is compatible with Native AOT, it would be better to use a JsonTypeInfo modifier that sets the JsonTypeInfo<T>.CreateObject delegate to serviceProvider.GetRequiredService(concreteType) for each concrete type A and B.

First create the following extension methods:

public static partial class JsonExtensions
{
    public static void WithDependencyInjectedCreateObject(JsonTypeInfo typeInfo, IServiceProvider serviceProvider, params Type [] concreteTypes)
    {
        foreach (var concreteType in concreteTypes)
            WithDependencyInjectedCreateObject(typeInfo, serviceProvider, concreteType);
    }

    public static void WithDependencyInjectedCreateObject(JsonTypeInfo typeInfo, IServiceProvider serviceProvider, Type concreteType)
    {
        if (typeInfo.Type != concreteType)
            return;
        typeInfo.CreateObject = () => serviceProvider.GetRequiredService(concreteType);
    }
}

Then when setting up your JsonSerializerOptions, apply the modifier for your RegisteredTypes array as follows:

// MSFT recommends caching & reusing options for best performance.
JsonSerializerOptions defaultOptions = new ()
{
    // In Native AOT scenarios, set TypeInfoResolver to your JsonSerializerContext instance
    TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
    // Use whatever you want here.
    WriteIndented = true,
};
Type[] RegisteredTypes = [typeof(A), typeof(B)];

// Clone your default options and apply the typeInfo modifier to inject the dependency-injected creation function for each registered type.
JsonSerializerOptions options = new (defaultOptions);
options.TypeInfoResolver = 
    (options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver())
    .WithAddedModifier(t => JsonExtensions.WithDependencyInjectedCreateObject(t, provider, RegisteredTypes));

Then if you serialize and deserialize with these options, your types will be round-tripped successfully:

string json = JsonSerializer.Serialize(items, options);

IMyType[]? deserialized = JsonSerializer.Deserialize<IMyType[]>(json, options);

Notes -

Demo .NET 8 fiddle here.

Sign up to request clarification or add additional context in comments.

2 Comments

I was so close, thank you!
@aepot - you're welcome. If you're coming from Newtonsoft you are probably used to inheriting from DefaultContractResolver but with STJ it's almost always better to use a modifier because 1) you can mix and match them, and 2) they can work with source-generated contexts as well as reflection-based contexts.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.