Search code examples
c#jsonsystem.text.json

Select type to use for deserialization in System.Text.Json


I have a .NET 6 app that needs to deserialize some JSON data to C# objects, using System.Text.Json.

However, in the classes I'm trying to deserialize I have a classic case where a property has a base-class type, like this:

public class ClassToDeserialize
{
    public JobBase SomeJob { get; set; }
}

public class JobBase
{
    public string JobName { get; set; }
}

that needs to be deserialized to the correct derived type, for example:

public class OnDemandJob : JobBase
{
    public string RequestUser { get; set; }
}

public class ScheduledJob: JobBase
{
    public string Schedule { get; set; }
}

What I would like to do is write a custom Converter that checks the JSON data to decide which type to use for deserialization (for example, in the example above we could find out if the actual object is an instance of ScheduledJob by checking if there is a Schedule property in the data) and then deserializing to that type.

Now, reading the official documentation I was happy to find that Microsoft thought of this and has an article with an example on how to implement this scenario using converters:

https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0#support-polymorphic-deserialization

However my problem is this: to me, the Converter they implement in the example seems overcomplicated for what it needs to do. In particular, what they do after finding out the correct type is "manually" build the correct derived object by creating an instance and manually reading the JSON data and assigning properties to the object one by one.

This seems like a lot of manual work for what I want to achieve. Intuitively, I'd think it should be possible to do something like this (pseudocode):

public JobBase Read(...)
{
    if(jsonObject.HasProperty("Schedule")
    {
        return JsonSerializer.Deserialize<ScheduledJob>(jsonString);
    }
    else 
    {
        return JsonSerializer.Deserialize<OnDemandJob>(jsonString);
    }
}

Is something like this possible? Or do I really have to manually build the entire object field-by-field?


Solution

  • Thanks to @dbc for pointing me in the right direction, here's the converter I've ended up with:

    public class JobBaseConverter : JsonConverter<JobBase>
    {
        public override JobBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                throw new JsonException();
            }
    
            using (var jsonDocument = JsonDocument.ParseValue(ref reader))
            {
                var jsonObject = jsonDocument.RootElement.GetRawText();
                if (jsonDocument.RootElement.TryGetProperty(nameof(ScheduledJob.Schedule), out var typeProperty))
                {
                    return JsonSerializer.Deserialize<ScheduledJob>(jsonObject, options);
                }
                else
                {
                    return JsonSerializer.Deserialize<OnDemandJob>(jsonObject, options);
                }
            }
        }
    
        public override void Write(Utf8JsonWriter writer, JobBase value, JsonSerializerOptions options)
        {
            if(value is ScheduledJob)
            {
                JsonSerializer.Serialize(writer, value as ScheduledJob, options);
            }
            else
            {
                JsonSerializer.Serialize(writer, value as OnDemandJob, options);
            }
        }
    }