6

I am using an API that returns a json object that I need to deserialize. My problem is that one of the members of those object is sometimes an empty array ("[]") and sometimes a dictionary ("{"1":{...}, "2":{...}}"). I want to deserialize it into either an array or a dictionary, since I don't car about the IDs, I just want a list of all the objects. Here is how I deserialize the object:

var response = JsonConvert.DeserializeObject<Response>(json);

And here is the definition of the Response class:

public class Response
{
    [JsonProperty(PropertyName = "variations")]
    public Dictionary<int, Variation> Variations { get; set; }
}

It works well when the Response contains a dictionary in it's variations field, but it fails when it contains an empty array. I'm getting an error from Newtonsoft saying that an array cannot be deserialized into a dictionary. If I define the Variations property as an array, it works for empty arrays, but it fails when it is a dictionary. What could I do to either deserialize correctly both possible values, or to ignore empty arrays and set Variations to null when it's an array instead of failing.

Thanks.

James Newton-King
  • 48,174
  • 24
  • 109
  • 130
Carl
  • 1,224
  • 2
  • 19
  • 35
  • 2
    Well, what a bum source :( I bet it is coming from PHP, huh? –  Aug 31 '12 at 20:32
  • I have seen suggestions to "text replace before converting", but I think it could also be done via a custom JsonConverter. –  Aug 31 '12 at 20:38
  • @pst : That's a great idea, did't think of doing that. – Carl Aug 31 '12 at 20:40
  • If referring to the text replace, I don't think I'd call it "great", but perhaps "hack-ish and working in a pinch". It is easy to imagine degenerate data .. if in .NET4 I believe it would be possible to use `dynamic` with a wrapper accessor (but I do not use .NET4 or dynamic). –  Aug 31 '12 at 20:41
  • @pst : It's true that it's a hack, but I don't see when changing [] for {} might alter the data. As for the dynamic keyword, it does deserialize, but I'm not very familiar with how I can use the object afterwards. – Carl Aug 31 '12 at 20:51
  • @pst : By the way, if you post an answer regarding the use of dynamic, I'll accept it as an answer. – Carl Aug 31 '12 at 20:55
  • Should be able to do something like `IDictionary<..,..> Variations { get { return (_variations is IList /* Array or List but not Dictionary */) ? new Dictionary<..,..>() : (IDictionary<..,..>)_variations; } }`. Just post the solution you ended up using :) Also, will it work with just `object` and not `dynamic` for the mapped `_variation` property? –  Aug 31 '12 at 21:15
  • As for the text-replace solution: `{foo: "string that contains things like variations=[] that should be left alone"}`. Granted the input may never be as such, but .. one day, somewhere, this, or something similar, will jump up and bite you. –  Aug 31 '12 at 21:21
  • It will probably work with just object instead of dynamic. I'll try that on Tuesday (I won't be at work before then) and let you know. Thanks for your help! – Carl Sep 02 '12 at 14:23

3 Answers3

9

Here an alternative solution using JsonConverter

public class Response
{
    [JsonProperty(PropertyName = "variations")]
    [JsonConverter(typeof(EmptyArrayOrDictionaryConverter))]
    public Dictionary<int, Variation> Variations { get; set; }
}


public class EmptyArrayOrDictionaryConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType.IsAssignableFrom(typeof(Dictionary<string, object>));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);
        if (token.Type == JTokenType.Object)
        {
            return token.ToObject(objectType, serializer);
        }
        else if (token.Type == JTokenType.Array)
        {
            if (!token.HasValues)
            {
                // create empty dictionary
                return Activator.CreateInstance(objectType);
            }
        }

        throw new JsonSerializationException("Object or empty array expected");
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}
StackOverthrow
  • 1,158
  • 11
  • 23
2

Here is a variation (sorry for the pun) on Carl's example. I had a similar need, but instead of returning a dictionary, I needed an array. The API I am using says it returns an array. However, in the case where there is only one item in the result, it is returned as an object instead!

public class VarationsContainer
{
    [JsonIgnore]
    public Varation[] Varations
    {
        get
        {
            return ParseObjectToArray<Variation>(VariationObject);
        }
    }

    [JsonProperty(PropertyName = "varations")]
    public object VarationsObject { get; set; }

    protected T[] ParseObjectToArray<T>(object ambiguousObject)
    {
        var json = ambiguousObject.ToString();
        if (String.IsNullOrWhiteSpace(json))
        {
            return new T[0]; // Could return null here instead.
        }
        else if (json.TrimStart().StartsWith("["))
        {
            return JsonConvert.DeserializeObject<T[]>(json);
        }
        else
        {
            return new T[1] { JsonConvert.DeserializeObject<T>(json) };
        }
    }
}

I hope this is useful to some other sad API consumer.

Todd
  • 12,995
  • 3
  • 30
  • 25
1

Here is the solution I used :

    public Dictionary<int, Variation> Variations
    {
        get
        {
            var json = this.VariationsJson.ToString();
            if (json.RemoveWhiteSpace() == EmptyJsonArray)
            {
                return new Dictionary<int, Variation>();
            }
            else
            {
                return JsonConvert.DeserializeObject<Dictionary<int, Variation>>(json);
            }
        }
    }

    [JsonProperty(PropertyName = "variations")]
    public object VariationsJson { get; set; }

Basically, the variations are first deserialized in a basic object. When I want to read the value, I check if the object is an empty array, and if so I return an empty dictionary. If the object is a good dictionary, I deserialize it and return it.

Carl
  • 1,224
  • 2
  • 19
  • 35