0

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?

Master_T
  • 7,232
  • 11
  • 72
  • 144
  • Do you need the properties of `ScheduledJob` and `OnDemandJob` or just the properties of the `JobBase`? – klekmek Jan 24 '22 at 12:18
  • @klekmek: I need to deserialize to the appropriate derived class with all the properties – Master_T Jan 24 '22 at 12:57
  • @Master_T You can do this using Newtonsoft.Json. Is it important for you? – Serge Jan 24 '22 at 13:10
  • 2
    Yes, you can load into a `JsonDocument`, check the properties, then deserialize the document to the correct type. See [this answer specifically](https://stackoverflow.com/a/59785679/3744182) by [Demetrius Axenowski](https://stackoverflow.com/users/4040476/demetrius-axenowski) to [Is polymorphic deserialization possible in System.Text.Json?](https://stackoverflow.com/q/58074304/3744182). In fact this looks to be a duplicate, agree? – dbc Jan 24 '22 at 13:16
  • One improvement: in .NET 6 you can deserialize directly from `JsonDocument`, see [System.Text.Json.JsonElement ToObject workaround](https://stackoverflow.com/a/59047063/3744182). – dbc Jan 24 '22 at 13:16
  • @dbc: thanks, I'll check out the links – Master_T Jan 24 '22 at 13:41
  • @Serge: mainly, since I've been working more and more with .NET 6 I want to learn to do this with System.Text.Json, so I don't have to rely on 3rd party libraries. – Master_T Jan 24 '22 at 13:43
  • @Master_T It is up to you, but I never seen anything so bugy and tricky as System.Text.Json. It only works properly for very simple objects – Serge Jan 24 '22 at 13:44

1 Answers1

4

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);
        }
    }
}
Master_T
  • 7,232
  • 11
  • 72
  • 144
  • What is `jsonObject` in your code example? I don't see that as a parameter or variable. – Ryan Wilson Jun 06 '23 at 13:09
  • @RyanWilson: sorry, there was a line missing from the sample, I've added it. – Master_T Jun 06 '23 at 13:16
  • Thanks!! I'm working on something like this myself. But with about 15 different types that come back in a single array, trying to deserialize to lists of derived types +1 – Ryan Wilson Jun 06 '23 at 13:17