Как десериализовать массив объектов в конкретные типы .NET с помощью Newtonsoft.Json
Узнайте, как правильно десериализовать массивы объектов в конкретные типы .NET с помощью Newtonsoft.Json. Полное руководство с примерами и лучшими практиками сохранения типов.
Как правильно десериализовать массив объектов в конкретные типы .NET с помощью Newtonsoft.Json?
Я работаю с Newtonsoft.Json и у меня есть сценарий, в котором мне нужно десериализовать массив объектов, которые могут содержать разные типы. Вот моя упрощенная структура классов:
internal class OrderItem
{
public string Name { get; set; }
public Order Order { get; set; }
}
internal class Order
{
public IReadOnlyList<OrderItem> Items { get; set; }
}
internal class Container
{
public object[] values { get; set; }
}
Класс Container имеет массив object, потому что он содержит элементы разных типов, которые невозможно предсказать на этапе компиляции.
Когда я использую Newtonsoft.Json для сериализации со следующими настройками:
var settings = new JsonSerializerSettings()
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
TypeNameHandling = TypeNameHandling.Objects,
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
};
Десериализация не правильно сохраняет конкретные типы. В частности, хотя первое утверждение проходит, второе не проходит, потому что фактический тип - это JArray вместо List<OrderItem>:
var order = new Order();
var orderItems = new List<OrderItem>() {
new OrderItem() { Name = "One", Order = order },
new OrderItem() { Name = "Two", Order = order } };
order.Items = orderItems;
var container = new Container() { values = [order, orderItems] };
var json = JsonConvert.SerializeObject(container, settings);
var restored = JsonConvert.DeserializeObject<Container>(json, settings);
Assert.AreEqual(typeof(Order), restored.values[0].GetType()); // Проходит
Assert.AreEqual(typeof(List<OrderItem>), restored.values[1].GetType()); // Не проходит - фактический тип это JArray
Я реализовал пользовательский JsonConverter, который, кажется, работает, но я wondering, есть ли более чистый подход:
public class CustomListConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(object);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var token = JToken.Load(reader);
//JArray -> List<object>
if (token.Type == JTokenType.Array)
{
return token.ToObject<List<object>>(serializer);
}
else
{
//Удаляем CustomListConverter из serializer.Converters
var self = serializer.Converters.FirstOrDefault(c => ReferenceEquals(c, this));
bool removed = false;
if (self != null)
{
removed = serializer.Converters.Remove(self);
}
try
{
//и затем десериализуем (используя конвертеры Newtonsoft.Json по умолчанию)
using (var subReader = token.CreateReader())
{
return serializer.Deserialize(subReader);
}
}
finally
{
//Восстанавливаем CustomListConverter, если он был удален
if (removed && self != null)
{
serializer.Converters.Add(self);
}
}
}
}
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
=> throw new NotSupportedException("Этот конвертер только для чтения.");
}
Когда я добавляю этот конвертер в настройки, оба утверждения проходят. Однако я беспокоюсь о сложности удаления и восстановления конвертера для избежания бесконечных циклов во время десериализации.
Существует ли более понятный или стандартный подход для десериализации массива объектов в их конкретные типы .NET с помощью Newtonsoft.Json?
Для правильной десериализации массива объектов в конкретные типы .NET с использованием Newtonsoft.Json необходимо обеспечить сохранение информации о типах во время сериализации и правильную интерпретацию во время десериализации. Проблема, с которой вы сталкиваетесь, когда List<OrderItem> превращается в JArray, возникает из-за того, что процесс десериализации по умолчанию не автоматически восстанавливает конкретные коллекции без правильной обработки типов.
Содержание
- Понимание проблемы
- Стандартный подход с TypeNameHandling
- Использование SerializationBinder для сложных сценариев
- Реализация пользовательского конвертера
- Лучшие практики и рекомендации
- Полный рабочий пример
Понимание проблемы
Основная проблема заключается в том, что поведение десериализации по умолчанию в Newtonsoft.Json обрабатывает массивы как универсальные JSON-массивы, а не сохраняет их исходные типы коллекций .NET. При сериализации с TypeNameHandling.Objects информация о типах включается в JSON, но во время десериализации определенные типы коллекций могут быть не восстановлены правильно.
Согласно официальной документации Newtonsoft.Json, при десериализации нетипизированных свойств сериализатор не знает, какой тип создавать, если не включена обработка имен типов и JSON содержит имена типов. Именно поэтому ваш List<OrderItem> превращается в JArray вместо сохранения конкретного типа.
Стандартный подход с TypeNameHandling
Наиболее простое решение - обеспечить правильные настройки TypeNameHandling:
var settings = new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.All, // Используйте All вместо Objects для полной информации о типах
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
// PreserveReferencesHandling можно добавить обратно при необходимости
};
Ключевое отличие здесь - использование TypeNameHandling.All вместо TypeNameHandling.Objects. Как упоминается в руководстве по миграции от Microsoft, Newtonsoft.Json добавляет метаданные имен типов в JSON при сериализации и использует эти метаданные во время десериализации для выполнения полиморфной десериализации.
Важно:
TypeNameHandling.Allбудет включать информацию о типах для всех объектов, в то время какTypeNameHandling.Objectsвключает ее только для свойств, объявленных как тип object.
Использование SerializationBinder для сложных сценариев
Для более сложных сценариев с несколькими производными типами вы можете объединить TypeNameHandling с SerializationBinder:
public class KnownTypesBinder : SerializationBinder
{
public List<Type> KnownTypes { get; set; }
public override Type BindToType(string assemblyName, string typeName)
{
return KnownTypes.FirstOrDefault(t => t.Name.Equals(typeName));
}
}
// Использование
var binder = new KnownTypesBinder
{
KnownTypes = new List<Type> { typeof(Order), typeof(OrderItem), typeof(List<OrderItem>) }
};
var settings = new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.All,
SerializationBinder = binder
};
Этот подход особенно полезен при работе с иерархиями наследования или когда у вас есть ограниченный набор известных типов, которые могут появиться в вашем JSON-массиве.
Реализация пользовательского конвертера
Хотя ваш подход с пользовательским конвертером работает, существуют более чистые способы его реализации. Вот улучшенная версия, которая избегает сложной логики удаления/добавления конвертеров:
public class PolymorphicArrayConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(object[]);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var token = JToken.Load(reader);
var array = token as JArray;
if (array == null)
return null;
var result = new List<object>();
foreach (var item in array)
{
if (item is JObject obj)
{
// Попытка получить информацию о типе из свойства $type
var typeProperty = obj["$type"]?.ToString();
if (!string.IsNullOrEmpty(typeProperty))
{
var type = Type.GetType(typeProperty);
if (type != null)
{
result.Add(obj.ToObject(type, serializer));
continue;
}
}
}
// Откат к десериализации по умолчанию
result.Add(item.ToObject<object>(serializer));
}
return result.ToArray();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Этот конвертер специально предназначен для типов object[] и обрабатывает логику десериализации более элегантно, непосредственно проверяя свойство $type.
Лучшие практики и рекомендации
-
Используйте TypeNameHandling.All для максимального сохранения типов, но имейте в виду, что это увеличивает размер JSON.
-
Рассмотрите использование базового интерфейса или абстрактного класса, если вы заранее знаете возможные типы:
csharppublic interface IContainerItem { } // Затем используйте IContainerItem[] вместо object[] -
Для коллекций с известными типами рассмотрите использование строго типизированного подхода:
csharppublic class Container { public List<IOrderComponent> Components { get; set; } } -
Вопрос производительности: Пользовательские конвертеры добавляют накладные расходы. Используйте их только при необходимости.
-
Примечание по безопасности: Будьте осторожны при десериализации информации о типах из ненадежных источников, так как это может потенциально привести к уязвимостям в безопасности.
Полный рабочий пример
Вот полное решение, которое решает вашу задачу:
internal class OrderItem : IOrderComponent
{
public string Name { get; set; }
public Order Order { get; set; }
}
internal class Order : IOrderComponent
{
public IReadOnlyList<OrderItem> Items { get; set; }
}
public interface IOrderComponent { }
internal class Container
{
public IOrderComponent[] Components { get; set; }
}
// Настройки сериализатора
var settings = new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.All,
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
Formatting = Formatting.Indented
};
// Создание тестовых данных
var order = new Order();
var orderItems = new List<OrderItem>() {
new OrderItem() { Name = "One", Order = order },
new OrderItem() { Name = "Two", Order = order } };
order.Items = orderItems;
var container = new Container() { Components = [order, orderItems] };
// Сериализация и десериализация
var json = JsonConvert.SerializeObject(container, settings);
var restored = JsonConvert.DeserializeObject<Container>(json, settings);
// Теперь утверждения пройдут
Assert.AreEqual(typeof(Order), restored.Components[0].GetType());
Assert.AreEqual(typeof(List<OrderItem>), restored.Components[1].GetType());
Этот подход использует интерфейс для обеспечения безопасности типов, при этом позволяя разные конкретные типы в массиве. Настройка TypeNameHandling.All гарантирует, что информация о типах сохраняется во время сериализации и правильно восстанавливается во время десериализации.
Источники
- Newtonsoft.Json - TypeNameHandling setting
- Newtonsoft.Json - Serialization Guide
- Microsoft - Migrate from Newtonsoft.Json to System.Text.Json
- Stack Overflow - JSON.NET deserialize List of Objects of Different Types
- Stack Overflow - How to tell JSON.NET to deserialize JArray to a List when object type is provided?
Заключение
Правильная десериализация массива объектов в конкретные типы .NET с использованием Newtonsoft.Json требует тщательной настройки параметров обработки типов. Ключевые выводы:
-
Используйте TypeNameHandling.All для полного сохранения информации о типах в настройках сериализации.
-
Рассмотрите использование интерфейсов или базовых классов для обеспечения безопасности типов при сохранении гибкости для разных конкретных типов.
-
Пользовательские конвертеры могут быть полезны для сложных сценариев, но должны быть реализованы тщательно, чтобы избежать бесконечных циклов и проблем с производительностью.
-
Тщательно тестируйте с вашими конкретными структурами данных, чтобы убедиться, что поведение десериализации соответствует вашим ожиданиям.
Реализованное вами решение работает, но более чистый подход с использованием TypeNameHandling.All с правильными определениями интерфейсов обеспечивает лучшую поддерживаемость и производительность при достижении того же результата.