Десериализация C# Newtonsoft JSON: значение пустого списка, представленное как «{}» вместо «[]», вызывает исключение

В настоящее время мы поддерживаем интеграцию API со сторонней организацией, которая передает ответы через JSON. Мы используем C# .NET и Newtonsoft JSON (версия 13.0.1) для сериализации запросов и десериализации ответов.

Однако третья сторона сериализует пустые списки в некоторых своих ответах таким образом, которого мы не ожидаем. Вот пример.

Когда параметр списка содержит значения

{
    "code": 0,
    "msg": "OK",
    "info": [
        {/* Line Item 1 */}, 
        {/* Line Item 2*/}
    ]
}

(с заменой позиций 1 и 2 на действительный JSON для другого типа объекта)

Когда параметр списка пуст

{
    "code": 0,
    "msg": "OK",
    "info": {}
}

Насколько я могу судить, когда параметр списка/коллекции должен быть нулевым или пустым, он либо должен быть "[]", либо просто полностью отсутствует. И то, и другое было бы хорошо с точки зрения десериализации. Вместо этого мы получаем обратное исключение.

Newtonsoft.Json.JsonSerializationException: невозможно десериализовать текущий объект JSON (например, {"name":"value"}) в тип 'System.Collections.Generic.IList`1[GenericResponseLineItem]', поскольку для этого типа требуется массив JSON (например, [ 1,2,3]) для правильной десериализации. Чтобы исправить эту ошибку, либо измените JSON на массив JSON (например, [1,2,3]), либо измените десериализованный тип, чтобы он был обычным типом .NET (например, не примитивным типом, таким как целое число, а не типом коллекции, например массив или список), который можно десериализовать из объекта JSON. JsonObjectAttribute также можно добавить к типу, чтобы принудительно выполнить его десериализацию из объекта JSON.

Вот как я сейчас определяю класс в C#

[Serializable]
public class GenericNamedResponseObject
{
    [JsonProperty("code")]
    public int? Code { get; set; }

    [JsonProperty("msg")]
    public string ErrorMessage { get; set; }
    
    private IList<GenericResponseLineItem> _info;

    [JsonProperty("info")]
    public IList<GenericResponseLineItem> Info
    {
        get => _info ?? (_info = new List<GenericResponseLineItem>());
        set => _info = value;
    }
    
}

Текущее решение

На данный момент у меня есть обходной путь, который выполняет попытку десериализации моего GenericNamedResponseObject, а затем пытается десериализовать написанный мной объект GenericNamedResponseAlternateFormatting, в котором параметр Info полностью отсутствует.

Это работает, но это уродливый хак, и мы видим еще больше ответов от третьих лиц, имеющих аналогичный формат.

Есть ли в Newtonsoft JSON свойство, которое принимало бы как «[]», так и «{}» как представления пустого списка во время десериализации? Сторонняя сторона совершенно не согласна с этим, поскольку некоторые типы ответов десериализуются аккуратно, а некоторые нет.

Или, если мне нужно написать собственный десериализатор, как мне с этим правильно справиться?

Другой обходной путь — сначала предварительно обработать json, то есть, например, просто заменить все "info": {} на "info" : []. Эта предварительная обработка также позволяет четко отделить «грязные хаки» (вызванные неуклюжей конструкцией API) от фактической десериализации.

Pac0 03.07.2024 23:33

Я тоже думал об этом, хотя со всеми местами, которые нужно будет изменить, это будет примерно так же ужасно. Кроме того, существует проблема, заключающаяся в том, что многие из их параметров имеют одинаковые имена для разных типов объектов, но имеют разные значения. Таким образом, одна из этих позиций может иметь собственный параметр «info», который является фактическим типом объекта. В результате предварительная обработка замены струн кажется мне более рискованной.

Alex 03.07.2024 23:40
string json = Newtonsoft.Json.JsonConvert.SerializeObject(new GenericNamedResponseObject()); получил -->> {"code":null,"msg":null,"info":[]}
T.S. 04.07.2024 01:12

Да, я думаю, что у вас что-то не так с вашей конфигурацией Newtonsoft или сериализацией через какую-то другую библиотеку, и я предполагаю, что сериализацию выполняет Newtonsoft, потому что мои тесты с вашим кодом возвращают "info" : [] также для пустого списка точно так же, как у вас настроен метод доступа к свойству. .

Steve Py 04.07.2024 01:21

По поводу двух последних комментариев: мне кажется, что проблема OP заключается в том, что API не находится под их контролем, здесь возможна только десериализация.

Pac0 04.07.2024 01:30

@Pac0 Моя вина. Я вижу. Однако третья сторона сериализует пустые списки в некоторых своих ответах таким образом, чего мы не ожидаем. Вот пример. Похоже, что это ошибка, на которую необходимо указать третьей стороне, поскольку она сериализуется в разные типы на основе данных. Должен сериализоваться в один и тот же тип

T.S. 04.07.2024 02:21

О, я в курсе, что это баг на их стороне ТС, и на него было указано (и будет еще раз). Просто очень высока вероятность того, что на исправление потребуется очень много времени.

Alex 04.07.2024 04:00

Решить эту проблему можно с помощью кастома JsonConverter. См. Как обрабатывать один элемент и массив для одного и того же свойства с помощью JSON.net. Класс SingleOrArrayConverter<T> в первом ответе должен стать хорошей отправной точкой.

Brian Rogers 04.07.2024 18:05

использовать объект. поэтому позже вы сможете преобразовать тип объекта в любой известный класс. dotnetfiddle.net/KFXISp пример.

Power Mouse 04.07.2024 19:04
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
10
87
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Эту проблему можно решить, используя специальный JsonConverter , аналогичный тому, который можно найти в этом ответе на Как обрабатывать как отдельный элемент, так и массив для одного и того же свойства с помощью JSON.net. В вашем случае вы хотите вернуть пустой список, если вы получаете в JSON что-то кроме массива. Вот конвертер, который это делает:

public class SafeArrayConverter<T> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(List<T>));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);
        if (token.Type == JTokenType.Array)
        {
            return token.ToObject<List<T>>();
        }
        return new List<T>();
    }

    public override bool CanWrite => false;

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

Чтобы использовать конвертер, вам просто нужно добавить атрибут [JsonConverter] к свойству Info в вашем классе GenericNamedResponseObject, например:

    [JsonProperty("info")]
    [JsonConverter(typeof(SafeArrayConverter<GenericResponseLineItem>))]
    public IList<GenericResponseLineItem> Info
    {
        get => _info ?? (_info = new List<GenericResponseLineItem>());
        set => _info = value;
    }

Вот рабочая демо: https://dotnetfiddle.net/HnQTPM

Лично я бы добавил обработку нулевых и объектных типов токенов (и выдал бы исключение в случае какого-либо другого типа токена).

Guru Stron 04.07.2024 20:25

@GuruStron Конечно, вы наверняка могли бы добавить для этого дополнительную обработку. Я просто пытался сделать все максимально простым, основываясь на вариантах использования, описанных в вопросе.

Brian Rogers 04.07.2024 20:32

Другие вопросы по теме