Search code examples
c#json.netserializationjson.net

Deserialize complex JSON structure


I am working on a C# app that fetches data from an API. The response from the API has blocks that contain a node Data that changes in structure and only shares one attribute, Type. I am using Newtonsoft. How can I deserialize the Data node to the exact class based on the Type attribute? So that if I create an items array, when I query the array I can switch on the Type attribute and know which object maps to it.

The JSON response from the API looks like this:

{
    "nextLink": "NextLink",
    "value": [
        {
            "Block": {
                "BlockName": "Name",
                "BlockClass": "Class",
                "IsVisibile": true,
                "Data": {
                    "Type": "Footer",
                    "Values": [
                        { 
                            "Property1": "Property1",
                            "Property2": "Property2"
                        }
                    ],
                    "Show": false
                }
            }
        },
        {
            "Block": {
                "BlockName": "Name",
                "BlockClass": "Class",
                "IsVisibile": true,
                "Data": {
                    "Type": "Header",
                    "Title": "Main title",
                    "Subtitle": "Subtitle"
                }
            }
        },
        {
            "Block": {
                "BlockName": "Name",
                "BlockClass": "Class",
                "IsVisibile": true,
                "Data": {
                    "Type": "Body",
                    "Information": "Info",
                    "AdditionalText": "More text",
                    "Projects": [
                        "Project1",
                        "Project2"
                    ]
                }
            }
        }
    ]
}

These are the classes I have defined based on the API response:

public class Blocks
{
    public string NextLink { get; set; }
    public Response[] Value { get; set; }
}

public class Response
{
    public Block Block { get; set; }
}

public class Block
{
    public string BlockName { get; set; }
    public string BlockClass { get; set; }
    public bool IsVisible { get; set; }
    public Data Data { get; set; }
}

public class Data 
{
    public string Type { get; set; }
    ???
}

public class FooterType
{
    public bool Show { get; set; }
    public object[] Values { get; set; }
}

public class HeaderType 
{
    public string Title { get; set; }
    public string Subtitle { get; set; }
}

public class BodyType
{
    public string Information { get; set; }
    public string AdditionalText { get; set; }
    public object[] Projects { get; set; }
}

If I deserialize the response to the specific Type (like below) it of course works for that node in the JSON but not all of them.

public class Data 
{
    public string Type { get; set; }
    public string Title { get; set; }
    public string Subtitle { get; set; }
}

How can I deserialize each item in the JSON to the specific type it maps to?


Solution

  • The simpliest way is to create the polymorphic clases and use a json property constructor

        Root root = JsonConvert.DeserializeObject<Root>(json);
    
    public class Root
    {
        public string nextLink { get; set; }
        public List<Item> value { get; set; }
    }
    public class Item
    {
        public Block Block { get; set; }
    }
    public class Block
    {
        public string BlockName { get; set; }
        public string BlockClass { get; set; }
        public bool IsVisibile { get; set; }
        public DataBase Data { get; set; }
        
        [JsonConstructor]
        public Block (JObject Data)
        {
            this.Data = (DataBase)Data.ToObject(Type.GetType((string)Data["Type"]));
        }
        public Block() {}
    }
    
    public class DataBase
    {
        public string Type { get; set;}
        //public string Type { get { return this.GetType().Name; } }
    }
    
    public class Footer : DataBase
    {
        public List<Properties> Values { get; set; }
        public bool Show { get; set; }
    }
    
    public class Header : DataBase
    {
        public string Title { get; set; }
        public string Subtitle { get; set; }
    }
    
    public class Body : DataBase
    {
        public string Information { get; set; }
        public string AdditionalText { get; set; }
        public List<string> Projects { get; set; }
    }
    
    public class Properties
    {
        public string Property1 { get; set; }
        public string Property2 { get; set; }
    }
    

    but if you have a lot of json of this kind, instead of adding one line to a constructor you can create a json property converter

    Root root = JsonConvert.DeserializeObject<Root>(json);
    
    public class DataPropertyConverter : JsonConverter<DataBase>
    {
        public override DataBase ReadJson(JsonReader reader, Type objectType, DataBase existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            var jObj = JObject.Load(reader);
             return (DataBase)jObj.ToObject(Type.GetType((string)jObj["Type"]));
        }
        
        public override bool CanWrite => false;
        public override void WriteJson(JsonWriter writer, DataBase value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    
    public class Block
    {
        public string BlockName { get; set; }
        public string BlockClass { get; set; }
        public bool IsVisibile { get; set; }
    
        [JsonConverter(typeof(DataPropertyConverter))]
        public DataBase Data { get; set; }
    }