5

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"
         }
      ]
   }
user2839999
  • 99
  • 2
  • 9
  • one way to go, create a custom deserlizeing class (check https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to) – Maytham Fahmi May 31 '20 at 19:17
  • 1
    **Edit** -> **Paste Special** -> **Paste JSON as Classes** – Ňɏssa Pøngjǣrdenlarp May 31 '20 at 19:18
  • you're missing a comma in this line `"descriptiontext":"its a beatiful day in lost santos"` – Mohammed Sajid May 31 '20 at 19:23
  • Well since the only difference between the two classes is that `Title` has more properties that `Component`, you could just deserialize to a `List`. But if you need a polymorphic hierarchy you will need to write a custom `JsonConverter`. See [Is polymorphic deserialization possible in System.Text.Json?](https://stackoverflow.com/q/58074304/3744182) and [System.Text.Json and Dynamically Parsing polymorphic objects](https://stackoverflow.com/q/60792311/3744182). – dbc May 31 '20 at 19:25

2 Answers2

4

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.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • This works exactly as I had hoped. You mention that this solution is "not very performant implementation", could you point me in a direction or give me a clue as to how I could make it more performant? – user2839999 Dec 16 '20 at 23:13
  • @user2839999 was glad to help! As for performance I don't have a lot of ideas out of the box, at least if they do not involve handling all the fields manually. Maybe [this](https://stackoverflow.com/a/59744873/2501279) will give you some ideas. Also please check that performance of this implementation is a valid concern for you, cause some times "is good enough" really is good enough =) – Guru Stron Dec 17 '20 at 10:43
2

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 });

tymtam
  • 31,798
  • 8
  • 86
  • 126