Search code examples
c#jsonjson.netdeserializationabstract-class

Deserialize an abstract class that has only a getter using Newtonsoft


I'm trying to deseralize JSON I'm getting:

[
{
    "Name":"0",
    "Health":0,
    "TypeName":"SpellInfo",
    "Info":{
      "Effect":1,
      "EffectAmount":4
    }
  },
  {
    "Name":"1",
    "Health":0,
    "TypeName":"MonsterInfo",
    "Info":{
      "Health":10,
      "AttackDamage":10
    }
  },
...
...
]

Created a class to handle the JSON:

    [System.Serializable]
    public class CardDataStructure
    {
        public string Name; 
        public int Health; 
        public string TypeName;
        public Info Info;
    }

I managed to get all the info I needed but the Info. From the research I did, I created a JsonConverter from a link - https://blog.codeinside.eu/2015/03/30/json-dotnet-deserialize-to-abstract-class-or-interface/ Which is actually pretty close,

public class InfoConvert: JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(Info));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject jo = JObject.Load(reader);

        if (jo.ToString().Contains("Effect"))
        {
            if (jo["Effect"].Value<string>() is string)
                return jo.ToObject<SpellInfo>(serializer);
        }
        if (jo.ToString().Contains("Health"))
        {
            if (jo["Health"].Value<string>() is string)
                return jo.ToObject<MonsterInfo>(serializer);
        }  
        return null;
        }
}

(It would have been better to find it by 'typename' but I couldn't figure out how to do that, so went with something simple)
When checking 'jo', the properties are there and go to the correct class yet once out of the converter I get default properties and not the once the converter showed. I can't find the link but on the Newtonsoft doc it said somewhere there's a problem with deserializing an abstract class and if the abstract class doesn't have a public setter.

Both monsterinfo and spellinfo inherit from info:

    [Serializable]
    public abstract class Info
    {

    }

The monsterinfo and spellinfo look basically the same. Problem is they don't have a public setters and I cannot change them right now.

{
    [Serializable]
    public class MonsterInfo: Info
    {
        [SerializeField] 
        private int m_Health;
        public int Health => m_Health;
        
        [SerializeField]
        private int m_AttackDamage;
        public int AttackDamage => m_AttackDamage;
        

    }
}

So, when trying to deseralize the JSON:

string contents = File.ReadAllText(source);
contents = "{\"cards\":" + contents + "}";
JsonConverter[] converters = { new InfoConvert() };
cardsData = JsonConvert.DeserializeObject<Cards>(contents, new JsonSerializerSettings() {
Converters = converters, NullValueHandling = NullValueHandling.Ignore, 
TypeNameHandling = TypeNameHandling.Auto});

        

*Cards is a list of CardDataStructure

Is it even possible to get the data in Info without giving them a public setter? Best I got is all the data inside the JSON and an empty Monster/Spell Info.

At the end I just need to parse the json I'm getting, but while the 'name', 'health', 'typeinfo' are parsed correctly, info is always an empty object filled with 0s.

Edit: Corrected some things.


Solution

  • You should do that like this dude:

    • A marker interface for detecting the type or deserializing

    • A container class

    • Dto classes

       //marker interface
       public interface Info { }
      
       public class HealthInfo : Info
       {
           public int MoreHealth { set; get; }
           public int AttackDamage { set; get; }
       }
      
       public class SpellInfo : Info
       {
      
           public int Effect { set; get; }
      
           public int EffectAmount { set; get; }
       }
      
       public class Card<T> where T : Info
       {
           public Card(string name, int health, T info)
           {
               this.Info = info;
               this.Name = name;
               this.Health = health;
           }
      
           public T Info { private set; get; }
           public string Name { set; get; }
           public int Health { set; get; }
       }
      
       public class InfoConverter : JsonConverter
       {
           public override bool CanConvert(Type objectType)
           {
               if (objectType == typeof(Card<Info>))
               {
                   return true;
               }
      
               return false;
           }
      
           public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
           {
               JObject jObject = JObject.Load(reader);
               if (jObject.ContainsKey("TypeName"))
               {
                   string typeName = jObject["TypeName"]?.ToString()?.Trim()?.ToLower();
                   if (typeName?.Equals("monsterinfo") == true)
                   {
                       Card<HealthInfo> deseerialized = jObject.ToObject<Card<HealthInfo>>();
                       return new Card<Info>(deseerialized.Name, deseerialized.Health, deseerialized.Info);
                   }
                   if (typeName?.Equals("spellinfo") == true)
                   {
                       string json = jObject.ToString();
                       Card<SpellInfo> deseerialized = jObject.ToObject<Card<SpellInfo>>();
                       return new Card<Info>(deseerialized.Name, deseerialized.Health, deseerialized.Info);
                   }
               }
      
               return null;
           }
      
           public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
           {
               throw new NotImplementedException();
           }
       }
      

    And you should execute:

    List<Card<Info>> list = JsonConvert.DeserializeObject<List<Card<Info>>>(jsonText, new InfoConverter());