Десериализовать JSON, когда значение может быть объектом или пустым массивом

Я работаю с VK API. Иногда сервер может возвращать пустой массив вместо объекта, например:

personal: [] //when it is empty

or

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

Я десериализую большую часть json с помощью JsonConvert.DeserializeObject, а эту часть json с

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

Но это заставляет приложение работать медленно, когда оно обрабатывает много исключений. И только сейчас я понял, что вот еще несколько полей, которые могут возвращать массив, когда они пусты. У меня просто нет идей, как это сделать быстро и четко. Какие-либо предложения?

Мой класс десериализации (не работает, когда поле пустое):

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

Другая идея (не работает, когда не пусто):

public List<Personal> personal { get; set; }

person nikita_97_10    schedule 04.04.2015    source источник
comment
Попробуйте адаптировать SingleOrArrayConverter отсюда: stackoverflow.com/questions/18994685/   -  person dbc    schedule 04.04.2015
comment
Спасибо за хорошую идею, это то, что я искал, надеюсь, я смогу это реализовать...   -  person nikita_97_10    schedule 04.04.2015


Ответы (2)


Вы можете создать JsonConverter, как показано ниже, который ищет объект указанного типа или пустой массив. Если это объект, он десериализует этот объект. Если массив пустой, он возвращает 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;
    }
}

Затем используйте его как:

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

Теперь вы сможете десериализовать пользователя в свой класс User.

Примечания:

  • Преобразователь можно применить через атрибуты или в JsonSerializerSettings.Converters.

  • Преобразователь не предназначен для работы с простыми типами, такими как строки, он предназначен для классов, которые сопоставляются с объектом JSON. Это связано с тем, что он использует JsonSerializer.Populate(), чтобы избежать бесконечной рекурсии во время чтения.

Рабочий образец скриптов .Net здесь и здесь.

person dbc    schedule 04.04.2015
comment
Отличная идея! Большое спасибо! - person nikita_97_10; 04.04.2015
comment
CS1061 «Тип» не содержит определения для «IsAssignableFrom», и не удалось найти метод расширения «IsAssignableFrom», принимающий первый аргумент типа «Тип» (вам не хватает директивы использования или ссылки на сборку?) Здесь: public override bool CanConvert(Type objectType) { return typeof(T).IsAssignableFrom(objectType); } - person nikita_97_10; 04.04.2015
comment
@nikita_97_10 - objectType == typeof(T)) должно быть хорошо, если вы не создаете подклассы для своих классов. - person dbc; 05.04.2015
comment
а в windows phone 8.1 его нет, как мне кажется - person nikita_97_10; 05.04.2015

Вместо использования try catch для переключения между двумя вариантами просто проверьте первый символ. Если это '[', это null, если это '{', вы десериализуете.

РЕДАКТИРОВАТЬ:

Теперь, учитывая, что объект — это не весь JSON, это дает мне представление: у нас была аналогичная проблема с API, возвращающим несовместимые сериализации JSON. В итоге мы воспользовались библиотекой NewtonSoft ServiceStack.Text (доступной в NuGet). Мы сериализуем объекты JToken вместо целевого класса. Затем мы обработали структуры JToken для поэтапной десериализации.

person Boluc Papuccuoglu    schedule 04.04.2015
comment
Интересная идея, но проблема в том, что мне нужно делать это в каждой ситуации, а у меня много разных json-файлов. Было бы лучше понять это на самом уроке. Кроме того, это всего лишь небольшая часть json, и я не уверен, что смогу быстро ее проверить... - person nikita_97_10; 04.04.2015
comment
Понятно, у меня сложилось впечатление, что это весь JSON-файл. - person Boluc Papuccuoglu; 04.04.2015
comment
У нас была аналогичная проблема с API, возвращающим несовместимые сериализации JSON. В итоге мы воспользовались библиотекой NewtonSoft ServiceStack.Text (доступной в NuGet). Мы сериализуем объекты JToken вместо целевого класса. Затем мы обработали структуры JToken для поэтапной десериализации. - person Boluc Papuccuoglu; 04.04.2015
comment
Знаете ли вы, есть ли другие типы объектов, которые можно было бы десериализовать позже? - person nikita_97_10; 04.04.2015
comment
Да, JToken. В вашем случае, если объект пуст, JToken также будет JArray. Если это не так, это будет JObject (JToken является базовым классом для JArray и JObject, если я правильно помню). Вы можете проверить это с помощью оператора is и соответственно десериализовать. - person Boluc Papuccuoglu; 04.04.2015
comment
Если это stackoverflow.com/questions/18994685/ не сработает, попробую сделать как вы посоветовал - person nikita_97_10; 04.04.2015
comment
Я бы посоветовал вам попробовать это решение тоже. Это гораздо более элегантно и ремонтопригодно, чем мое предложение. - person Boluc Papuccuoglu; 04.04.2015