1

Consider the following json and models:

{
    "Reference": "Bay House 22",
    "Appliances": [
        {
            "Reference": "Kitchen Appliance 1",
            "ApplianceType": "0",
            "NumberOfSlots": 4
        },
        {
            "Reference": "Kitchen Appliance 2",
            "ApplianceType": "1",
            "Capacity": 1500
        }
    ]
}

public class HouseModel
{
    public String Reference { get; set; }
    [JsonConverter(typeof(ApplianceModelConverter))]
    public IEnumerable<IApplianceModel> Appliances { get; set; }
}

public interface IApplianceModel
{
    String Reference { get; set; }
    ApplianceType ApplianceType { get; set; } // this is an enum
}

public class ToasterModel : IApplianceModel
{
    public String Reference { get; set; }
    public ApplianceType ApplianceType { get; set; }

    public Int32 NumberOfSlots { get; set; }
}

public class KettleModel : IApplianceModel
{
    public String Reference { get; set; }
    public ApplianceType ApplianceType { get; set; }

    public Int32 Capacity { get; set; }
}

I'm trying to deserialize an IEnumerable which could be a Toaster or a Kettle, using a custom json converter. The idea here is that I can return a concrete type once I know what the ApplianceType is, by looking at the json. I've been following this stackoverflow post to try and get this to work, but without success.

Here is the converter code:

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

    public override Boolean CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }

    public override Boolean CanWrite
    {
        get { return false; }
    }

    public override Object ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer)
    {
        // Load JObject from stream
        JObject jObject = JObject.Load(reader);

        // Create target object based on JObject
        T target = Create(objectType, jObject);

        // Populate the object properties
        serializer.Populate(jObject.CreateReader(), target);

        return target;
    }
}

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

        ApplianceType applianceType = jObject.Value<ApplianceType>();

        switch (applianceType )
        {
            case ApplianceType.Kettle:
                return new KettleModel();
            case ApplianceType.Toaster: 
                return new ToasterModel();
            default:
                throw new InvalidEnumArgumentException("ApplianceType not supported");
        }
    }
}

Currently I get an exception thrown when this line is executed: JObject jObject = JObject.Load(reader); in the JsonCreationConverter

Newtonsoft.Json.JsonReaderException: 'Error reading JObject from JsonReader. Current JsonReader item is not an object: StartArray. Path 'Appliances'

I guess that it's getting to the IEnumerable and just failing, what am I doing wrong here?

Jack Pettinger
  • 2,715
  • 1
  • 23
  • 37

1 Answers1

2

Comment this line of code

    public class HouseModel
{
    public String Reference { get; set; }
    //[JsonConverter(typeof(ApplianceModelConverter))] //=> You don't need this
    public IEnumerable<IApplianceModel> Appliances { get; set; }
}

Also Change the Create method to this

 protected override IApplianceModel Create(Type objectType, JObject jObject)
    {
        if (jObject["ApplianceType"] == null) //case sensitive
        {
            throw new ArgumentException("Missing ApplianceType");
        }

        //ApplianceType applianceType = jObject.Value<ApplianceType>(); //this might throw invalid cast exception
        ApplianceType applianceType = jObject["ApplianceType"].ToObject<ApplianceType>();


        switch (applianceType)
        {
            case ApplianceType.Kettle:
                return new KettleModel();
            case ApplianceType.Toaster:
                return new ToasterModel();
            default:
                throw new InvalidEnumArgumentException("ApplianceType not supported");
        }
    }

If We are using this method to deserialize

string json = "{\"Reference\": \"Bay House 22\",\"Appliances\": [{\"Reference\": \"Kitchen Appliance 1\",\"ApplianceType\": \"0\",\"NumberOfSlots\": 4},{\"Reference\": \"Kitchen Appliance 2\",\"ApplianceType\": \"1\",\"Capacity\": 1500}]}";

HouseModel houseModels = JsonConvert.DeserializeObject<HouseModel>(json, new ApplianceModelConverter());
  • Thanks for answering but I don't think you've understood the question, you're saying to remove the converter and then saying to change the converter. It doesn't make sense. – Jack Pettinger Sep 05 '18 at 13:59
  • string json = "{\"Reference\": \"Bay House 22\",\"Appliances\": [{\"Reference\": \"Kitchen Appliance 1\",\"ApplianceType\": \"0\",\"NumberOfSlots\": 4},{\"Reference\": \"Kitchen Appliance 2\",\"ApplianceType\": \"1\",\"Capacity\": 1500}]}"; HouseModel houseModels = JsonConvert.DeserializeObject(json, new ApplianceModelConverter()); assuming this is how you are converting what I mentioned above should work – Ankan Mookherjee Sep 05 '18 at 14:25
  • Yes! My apologies, it seems I didn't understand your answer. Not the other way around. TY – Jack Pettinger Sep 05 '18 at 15:32