Deserialize JSON array which has mixed values System.Text.JSON

Viewed 6286

I'm trying to create a page rendered in .net core 3.1 which renders pages based on JSON.

How can I deserialzie the JSON at the end of this post?

I've tried to deserialize this however it doesn't work because I loose the data for each Component, since the Page class has a List<Component> - but I need this to be a list of varying different components.

Page Model :

public class Page
    {
        public int id { get; set; }
        public string pagename { get; set; }
        public string metatitle { get; set; }
        public string metadescription { get; set; }
        public string created_at { get; set; }
        public string updated_at { get; set; }
        public List<Component> components { get; set; }
    }

    public class Pages
    {
        public List<Page> pages { get; set; }
    }

Component Model:

public class Component
    {
        public string component { get; set; }
        public int id { get; set; }
    }

A Component :

public class Title : Component
    {
        public string component { get; set; }
        public int id { get; set; {
        public string titletext { get; set; }
    }

This is the JSON:

{
      "id":1,
      "pagename":"home",
      "metatitle":"no title",
      "metadescription":"no meta",
      "created_at":"2020-05-31T16:35:52.084Z",
      "updated_at":"2020-05-31T16:35:52.084Z",
      "components":[
         {
            "component":"components.titletext",
            "id":1,
            "titletext":"hello"
         },
         {
            "component":"components.section",
            "id":2,
            "titletext":"hello",
            "descriptiontext":"its a beatiful day in lost santos",
            "buttonlink":"/go"
         },
         {
            "component":"components.cta",
            "id":3,
            "sectiontitle":"hello",
            "buttonlink":"/go",
            "buttontext":"click me"
         }
      ]
   }
2 Answers

If you don't want to add all properties to the Component class like that:

public class Component
{
    public string component { get; set; }
    public int id { get; set; }
    public string titletext { get; set; }
    public string sectiontitle { get; set; }
    public string buttonlink { get; set; }
    public string descriptiontext { get; set; }
}

You will need to write custom JsonConverter for example (not very performant implementation but works with your json and you will not need to parse every field by hand):

public class ComponentConverter : JsonConverter<Component>
{
    public override Component Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        using (var doc = JsonDocument.ParseValue(ref reader))
        {
            var type = doc.RootElement.GetProperty(@"component").GetString();
            switch(type)
            {
                case "components.titletext": 
                    return JsonSerializer.Deserialize<Title>(doc.RootElement.GetRawText());
                // other types handling
                default: return JsonSerializer.Deserialize<Component>(doc.RootElement.GetRawText());
            }
        }
    }

    public override void Write(Utf8JsonWriter writer, Component value, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }
}

public class Component
{
    public string component { get; set; }
    public int id { get; set; }
}

public class Title : Component
{
    public string titletext { get; set; }
}

And usage example:

var json = @"[
     {
        ""component"":""components.titletext"",
        ""id"":1,
        ""titletext"":""hello""
     },
     {
""component"":""components.section"",
        ""id"":2,
        ""titletext"":""hello"",
        ""descriptiontext"":""its a beatiful day in lost santos"",
        ""buttonlink"":""/go""
     },
     {
""component"":""components.cta"",
        ""id"":3,
        ""sectiontitle"":""hello"",
        ""buttonlink"":""/go"",
        ""buttontext"":""click me""
     }
  ]";
var deserializeOptions = new JsonSerializerOptions();
deserializeOptions.Converters.Add(new ComponentConverter());
JsonSerializer.Deserialize<List<Component>>(json, deserializeOptions).Dump();

Also do not use this converter as parameter for JsonConverterAttribute cause it will end in stackoverflow.

If you want/need completely separate unrelated classes you could use a technique that doesn't use a converter:

var ays = new List<A>();
var bees = new List<B>();

using var doc = JsonDocument.Parse(json);
foreach (var block in doc.RootElement.EnumerateArray())
{
    switch (block.GetProperty("component").GetString())
    {
        case "typeA": ays.Add(Deserialise<A>(block)); break;
        case "typeB": bees.Add(Deserialise<B>(block)); break;
        // ... case 
        //default: ...
    }
}

var composite = new
{
    As = ays,
    Bs = bees 
};

// This is OK, but if you need to speed it up, please have a look at
// https://stackoverflow.com/questions/58138793/system-text-json-jsonelement-toobject-workaround
static T Deserialise<T>(JsonElement e) => JsonSerializer.Deserialize<T>(e.GetRawText(), options: new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

Related