Другое

Как десериализовать массив объектов в конкретные типы .NET с помощью Newtonsoft.Json

Узнайте, как правильно десериализовать массивы объектов в конкретные типы .NET с помощью Newtonsoft.Json. Полное руководство с примерами и лучшими практиками сохранения типов.

Как правильно десериализовать массив объектов в конкретные типы .NET с помощью Newtonsoft.Json?

Я работаю с Newtonsoft.Json и у меня есть сценарий, в котором мне нужно десериализовать массив объектов, которые могут содержать разные типы. Вот моя упрощенная структура классов:

csharp
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 для сериализации со следующими настройками:

csharp
var settings = new JsonSerializerSettings()
{
    PreserveReferencesHandling = PreserveReferencesHandling.Objects,
    TypeNameHandling = TypeNameHandling.Objects,
    ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
};

Десериализация не правильно сохраняет конкретные типы. В частности, хотя первое утверждение проходит, второе не проходит, потому что фактический тип - это JArray вместо List<OrderItem>:

csharp
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, есть ли более чистый подход:

csharp
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, возникает из-за того, что процесс десериализации по умолчанию не автоматически восстанавливает конкретные коллекции без правильной обработки типов.

Содержание

Понимание проблемы

Основная проблема заключается в том, что поведение десериализации по умолчанию в Newtonsoft.Json обрабатывает массивы как универсальные JSON-массивы, а не сохраняет их исходные типы коллекций .NET. При сериализации с TypeNameHandling.Objects информация о типах включается в JSON, но во время десериализации определенные типы коллекций могут быть не восстановлены правильно.

Согласно официальной документации Newtonsoft.Json, при десериализации нетипизированных свойств сериализатор не знает, какой тип создавать, если не включена обработка имен типов и JSON содержит имена типов. Именно поэтому ваш List<OrderItem> превращается в JArray вместо сохранения конкретного типа.

Стандартный подход с TypeNameHandling

Наиболее простое решение - обеспечить правильные настройки TypeNameHandling:

csharp
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:

csharp
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-массиве.

Реализация пользовательского конвертера

Хотя ваш подход с пользовательским конвертером работает, существуют более чистые способы его реализации. Вот улучшенная версия, которая избегает сложной логики удаления/добавления конвертеров:

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

Лучшие практики и рекомендации

  1. Используйте TypeNameHandling.All для максимального сохранения типов, но имейте в виду, что это увеличивает размер JSON.

  2. Рассмотрите использование базового интерфейса или абстрактного класса, если вы заранее знаете возможные типы:

    csharp
    public interface IContainerItem { }
    
    // Затем используйте IContainerItem[] вместо object[]
    
  3. Для коллекций с известными типами рассмотрите использование строго типизированного подхода:

    csharp
    public class Container
    {
        public List<IOrderComponent> Components { get; set; }
    }
    
  4. Вопрос производительности: Пользовательские конвертеры добавляют накладные расходы. Используйте их только при необходимости.

  5. Примечание по безопасности: Будьте осторожны при десериализации информации о типах из ненадежных источников, так как это может потенциально привести к уязвимостям в безопасности.

Полный рабочий пример

Вот полное решение, которое решает вашу задачу:

csharp
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 гарантирует, что информация о типах сохраняется во время сериализации и правильно восстанавливается во время десериализации.

Источники

  1. Newtonsoft.Json - TypeNameHandling setting
  2. Newtonsoft.Json - Serialization Guide
  3. Microsoft - Migrate from Newtonsoft.Json to System.Text.Json
  4. Stack Overflow - JSON.NET deserialize List of Objects of Different Types
  5. Stack Overflow - How to tell JSON.NET to deserialize JArray to a List when object type is provided?

Заключение

Правильная десериализация массива объектов в конкретные типы .NET с использованием Newtonsoft.Json требует тщательной настройки параметров обработки типов. Ключевые выводы:

  1. Используйте TypeNameHandling.All для полного сохранения информации о типах в настройках сериализации.

  2. Рассмотрите использование интерфейсов или базовых классов для обеспечения безопасности типов при сохранении гибкости для разных конкретных типов.

  3. Пользовательские конвертеры могут быть полезны для сложных сценариев, но должны быть реализованы тщательно, чтобы избежать бесконечных циклов и проблем с производительностью.

  4. Тщательно тестируйте с вашими конкретными структурами данных, чтобы убедиться, что поведение десериализации соответствует вашим ожиданиям.

Реализованное вами решение работает, но более чистый подход с использованием TypeNameHandling.All с правильными определениями интерфейсов обеспечивает лучшую поддерживаемость и производительность при достижении того же результата.

Авторы
Проверено модерацией
Модерация