В настоящее время мы поддерживаем интеграцию 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 свойство, которое принимало бы как «[]», так и «{}» как представления пустого списка во время десериализации? Сторонняя сторона совершенно не согласна с этим, поскольку некоторые типы ответов десериализуются аккуратно, а некоторые нет.
Или, если мне нужно написать собственный десериализатор, как мне с этим правильно справиться?
Я тоже думал об этом, хотя со всеми местами, которые нужно будет изменить, это будет примерно так же ужасно. Кроме того, существует проблема, заключающаяся в том, что многие из их параметров имеют одинаковые имена для разных типов объектов, но имеют разные значения. Таким образом, одна из этих позиций может иметь собственный параметр «info», который является фактическим типом объекта. В результате предварительная обработка замены струн кажется мне более рискованной.
string json = Newtonsoft.Json.JsonConvert.SerializeObject(new GenericNamedResponseObject()); получил -->> {"code":null,"msg":null,"info":[]}Да, я думаю, что у вас что-то не так с вашей конфигурацией Newtonsoft или сериализацией через какую-то другую библиотеку, и я предполагаю, что сериализацию выполняет Newtonsoft, потому что мои тесты с вашим кодом возвращают "info" : [] также для пустого списка точно так же, как у вас настроен метод доступа к свойству. .
По поводу двух последних комментариев: мне кажется, что проблема OP заключается в том, что API не находится под их контролем, здесь возможна только десериализация.
@Pac0 Моя вина. Я вижу. Однако третья сторона сериализует пустые списки в некоторых своих ответах таким образом, чего мы не ожидаем. Вот пример. Похоже, что это ошибка, на которую необходимо указать третьей стороне, поскольку она сериализуется в разные типы на основе данных. Должен сериализоваться в один и тот же тип
О, я в курсе, что это баг на их стороне ТС, и на него было указано (и будет еще раз). Просто очень высока вероятность того, что на исправление потребуется очень много времени.
Решить эту проблему можно с помощью кастома JsonConverter. См. Как обрабатывать один элемент и массив для одного и того же свойства с помощью JSON.net. Класс SingleOrArrayConverter<T> в первом ответе должен стать хорошей отправной точкой.
Вот еще один пример: Как десериализовать свойство JSON, которое может иметь два разных типа данных, с помощью Json.NET
использовать объект. поэтому позже вы сможете преобразовать тип объекта в любой известный класс. dotnetfiddle.net/KFXISp пример.





Эту проблему можно решить, используя специальный 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
Лично я бы добавил обработку нулевых и объектных типов токенов (и выдал бы исключение в случае какого-либо другого типа токена).
@GuruStron Конечно, вы наверняка могли бы добавить для этого дополнительную обработку. Я просто пытался сделать все максимально простым, основываясь на вариантах использования, описанных в вопросе.
Другой обходной путь — сначала предварительно обработать json, то есть, например, просто заменить все
"info": {}на"info" : []. Эта предварительная обработка также позволяет четко отделить «грязные хаки» (вызванные неуклюжей конструкцией API) от фактической десериализации.