Search code examples
c#jsonjson.netvk

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


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; }

Solution

  • 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.