Home > Back-end >  Deserializing JSON to a Dictionary<string, Item> with Item being abstract
Deserializing JSON to a Dictionary<string, Item> with Item being abstract

Time:02-19

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);
  • Related