19

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?

Community
  • 1
  • 1
Electric Coffee
  • 11,733
  • 9
  • 70
  • 131
  • I found that I got exactly the same error if I used the JsonConvert.DeserializeObject and passed a serializer settings with my converter added. When I used the same overload as you it worked... I don't know whether this is the JsonSerializer at fault or my understanding of the serializer settings. – The Senator Jan 20 '16 at 14:37

2 Answers2

30

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;
}
Mouhong Lin
  • 4,402
  • 4
  • 33
  • 48
  • I was gonna do your "btw" after I got this to work, the original code didn't have polymorphism, but I decided to extend it to modularise the functionality – Electric Coffee Mar 18 '15 at 14:41
  • 1
    I deleted that "btw". I just found your properties currently in the base `MenuItem` should be essential. :P – Mouhong Lin Mar 18 '15 at 14:44
  • For others with a similar question, this works on any polymorphic deserialization of an abstract class as well. I was getting a StackOverflowException prior without a self-referential structure because jObject.ToObject(targetType) would recursively send me back in. – daniel.caspers Aug 23 '18 at 17:06
0

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); } }
Community
  • 1
  • 1
Bernd Linde
  • 2,098
  • 2
  • 16
  • 22