Десериализовать сложную структуру JSON

Я работаю над приложением С#, которое извлекает данные из API. Ответ от API содержит блоки, содержащие узел Data, который меняет структуру и имеет только один общий атрибут, Type. Я использую Ньютонсофт. Как я могу десериализовать узел Data в точный класс на основе атрибута Type? Так что, если я создаю массив элементов, при запросе массива я могу включить атрибут Type и узнать, какой объект ему соответствует.

Ответ JSON от API выглядит так:

{
    "nextLink": "NextLink",
    "value": [
        {
            "Block": {
                "BlockName": "Name",
                "BlockClass": "Class",
                "IsVisibile": true,
                "Data": {
                    "Type": "Footer",
                    "Values": [
                        { 
                            "Property1": "Property1",
                            "Property2": "Property2"
                        }
                    ],
                    "Show": false
                }
            }
        },
        {
            "Block": {
                "BlockName": "Name",
                "BlockClass": "Class",
                "IsVisibile": true,
                "Data": {
                    "Type": "Header",
                    "Title": "Main title",
                    "Subtitle": "Subtitle"
                }
            }
        },
        {
            "Block": {
                "BlockName": "Name",
                "BlockClass": "Class",
                "IsVisibile": true,
                "Data": {
                    "Type": "Body",
                    "Information": "Info",
                    "AdditionalText": "More text",
                    "Projects": [
                        "Project1",
                        "Project2"
                    ]
                }
            }
        }
    ]
}

Это классы, которые я определил на основе ответа API:

public class Blocks
{
    public string NextLink { get; set; }
    public Response[] Value { get; set; }
}

public class Response
{
    public Block Block { get; set; }
}

public class Block
{
    public string BlockName { get; set; }
    public string BlockClass { get; set; }
    public bool IsVisible { get; set; }
    public Data Data { get; set; }
}

public class Data 
{
    public string Type { get; set; }
    ???
}

public class FooterType
{
    public bool Show { get; set; }
    public object[] Values { get; set; }
}

public class HeaderType 
{
    public string Title { get; set; }
    public string Subtitle { get; set; }
}

public class BodyType
{
    public string Information { get; set; }
    public string AdditionalText { get; set; }
    public object[] Projects { get; set; }
}

Если я десериализую ответ на конкретный тип (как показано ниже), он, конечно, работает для этого узла в JSON, но не для всех из них.

public class Data 
{
    public string Type { get; set; }
    public string Title { get; set; }
    public string Subtitle { get; set; }
}

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

То, что вы ищете, называется полиморфной сериализацией и десериализацией. Вы можете это сделать (как зависит от сериализатора - и версии). Судя по всему, последний System.Text.Json делает это из коробки. Заставить его работать может быть неприятно

Flydog57 29.04.2023 05:45

Спасибо @Flydog57. Я читал о System.Text.Json, но, к сожалению, он доступен только в .Net7, а я использую .Net6. В любом случае я смог заставить его работать с приведенными ниже решениями. Спасибо.

PureJoyCoder 02.05.2023 02:45
Стоит ли изучать 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
2
115
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

В этом примере используется Newtonsoft в качестве библиотеки де/сериализации. Json.Net

public class DataConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType) => typeof(Data).IsAssignableFrom(objectType); 

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var obj = JObject.Load(reader);
            // Read the target Type from the json data object
            var dataType = (string)obj["Type"];
            var item = new Data();
            // Determine what is the correct sub class for your data
            switch(dataType)
            {
                case "Header": 
                    item = new HeaderType();
                    break;
                case "Body": 
                    item = new BodyType();
                    break;
                case "Footer": 
                    item = new FooterType();
                    break;
            }
            // Populate the instance with the data provided in json
            serializer.Populate(obj.CreateReader(), item);
            return item;
        }

        public override bool CanWrite => false;
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotSupportedException();
    }

Вот рабочий пример для вашего случая в dotnetfiddle

// Then just use it in your ´JsonConvert.DeserializeObject´ method
var blocks = JsonConvert.DeserializeObject<Blocks>(json, new DataConverter());

Спасибо, Мартин. В итоге я объединил ваше решение и решение Сержа.

PureJoyCoder 02.05.2023 02:49
Ответ принят как подходящий

Самый простой способ - создать полиморфные классы и использовать конструктор свойств json.

    Root root = JsonConvert.DeserializeObject<Root>(json);

public class Root
{
    public string nextLink { get; set; }
    public List<Item> value { get; set; }
}
public class Item
{
    public Block Block { get; set; }
}
public class Block
{
    public string BlockName { get; set; }
    public string BlockClass { get; set; }
    public bool IsVisibile { get; set; }
    public DataBase Data { get; set; }
    
    [JsonConstructor]
    public Block (JObject Data)
    {
        this.Data = (DataBase)Data.ToObject(Type.GetType((string)Data["Type"]));
    }
    public Block() {}
}

public class DataBase
{
    public string Type { get; set;}
    //public string Type { get { return this.GetType().Name; } }
}

public class Footer : DataBase
{
    public List<Properties> Values { get; set; }
    public bool Show { get; set; }
}

public class Header : DataBase
{
    public string Title { get; set; }
    public string Subtitle { get; set; }
}

public class Body : DataBase
{
    public string Information { get; set; }
    public string AdditionalText { get; set; }
    public List<string> Projects { get; set; }
}

public class Properties
{
    public string Property1 { get; set; }
    public string Property2 { get; set; }
}

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

Root root = JsonConvert.DeserializeObject<Root>(json);

public class DataPropertyConverter : JsonConverter<DataBase>
{
    public override DataBase ReadJson(JsonReader reader, Type objectType, DataBase existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        var jObj = JObject.Load(reader);
         return (DataBase)jObj.ToObject(Type.GetType((string)jObj["Type"]));
    }
    
    public override bool CanWrite => false;
    public override void WriteJson(JsonWriter writer, DataBase value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

public class Block
{
    public string BlockName { get; set; }
    public string BlockClass { get; set; }
    public bool IsVisibile { get; set; }

    [JsonConverter(typeof(DataPropertyConverter))]
    public DataBase Data { get; set; }
}

Спасибо. Удалось заставить его работать таким образом с оговоркой, что API не возвращает полное пространство имен, которое необходимо Type.GetType(). Я взломал способ, чтобы он работал с вашим решением и @Martin на случай, если я обнаружу проблемы с первым.

PureJoyCoder 02.05.2023 02:47

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