4

I've got a JSON response that is contained in an outer array like this:

[
  {
    "type": "a"
  },
  {
    "type": "b",
    "id": 1
  },
  {
    "type": "c",
    "name": "somename"
  }
]

I've tried to convert this to object like this:

class LoginOptions
{
    public IList<ILoginOption> Options { get; set; }
}

interface ILoginOption
{
    [JsonProperty("type")]
    LoginType LoginType { get; }
}

class LoginOptionA : ILoginOption{
    public LoginType LoginType
    {
        get { return LoginType.A; }
    }
}

class LoginOptionB : ILoginOption{
    public LoginType LoginType
    {
        get { return LoginType.B; }
    }

    [JsonProperty("id")]
    public int Id { get; set; }
}

class LoginOptionC : ILoginOption{
    public LoginType LoginType
    {
        get { return LoginType.C; }
    }

    [JsonProperty("name")]
    public string Name { get; set; }
}

Which results in this exception:

Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'Library.Models.Domain.LoginOptions' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly. To fix this error either change the JSON to a JSON object (e.g. {"name":"value"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array.

I would rather not implement a collection in my LoginOptions class since that would be way too much overhead when I should be able to just store it in the field. Using the [JsonArray] attribute returns a Cannot create and populate list type Library.Models.Domain.LoginOptions.

Most resources I've found deal with a {"name":"value"} pair for the top array, not an anonymous array. How should I deserialize this?

I've changed my code accordingly:

public sealed class LoginOptions
{    
    [JsonConverter(typeof(LoginOptionConverter))]
    public IList<ILoginOption> Options { get; set; }
}

Where my call dispatcher parses the JSON as such:

private List<ILoginOption> DeserializeObject<List<ILoginOption>>(Stream stream)
{
   using (StreamReader sr = new StreamReader(stream))
   using (JsonReader reader = new JsonTextReader(sr))
   {
       return new JsonSerializer().Deserialize<List<ILoginOption>>(reader);
   }
}

And a custom converter like this:

internal class LoginOptionConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(ILoginOption);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var item = JObject.Load(reader);
        var data = item["type"].Value<string>();

        if (data == "UsernamePassword")
        {
            return item.ToObject<LoginOptionA>();
        }

        if (data == "Saml")
        {
            return item.ToObject<LoginOptionB>();
        }

        if (data == "Crm")
        {
            return item.ToObject<LoginOptionC>();
        }

        throw new ArgumentException("Invalid JSON response");
    }
}

This throws the error

Could not create an instance of type Library.Models.Domain.ILoginOption. Type is an interface or abstract class and cannot be instantiated.

Using

new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Objects
};

as advised here did not make a difference.

Note that this error is thrown before ever making it inside the custom converter: when the JsonSerializer is created it throws this error.

Instantiation happens here:

var settings = new JsonSerializerSettings
{
     TypeNameHandling = TypeNameHandling.Objects
};

using (StreamReader sr = new StreamReader(stream))
using (JsonReader reader = new JsonTextReader(sr))
{
    return JsonSerializer.Create(settings).Deserialize<List<ILoginOption>>(reader);
}
Community
  • 1
  • 1
Jeroen Vannevel
  • 43,651
  • 22
  • 107
  • 170

1 Answers1

7

Since the top level in the JSON is an array, you should deserialize directly to a list. There is no need for a wrapper class to hold the list.

List<ILoginOption> loginOptions = 
                   JsonConvert.DeserializeObject<List<ILoginOption>>(json);

Now, because your list will hold several different types of ILoginObjects, Json.Net will not know which ones to create as it deserializes the list. For that you will need a JsonConverter. This answer shows how you can create a converter to handle this.

Community
  • 1
  • 1
Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • I have changed my code so that I can call the deserializing process with a collection and moved it into the actual object further down the line. This still caused an error though (see edit question). Have I forgotten something else? – Jeroen Vannevel Jun 20 '14 at 12:31
  • Alright, I managed to get past that. According to [this](http://stackoverflow.com/questions/5780888/casting-interfaces-for-deserialization-in-json-net) you have to add the converter through `serializer.Converters.Add` and not by using an attribute. This only results in me having a single loginoption (the first) instead of all 3 though. – Jeroen Vannevel Jun 20 '14 at 12:51
  • Aaand you can disregard that. Looks like the essence of my problem was me targetting a wrong environment that resulted in a response of just one option. – Jeroen Vannevel Jun 20 '14 at 13:09