Electric Coffee
Electric Coffee

Reputation: 12124

Polymorphic JSON Deserialization failing using Json.Net

I'm trying to deserialize some JSON to various sub-classes using a custom JsonConverter

I followed this almost to the point.

My abstract base-class:

abstract class MenuItem
{
    public String Title { get; set; }
    public String Contents { get; set; }
    public List<MenuItem> Submenus { get; set; }
    public String Source { get; set; }
    public String SourceType { get; set; }
    public abstract void DisplayContents();
}

And my derived JsonConverter:

class MenuItemConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return typeof(MenuItem).IsAssignableFrom(objectType);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            JObject item = JObject.Load(reader);
            switch (item["SourceType"].Value<String>())
            {
                case SourceType.File:    return item.ToObject<Menu.FileMenu>();
                case SourceType.Folder:  return item.ToObject<Menu.FolderMenu>();
                case SourceType.Json:    return item.ToObject<Menu.JsonMenu>();
                case SourceType.RestGet: return item.ToObject<Menu.RestMenu>();
                case SourceType.Rss:     return item.ToObject<Menu.RssMenu>();
                case SourceType.Text:    return item.ToObject<Menu.TextMenu>();
                case SourceType.Url:     return item.ToObject<Menu.UrlMenu>();
                default: throw new ArgumentException("Invalid source type");
            }
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }

SourceType is just a static class holding some string constants.

The JSON file is deserialized like this:

JsonConvert.DeserializeObject<MenuItem>(File.ReadAllText(menuPath), new MenuItemConverter());

Now, my issue is that whenever I run the code I get the following error:

An exception of type 'Newtonsoft.Json.JsonSerializationException' occurred in Newtonsoft.Json.dll but was not handled in user code

Additional information: Could not create an instance of type ConsoleMenu.Model.MenuItem. Type is an interface or abstract class and cannot be instantiated. Path 'Submenus[0].Title', line 5, position 21.

The Json file in question looks like this:

{
    "Title": "Main Menu",
    "Submenus": [
        {
            "Title": "Submenu 1",
            "Contents": "This is an example of the first sub-menu",
            "SourceType": "Text"
        },
        {
            "Title": "Submenu 2",
            "Contents": "This is the second sub-menu",
            "SourceType": "Text"
        },
        {
            "Title": "GitHub System Status",
            "Contents": "{\"status\":\"ERROR\",\"body\":\"If you see this, the data failed to load\"}",
            "Source": "https://status.github.com/api/last-message.json",
            "SourceType": "RestGet"
        },
        {
            "Title": "TF2 Blog RSS",
            "Contents": "If you see this message, an error has occurred",
            "Source": "http://www.teamfortress.com/rss.xml",
            "SourceType": "Rss"
        },
        {
            "Title": "Submenus Test",
            "Contents": "Testing the submenu functionality",
            "Submenus": [
                {
                    "Title": "Submenu 1",
                    "Contents": "This is an example of the first sub-menu",
                    "SourceType": "Text"
                },
                {
                    "Title": "Submenu 2",
                    "Contents": "This is the second sub-menu",
                    "SourceType": "Text"
                }
            ]
        }
    ],
    "SourceType": "Text"
}

It appears to me that it has trouble deserializing the nested objects, how do I get around that?

Upvotes: 19

Views: 7184

Answers (2)

Bernd Linde
Bernd Linde

Reputation: 2152

The reason you are getting the error is because your MenuItem class is marked as abstract. I am guessing you did this to enforce the implementation of the DisplayContents() method in inherited classes.

A different way of allowing the Json to be read, to what Mouhong Lin suggested, is to make a base Interface for your MenuItem structure, have your MenuItem class implement the interface with a basic version of the DisplayContents() method, mark it as virtual and then override it in your inherited subclasses.
This approach will ensure that you always will get something shown when calling DisplayContents() and remove the error that you are getting.

A very crude and simplified version of the classes and interface:

public interface IMenuItem
{
  String Title { get; set; }
  String Contents { get; set; }
  List<MenuItem> Submenus { get; set; }
  String Source { get; set; }
  String SourceType { get; set; }
  void DisplayContents();
}

public class MenuItem: IMenuItem
{
  public String Title { get; set; }
  public String Contents { get; set; }
  public List<MenuItem> Submenus { get; set; }
  public String Source { get; set; }
  public String SourceType { get; set; }
  public virtual void DisplayContents() { MessageBox.Show(Title); }
}

// Very very basic implementation of the classes, just to show what can be done
public class FileMenu : MenuItem { public override void DisplayContents() { MessageBox.Show(Title + this.GetType().ToString()); } }
public class FolderMenu : MenuItem { public override void DisplayContents() { MessageBox.Show(Title + "Folder Class"); } }
public class JsonMenu : MenuItem { public override void DisplayContents() { MessageBox.Show(Contents); } }
public class RestMenu : MenuItem { public override void DisplayContents() { MessageBox.Show(Source); } }
public class RssMenu : MenuItem { public override void DisplayContents() { MessageBox.Show(SourceType); } }
public class TextMenu : MenuItem { public override void DisplayContents() { MessageBox.Show(Title); } }
public class UrlMenu : MenuItem { public override void DisplayContents() { MessageBox.Show(Title); } }

Upvotes: 0

Mouhong Lin
Mouhong Lin

Reputation: 4509

Firstly, SourceType is missed for menu item "Submenus Test" in your json.

Secondly, you shouldn't simply use ToObject because of the Submenus property, which should be handled recursively.

The following ReadJson will work:

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    var jObject = JObject.Load(reader);
    var sourceType = jObject["SourceType"].Value<string>();

    object target = null;

    switch (sourceType)
    {
        case SourceType.File: 
            target = new FileMenu(); break;
        case SourceType.Folder: 
            target = new FolderMenu(); break;
        case SourceType.Json: 
            target = new JsonMenu(); break;
        case SourceType.RestGet: 
            target = new RestMenu(); break;
        case SourceType.Rss: 
            target = new RssMenu(); break;
        case SourceType.Text: 
            target = new TextMenu(); break;
        case SourceType.Url: 
            target = new UrlMenu(); break;
        default: 
            throw new ArgumentException("Invalid source type");
    }

    serializer.Populate(jObject.CreateReader(), target);

    return target;
}

Upvotes: 31

Related Questions