Search code examples
c#json.netderived-class

Deserializing JSON with List of abstract class


In my project i receive a JSON which represents a Configuration for an device. Each device has one or more Interfaces that it can be connected with. In the property "SupportedInterfaces" i receive a List with all Interfaces that are supported for this device. The List expects the type BaseInterface but i'll receive the derived classes "RestInterface", "ModbusTcpInterface" or more to come. I'm trying to deserialize List<BaseInterface> and directly convert it into the specific derived class type for further usage and to store in the DB, with the custom BaseInterfaceConverter. But that does not work like expected.

The following Code is my current state, i did not get it running - and for me it looks like it is because of the "nested" List inside the JSON that i'd like to deserialize. The Error in the Console is:

"Error reading JObject from JsonReader. Current JsonReader item is not an object: StartArray.

My code looks like this:

// The Parent Class
public class BaseInterface
{

 public string Name { get; set; }

 // This defines the specific interface-type 
 // TODO: change to an enumeration
 public string InterfaceType { get; set; }

 public string Option { get; set; }

}

// First Child
public class RestInterface : BaseInterface
{

 public string DefaultBaseUri { get; set; }

}


// 2nd Child
public class ModbusTcpInterface: BaseInterface
{

 public string IpAddress { get; set; }

 public int Port { get; set; }
}


// The Configuration which holds a list of Interfaces that i would like to parse while deserializing.

public class DeviceConfiguration 
{

 public string DeviceName { get; set; }

 public string DeviceManufacturer { get; set; }

 [JsonConverter(typeof(BaseInterfaceConverter))]
 public List<BaseInterface> SupportedInterfaces { get; set; }

 //... more props here

}

public class BaseInterfaceConverter : JsonConverter
{

  public override bool CanConvert(Type objectType)
  {
    return objectType == typeof(BaseInterface);
  }

  public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
  {
    JObject jobject = JObject.Load(reader);
    switch (jobject["InterfaceType"].Value<string>())
    {
      case "Rest":
        return JsonConvert.DeserializeObject<RestInterface>(jobject.ToString());
      case "ModbusTcp":
        return JsonConvert.DeserializeObject<ModbusTcpInterface>(jobject.ToString());
      default:
        throw new ArgumentException(String.Format("The Interfacetype {0} is not supported!", jobject["InterfaceType"].Value<string>()));
    }
  }

  public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
  {
    serializer.Serialize(writer, value);
  }
}

What is that i'm missing? Can anyone please give me a hint?

Edit 1, added the JSON that I receive:

{
    "_id": "1234",
    "DeviceName": "First Configuration",
    "DeviceManufacturer": "eeeitschi",
    "SupportedInterfaces": [
        {
            "Name": "My first interface",
            "InterfaceType": "Rest",
            "Option": "option string here..",
            "DefaultBaseUri": "mybaseurl.io/itsme",
          },
          {
            "Name": "My second interface",
            "InterfaceType": "ModbusTcp",
            "Option": "option string here..",
            "IpAddress": "127.0.0.1",
            "Port": 502
          },
          {
            "Name": "My third interface",
            "InterfaceType": "Rest",
            "Option": "option string here..",
            "DefaultBaseUri": "base.url/api/devices",
          },
    ]
}

Edit 2, rewrote the BaseInterfaceConverter after the answers from Max and Serge:

public class BaseInterfaceConverter : JsonConverter
{

  public override bool CanConvert(Type objectType)
  {
    return objectType == typeof(BaseInterface);
  }

  public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
  {
    JArray jarray = JArray.Load(reader);
    return jarray.Select(entry => {
      switch (entry["InterfaceType"].Value<string>())
      {
        case "Rest":
           return JsonConvert.DeserializeObject<RestInterface>(entry.ToString());
        case "ModbusTcp":
           return JsonConvert.DeserializeObject<ModbusTcpInterface>(entry.ToString());
        default:
           throw new ArgumentException(String.Format("The Interfacetype {0} is not supported!", entry["InterfaceType"].Value<string>()));
      }
    }).ToList();
}

Solution

  • You can try this code

    var jObject = JObject.Parse(json);
    
    DeviceConfiguration deviceConfiguration = jObject.ToObject<DeviceConfiguration>();
    
    deviceConfiguration.SupportedInterfaces = ((JArray)jObject["SupportedInterfaces"])
        .Select(x => (string)x["InterfaceType"] == "Rest" ? (BaseInterface)x.ToObject<RestInterface>()
                                                    : (BaseInterface)x.ToObject<ModbusTcpInterface>())
        .ToList();
                                                    
    

    or you can wrap it into converter

    public class BaseClassConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(List<BaseInterface>);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
       {
         JArray jArray = JArray.Load(reader);
         return jArray.Select(x => (string)x["InterfaceType"] == "Rest" ? (BaseInterface)x.ToObject<RestInterface>()
                                                        : (BaseInterface)x.ToObject<ModbusTcpInterface>()).ToList();
        } 
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            serializer.Serialize(writer, value);
        }
    }
    

    but if you have an access to code that creates a json string, you can change it by using TypeNameHandling setting

        var jsonSerializerSettings = new JsonSerializerSettings()
        {
            TypeNameHandling = TypeNameHandling.All
        };
    var json = JsonConvert.SerializeObject(deviceConfiguration, jsonSerializerSettings);
    

    In this case it will be much more simple code to deserialize

    deviceConfiguration=JsonConvert.DeserializeObject<DeviceConfiguration>(json, jsonSerializerSettings);