Deserialize JSON when a value can be an object or an empty array
Asked Answered
D

2

3

I`m working with VK API. Sometimes server can return empty array instead of object, for example:

personal: [] //when it is empty

or

personal: {
religion: 'Нет',
smoking: 1,
alcohol: 4
} //when not empty.

I`m deserializing most of json with JsonConvert.DeserializeObject, and this part of json with

MainObject = ((MainObject["response"].GetObject())["user"].GetObject())["personal"].GetObject();
try
{
Convert.ToByte(MainObject["political"].GetNumber();
} 
catch {}

But it makes app works slowly when it`s handling a lot of exeptions. And just now i realised that here are some more fields that might return array when empty. I just have no ideas how to make it fastly and clearly. Any suggestions?

My deserializing class (doen`t work when field is empty):

     public class User
            {
//some other fields...
                public Personal personal { get; set; }
//some other fields...
             }
    public class Personal
            {
                public byte political { get; set; }
                public string[] langs { get; set; }
                public string religion { get; set; }
                public string inspired_by { get; set; }
                public byte people_main { get; set; }
                public byte life_main { get; set; }
                public byte smoking { get; set; }
                public byte alcohol { get; set; }
            }

Another idea (doesn`t work when not empty):

public List<Personal> personal { get; set; }
Driving answered 4/4, 2015 at 17:55 Comment(2)
Try adapting SingleOrArrayConverter from here: #18995185Vanlandingham
Thanks for a good idea, it is something I was looking for, hope I can realise it...Driving
V
5

You could make a JsonConverter like the following, that looks for either an object of a specified type, or an empty array. If an object, it deserializes that object. If an empty array, it returns null:

public class JsonSingleOrEmptyArrayConverter<T> : JsonConverter where T : class
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var contract = serializer.ContractResolver.ResolveContract(objectType);
        if (!(contract is Newtonsoft.Json.Serialization.JsonObjectContract || contract is Newtonsoft.Json.Serialization.JsonDictionaryContract))
        {
            throw new JsonSerializationException(string.Format("Unsupported objectType {0} at {1}.", objectType, reader.Path));
        }

        switch (reader.SkipComments().TokenType)
        {
            case JsonToken.StartArray:
                {
                    int count = 0;
                    while (reader.Read())
                    {
                        switch (reader.TokenType)
                        {
                            case JsonToken.Comment:
                                break;
                            case JsonToken.EndArray:
                                return existingValue;
                            default:
                                {
                                    count++;
                                    if (count > 1)
                                        throw new JsonSerializationException(string.Format("Too many objects at path {0}.", reader.Path));
                                    existingValue = existingValue ?? contract.DefaultCreator();
                                    serializer.Populate(reader, existingValue);
                                }
                                break;
                        }
                    }
                    // Should not come here.
                    throw new JsonSerializationException(string.Format("Unclosed array at path {0}.", reader.Path));
                }

            case JsonToken.Null:
                return null;

            case JsonToken.StartObject:
                existingValue = existingValue ?? contract.DefaultCreator();
                serializer.Populate(reader, existingValue);
                return existingValue;

            default:
                throw new InvalidOperationException("Unexpected token type " + reader.TokenType.ToString());
        }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

public static partial class JsonExtensions
{
    public static JsonReader SkipComments(this JsonReader reader)
    {
        while (reader.TokenType == JsonToken.Comment && reader.Read())
            ;
        return reader;
    }
}

Then use it like:

public class User
{
    //some other fields...
    [JsonConverter(typeof(JsonSingleOrEmptyArrayConverter<Personal>))]
    public Personal personal { get; set; }
    //some other fields...
}

You should now be able to deserialize a user into your User class.

Notes:

  • The converter can be applied via attributes or in JsonSerializerSettings.Converters.

  • The converter isn't designed to work with simple types such as strings, it's designed for classes that map to a JSON object. That's because it uses JsonSerializer.Populate() to avoid an infinite recursion during reading.

Working sample .Net fiddles here and here.

Vanlandingham answered 4/4, 2015 at 18:59 Comment(4)
CS1061 'Type' does not contain a definition for 'IsAssignableFrom' and no extension method 'IsAssignableFrom' accepting a first argument of type 'Type' could be found (are you missing a using directive or an assembly reference?) Here: public override bool CanConvert(Type objectType) { return typeof(T).IsAssignableFrom(objectType); }Driving
@Driving - objectType == typeof(T)) should be fine as long as you're not subclassing your classes.Vanlandingham
@Driving - Type.IsAssignableFrom is a standard method on type, I think it's present in most frameworks.Vanlandingham
but in windows phone 8.1 it doesn`t exists, as I thinkDriving
A
0

Instead of using try catch to switch between two possibilities, just check the first character. If it is '[', it's null, if it is '{' then you deserialize.

EDIT:

Now considering that the object is not the whole of the JSON, it gives me an idea: We had a similar problem with API returning inconsistent JSON serializations. In the end, we used NewtonSoft's ServiceStack.Text library (available from NuGet). We serialized to JToken objects instead of the target class. Then we processed the JToken structures to do piecemeal deserialization.

Androsterone answered 4/4, 2015 at 18:6 Comment(7)
Interesting idea, but the problem is that I need to do it in every situation, and i have a lot of different json files. It would be better to realize that in class itself. Also, it is only the little part of json, and I`m not sure can I check It fast...Driving
I see, I was under the impression that this was the whole JSON fileAndrosterone
We had a similar problem with API returning inconsistent JSON serializations. In the end, we used NewtonSoft's ServiceStack.Text library (available from NuGet). We serialized to JToken objects instead of the target class. Then we processed the JToken structures to do piecemeal deserialization.Androsterone
Do you know, are there some other types of objects that might be deserialized larer?Driving
Yes, JToken. In your case if the object is empty, JToken will also be a JArray. If it's not, it will be a JObject (JToken is the base class for JArray and JObject if I recall correctly). You can test for that using the "is" operator and deserialize accordingly.Androsterone
If this #18995185 will not work, I'll try to make it as you advisedDriving
I would advise you to try that solution too. It is much more elegant and maintainable than my suggestion.Androsterone

© 2022 - 2024 — McMap. All rights reserved.