Reputation: 12124
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
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
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