Search code examples
c#jsonunity-game-enginejson.netjson-deserialization

Newtonsoft JSON Serialize/Deserialize derived types


Context; Using the Unity Engine. JSON files store game data of assets in the game.

I have the below JSON file which stores data about possible Units in the game. This is a Dictionary<string, UnitEntity> where UnitEntity inherits Entity (which is an abstract class)

The KEY is the name of the Unit The VALUE is the Unit:Entity itself

{
    "Infantry": {
        "moveSpeed": 2.0,
        "turnRateRadians": 2.0,
        "unitArmourType": 0,
        "unitDamage": 10.0,
        "unitAttackCooldownSeconds": 2.0,
        "unitAttackAoERange": 0.0,
        
        // List of structs of an enum and float
        "damageToArmourTypes": [
            {
                "armour": 0,
                "damageRatio": 1.0
            },
            {
                "armour": 1,
                "damageRatio": 0.25
            },
            {
                "armour": 2,
                "damageRatio": 0.0
            }
        ],

        // Below fields are from the abstract Base class
        "id": 0,
        "name": "Infantry",
        "faction": "USA",
        "description": "Basic Infantry of the USA",
        "menuLocation": 3,
        "menuOrderIndex": -1,
        "maxHealth": 50,
        "entityPrefabReference": "Assets/Resources/Prefabs/Units/InfantryPrefab.prefab",

        // A struct of an enum and int
        "constructionCost": [
            {
                "_resourceType": 0,
                "_resourceCount": 250
            }
        ]
    }
}

Above is an example of the JSON that is created on Serialize which is what I'd expect (perhaps not the base data being at the bottom half, but if its consistent....sure)

Serialize seems to execute fine. No errors. But when I attempt;

string factionEntityJson = File.ReadAllText(filePath);
Dictionary<string, UnitEntity> entity = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, UnitEntity>>(factionEntityJson); ;

I get the error;

Value cannot be null.
Parameter name: collection

I attempted to instead cast to a Dictionary<string, object> and whilst it executed, inspecting the contents it said something along the lines of "Method is not implemented" for the JToken.Value (or something along those lines).

What is the issue here (does NewtonSoft.Json not like inheritance / derived types?) and how do I fix it?

EDIT:

The UnitEntity Class as requested:


[Serializable]
public struct ArmourDamageRatio
{
    public Unit_Armour_Type armour;
    public float damageRatio;
}

[Serializable]
public class UnitEntity : Entity
{

    public float moveSpeed = 1f;
    public float turnRateRadians = 1f;
    public Unit_Armour_Type unitArmourType = Unit_Armour_Type.NONE;  
    public float unitDamage = 5f;
    public float unitAttackCooldownSeconds = 1f;
    public float unitAttackAoERange = 0f;       // 0 range means no AoE (IE bullets)
    public List<ArmourDamageRatio> damageToArmourTypes = new List<ArmourDamageRatio>();


    public UnitEntity(int id)
        : base(id)
    {

    }


    [JsonConstructor]
    public UnitEntity(int id, 
        string faction, 
        string name, 
        string desc, 
        int menuLoc, 
        int menuOrder, 
        string buildingPath, 
        int maxHp, 
        List<GameResourcePair> cost,
        float moveSpd,
        float turnRate,
        Unit_Armour_Type armourType,
        float dmgAmt,
        float attackCooldown,
        float aoeRange,
        List<ArmourDamageRatio> damageTypes)
        : base(id, faction, name, desc, menuLoc, menuOrder, buildingPath, maxHp, cost)
    {
        this.moveSpeed = moveSpd;
        this.turnRateRadians = turnRate;
        this.unitArmourType = armourType;
        this.unitDamage = dmgAmt;
        this.unitAttackCooldownSeconds+= attackCooldown;
        this.unitAttackAoERange = aoeRange;
        this.damageToArmourTypes.AddRange(damageTypes);
    }
}

The Entity Base class:


[Serializable]
public abstract class Entity
{
    public int id;                                          // Unique ID of entity (Not set by user / dev)
    public string name;                                     // Name of the Entity
    public string faction;                                  // Faction entity belongs to
    public string description;                              // Description of the Entity
    public UI_Menu menuLocation;                            // Which UI menu will it appear in (Infrastructure, Defence, Barracks etc)
    public int menuOrderIndex;                              // Order entity appears in the menu (-1 indicates not set)
    public int maxHealth;                                   // Max health of entity

    public string entityPrefabReference;                       // Entity prefab Object (File path to load at runtime)
    public List<GameResourcePair> constructionCost;         // List of construction costs of Building Entity

    public Entity(int id)
    {
        this.id = id;
        this.name = "";
        this.faction = "";
        this.description = "";
        this.menuLocation = UI_Menu.NONE;
        this.menuOrderIndex = -1;
        this.maxHealth = 0;
        this.entityPrefabReference = null;
        this.constructionCost = new List<GameResourcePair>();
    }


    [JsonConstructor]
    public Entity(int id, string faction, string name, string desc, int menuLoc, int menuOrder, string buildingPath, int maxHp, List<GameResourcePair> cost)
    {
        this.id = id;
        this.name = name;
        this.faction = faction;
        this.description = desc;
        this.menuLocation = (UI_Menu)menuLoc;
        this.menuOrderIndex = menuOrder;
        this.maxHealth = maxHp;
        this.constructionCost = cost;
        this.entityPrefabReference = buildingPath;
    }
// Other functions (==. != overrides etc)
}

Solution

  • you have a bug in your code, change the input parameter name "damageTypes" of UnitEntity constructor to "damageToArmourTypes" and IMHO it is better to add a null validation for the future

    [JsonConstructor]
    public UnitEntity(int id,
    
    //...another parameters
    
    float aoeRange,
    List<ArmourDamageRatio> damageToArmourTypes)
    
    {
      // ...another code
    
    if (damageToArmourTypes != null)
       this.damageToArmourTypes.AddRange(damageToArmourTypes);
    
    //...
        
    }