I'm looking to deserialize a JSON string to a Dictionary<string, Item> with Item being an abstract class. I serialize many types of items, some being Weapons, some being Armour, Consumables, etc.
Error: Newtonsoft.Json.JsonSerializationException: 'Could not create an instance of type Item. Type is an interface or abstract class and cannot be instantiated.
EDIT: I'm using Newtonsoft.Json for serializing / deserializing
Deserialization code:
public Dictionary<string, Item> LoadItemDictionary()
{
Dictionary<string, Item> items = new Dictionary<string, Item>();
using (var reader = new StreamReader(ITEM_DICTIONARY_PATH, new UTF8Encoding(false)))
{
string json = reader.ReadToEnd();
items = JsonConvert.DeserializeObject<Dictionary<string, Item>>(json);
}
return items;
}
JSON code:
{
"excalibur": {
"damage": 9999,
"critChance": 10,
"itemID": "excalibur",
"iconLink": "",
"name": "Excalibur",
"description": "placeholder",
"itemType": 1,
"rarity": 4,
"stackSize": 1,
"canBeSold": false,
"buyPrice": 0,
"sellPrice": 0
}
}
Item class:
public abstract class Item
{
public string iconLink = string.Empty;
public string name = string.Empty;
public string description = string.Empty;
public ItemType itemType = ItemType.NONE;
public ItemRarity rarity = ItemRarity.COMMON;
public int stackSize = 1;
public bool canBeSold = false;
public int buyPrice = 0;
public int sellPrice = 0;
public enum ItemRarity
{
COMMON,
UNCOMMON,
RARE,
MYTHIC,
LEGENDARY,
}
public enum ItemType
{
NONE,
WEAPON,
ARMOUR,
CONSUMABLE,
}
}
Weapon Example:
public class Weapon : Item
{
public int damage = 0;
public int critChance = 0;
public new ItemType itemType = ItemType.WEAPON;
}
CodePudding user response:
You can use custom converter to be able to deserialize to different types in same hierarchy. Also I highly recommend using properties instead of fields. So small reproducer can look like this:
public abstract class Item
{
public virtual ItemType Type => ItemType.NONE; // expression-bodied property
public enum ItemType
{
NONE,
WEAPON,
ARMOUR,
CONSUMABLE,
}
}
public class Weapon : Item
{
public override ItemType Type => ItemType.WEAPON; // expression-bodied property
public string SomeWeaponProperty { get; set; }
}
Custom converter:
public class ItemConverter : JsonConverter<Item>
{
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, Item? value, JsonSerializer serializer) => throw new NotImplementedException();
public override Item ReadJson(JsonReader reader, Type objectType, Item existingValue, bool hasExistingValue, JsonSerializer serializer)
{
// TODO handle nulls
var jObject = JObject.Load(reader);
Item result;
switch (jObject.GetValue("type", StringComparison.InvariantCultureIgnoreCase).ToObject<Item.ItemType>(serializer))
{
case Item.ItemType.WEAPON:
result = jObject.ToObject<Weapon>();
break;
// handle other types
// case Item.ItemType.ARMOUR:
// case Item.ItemType.CONSUMABLE:
case Item.ItemType.NONE:
default:
throw new ArgumentOutOfRangeException();
}
return result;
}
}
and example usage (or mark Item
with JsonConverterAttribute
):
var item = new Weapon();
var settings = new JsonSerializerSettings
{
Converters = { new ItemConverter() }
};
string json = JsonConvert.SerializeObject(item, settings);
var res = JsonConvert.DeserializeObject<Item>(json, settings);
Console.WriteLine(res.GetType()); // prints Weapon
CodePudding user response:
Based on Op's comments, after modifying Item to non-abstract class, you can use a converter to convert to various derived classes as follows:
public class ItemCreationConvertor : JsonConverter
{
private readonly Dictionary<Item.ItemType, Type> map = new Dictionary<Item.ItemType, Type>
{
{Item.ItemType.WEAPON, typeof(Weapon)},
{Item.ItemType.NONE, typeof(Item)}
};
public override bool CanWrite => false;
public override bool CanConvert(Type objectType) => typeof(Item).IsAssignableFrom(objectType);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}
// Load JObject from stream
var jObject = JObject.Load(reader);
// Create target object based on JObject
Type targetObjectType = objectType;
if (jObject["itemType"] != null)
{
targetObjectType = this.map.TryGetValue((Item.ItemType)(int)(jObject["itemType"]), out targetObjectType) ? targetObjectType : objectType;
// return new Weapon();
}
object target = Activator.CreateInstance(targetObjectType);
// Populate the object properties
serializer.Populate(jObject.CreateReader(), target);
return target;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Usage:
Dictionary<string, Item> items = new Dictionary<string, Item>();
items = JsonConvert.DeserializeObject<Dictionary<string, Item>>(json, new JsonSerializerSettings
{
Converters = new List<JsonConverter> {new ItemCreationConvertor()}
});
Or decorate Item class with attribute:
[Newtonsoft.Json.JsonConverter(typeof(ItemCreationConvertor))]
public class Item
{
// Other properties go here
}
items = JsonConvert.DeserializeObject<Dictionary<string, Item>>(testJson2);