List of abstract objects and json

I recently bumped into an issue where we would had a list of abstract objects that we needed to be able to serialize it and then deserialize it back. Further more, it needed to without special serializer settings or other configuration heavy solutions, since we are providing a model library to third parties and they should be able to deserialize our data without problems.

Given the following types

public abstract class ListItem
{
   public ListItem(Guid id, string name)
   {
      Id = id;
      Name = name;
   }
   public Guid Id { get; private set; }
   public string Name { get; private set; }
}

public class FirstItemType : ListItem
{
   public FirstItemType(Guid id, string name, string value) : base(id, name)
   {
      Value = value;
   }
   public string Value { get; private set; }
}

public class SecondItemType : ListItem
{
   public SecondItemType(Guid id, string name, string value) : base(id, name)
   {
      Value = value;
   }
   public string Value { get; private set; }
}

A list of List<ItemType> the serialized JSON will look something like this

[
  {
    "Value": "value of first name",
    "Id": "85ed8a4b-9af0-454e-91d2-f77570ed3451",
    "Name": "First item name"
  },
  {
    "Value": "value of second name",
    "Id": "e665a72c-0fd4-445c-b9d2-38cef9b7b042",
    "Name": "Second item name"
  }
]

But when we try to deserialize json.net will return the following error:

Could not create an instance of type ListItem. Type is an interface or abstract class and cannot be instantiated.

Which make perfect sense since there is no way JsonNet is able to tell the difference between the different types.

Json.Net supports adding type names to the serialized data using the setting TypeNameHandling = TypeNameHandling.All, but it messes up the data quite a lot.

{
  "Items": {
    "$type": "System.Linq.Enumerable+WhereSelectListIterator`2[[UserQuery+ListItem, query_emujjx],[<>f__AnonymousType0`2[[System.String, mscorlib],[UserQuery+ListItem, query_emujjx]], query_emujjx]], System.Core",
    "$values": [
      {
        "$type": "<>f__AnonymousType0`2[[System.String, mscorlib],[UserQuery+ListItem, query_emujjx]], query_emujjx",
        "section_type": "FirstItemType",
        "section_value": {
          "$type": "UserQuery+FirstItemType, query_emujjx",
          "Value": "value of first name",
          "Id": "81b7dfc3-6ee8-4f69-9bc6-ccc7600ee757",
          "Name": "First item name"
        }
      },
      {
        "$type": "<>f__AnonymousType0`2[[System.String, mscorlib],[UserQuery+ListItem, query_emujjx]], query_emujjx",
        "section_type": "SecondItemType",
        "section_value": {
          "$type": "UserQuery+SecondItemType, query_emujjx",
          "Value": "value of second name",
          "Id": "55a7a225-6394-4813-a3c3-5f82411df7fc",
          "Name": "Second item name"
        }
      }
    ]
  }
}

I really don’t like this, especially not when I might also have non .Net clients.

The solution was to make a special list with a JsonConverter attribute to instruct Json.Net to add the type name to the data and how to deserialise it again.

public class SerializeableListConverter : JsonConverter
{
   public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
   {
      value.GetType().GenericTypeArguments[0].UnderlyingSystemType.Dump();
   
      var list = value as SerializeableList<ListItem>;

      Func<object> items = () => null;

      if (list != null && list.Any())
      {
         items = () => ((SerializeableList<ListItem>)value)
            .Select(x => new
            {
               section_type = x.GetType().Name,
               section_value = x
            });
      }

      writer.WriteStartObject();
      writer.WritePropertyName("Items");

      serializer.Serialize(writer, items());
      writer.WriteEndObject();
   }

   private static Dictionary<string, Type> _knownTypes;

   private Dictionary<string, Type> GetKnownType()
   {
      return _knownTypes
            ?? (_knownTypes = Assembly.GetAssembly(typeof(ListItem))
               .GetTypes().Where(x => x.BaseType == typeof(ListItem))
               .ToDictionary(x => x.Name));
   }
   
   public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
   {
     var jsonObject = JObject.Load(reader);
      var items = jsonObject.GetValue("Items");
      var output = new SerializeableList<ListItem>();

      output.AddRange(
         items.Select(
            x => (ListItem)x["section_value"]
               .ToObject(GetKnownType()[x["section_type"]
                  .Value<string>()]))
            .ToList());

      return output;
   }

   public override bool CanConvert(Type objectType)
   {
      throw new NotImplementedException("Will not be used when using convnerter via attributes");
   }
}

With a list like this:

[JsonConverter(typeof(SerializeableListConverter))]
public class SerializeableList<T> : List<T> { }

And then the Json looks like this:

{
  "Items": [
    {
      "section_type": "FirstItemType",
      "section_value": {
        "Value": "value of first name",
        "Id": "16619e4b-b096-4d75-86e6-7d1a6898c3bd",
        "Name": "First item name"
      }
    },
    {
      "section_type": "SecondItemType",
      "section_value": {
        "Value": "value of second name",
        "Id": "e2f32af6-d2c1-4192-b64c-6bdc9e987224",
        "Name": "Second item name"
      }
    }
  ]
}

I really like this solution as we are not polluting our data, and not requiring our clients to do anything special to consume the data.

Michael Skarum avatar
About Michael Skarum
I'm Michael Skarum, an independent software developer, architect and consultant.