Search code examples
jsonjson.netjson-deserialization

Deserializing class with abstract property JSON


public abstract class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        [JsonProperty(Required = Required.Always)]
        public string Type { get; set; }
    }

    public class Employee : Person
    {
        public string Department { get; set; }
        public string JobTitle { get; set; }
    }

    public class Artist : Person
    {
        public string Skill { get; set; }
    }

I already have a JSON converter working to deserialize such objects based on the value of the Type property.

public abstract class JsonCreationConverter<T> : JsonConverter
    {
        protected abstract T Create(Type objectType, JObject jObject);

        public override bool CanConvert(Type objectType)
        {
            Type t = typeof(T);
            return typeof(T).IsAssignableFrom(objectType);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            JObject jObject = JObject.Load(reader);
            T target = Create(objectType, jObject);
            serializer.Populate(jObject.CreateReader(), target);
            return target;
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }

    public class PersonConverter : JsonCreationConverter<Person>
    {
        protected override Person Create(Type objectType, JObject jObject)
        {
            if(jObject["Type"] == null)
            {
                throw new ArgumentException();
            }

            string type = (string)jObject["Type"];
            if(type == null)
            {
                throw new ArgumentException();
            }

            if(type.Equals("Employee", StringComparison.InvariantCultureIgnoreCase))
            {
                return new Employee();
            }
            else if (type.Equals("Artist", StringComparison.InvariantCultureIgnoreCase))
            {
                return new Artist();
            }

            return null;
        }
    }

string json = "[{\"Department\": \"Department1\",\"JobTitle\": \"JobTitle1\",\"FirstName\": \"FirstName1\",\"LastName\": \"LastName1\",\"Type\": \"Employee\"},{\"Skill\": \"Drawer\",\"FirstName\": \"FirstName1\",\"LastName\": \"LastName1\",\"Type\": \"Artist\"}]";
List<Person> person = JsonConvert.DeserializeObject<List<Person>>(json, new PersonConverter());

The above works well.

Now, I have the following class:

public class City
    {
        public string Name { get; set; }
        public int Population { get; set; }
        public Person[] Persons { get; set; }
    }

How do I write a converter for this City class that can use the PersonConverter to initialize the Persons property? My initial thought was to extract the Persons part as JObject, and then call Deserialize on it using PersonConverter in its ReadJson method, similar to the sample below.

 var p = jObject["Persons"].ToString();
 List<Person> persons = JsonConvert.DeserializeObject<List<Person>>(p, new PersonConverter());

But ReadJson throws an exception in the Serializer.Populate method since abstract classes cannot be instantiated.

The following is the JSON string as an example:

string Cityjson = "{\"Name\": \"London\" , \"Population\": \"1000\" , \"Persons\": [{\"Department\": \"Department1\",\"JobTitle\": \"JobTitle1\",\"FirstName\": \"FirstName1\",\"LastName\": \"LastName1\",\"Type\": \"Employee\"},{\"Skill\": \"Drawer\",\"FirstName\": \"FirstName1\",\"LastName\": \"LastName1\",\"Type\": \"Artist\"}]}";

Approach #1

I solved this by

  1. Marking the Persons property to be ignored in deserialization

    [JsonIgnore] public Person[] Persons { get; set; }

  2. In the Create method, instantiating the City object and using PersonConverter to initialize the Persons property

    protected override City Create(Type objectType, JObject jObject) { if (jObject["Persons"] == null) { throw new ArgumentException(); }

             var p = jObject["Persons"].ToString();
    
             List<Person> persons = JsonConvert.DeserializeObject<List<Person>>(p, new PersonConverter());
             var city = new City();
             city.Persons = persons.ToArray();
             return city;
         }
    

The ReadJson method would populate the remaining City properties as usual.

Are there any other approaches?


Solution

  • I think this is the most suitable way

    In ReadJson when array is passed it was basically crashing since Jarray is not jboject. So, I updated the ReadJson as follows and it worked.

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
            {
                if (reader.TokenType == JsonToken.StartArray)
                {
                    JArray jObject = JArray.Load(reader);
                    List<T> list = new List<T>();
                    for (int i = 0; i < jObject.Count(); i++)
                    {
                        var p = jObject[i];
                        JObject ob = p as JObject;
                        T value = Create(objectType, ob);
                        serializer.Populate(ob.CreateReader(), value);
                        list.Add(value);
                    }
    
                    return list.ToArray();
                }
                else
                {
                    JObject jObject = JObject.Load(reader);
                    T target = Create(objectType, jObject);
                    serializer.Populate(jObject.CreateReader(), target);
                    return target;
                }
            }
    

    And yes, I don't need a CityConverter. Adding PersonConverter is enough.