System.Text.Json: как десериализовать класс со свойствами интерфейса (.NET 6)

В настоящее время я занимаюсь миграцией vom Newtonsoft на System.Text.Json. Newtonsoft смогла автоматически десериализовать объекты, имеющие одно или несколько свойств интерфейса. С System.Text.Json я получаю следующее сообщение об ошибке для соответствующих классов, когда пытаюсь выполнить то же самое:

each parameter in the deserialization constructor must bind to an object property or field on deserialization. Each parameter name must match with a property or field on the object.

Я могу избежать этой проблемы, написав пользовательские преобразователи, но это приведет к большим накладным расходам для объектов с несколькими вложенными слоями, где каждое свойство интерфейса может снова иметь несколько свойств интерфейса (таким образом, требуется несколько пользовательских преобразователей). Есть ли более простое решение этой проблемы?

Я создаю пример, чтобы проиллюстрировать проблему:

 [Fact]
        public void JsonTest()
        {

            var child = new Child(new Name("Peter"), 10);
            var parent = new Parent(child);

            var str = JsonSerializer.Serialize(parent);
            var jsonObj = JsonSerializer.Deserialize<Parent>(str);
            Console.WriteLine(jsonObj!.Child.Name);
        }
    }

    [JsonConverter(typeof(ParentConverter))]
    public class Parent
    {
        public Parent(Child child)
        {
            Child = child;
        }

        public IChild Child { get; set;}
    }

    [JsonConverter(typeof(ChildConverter))]
    public class Child : IChild
    {
        [JsonConstructor]
        public Child(Name name, int age)
        {
            Name = name;
            Age = age;
        }

        public IName Name { get; set; }
        public int Age { get; set; }
    }

    public class Name : IName
    {
        public string NameValue { get; set; }
        public Name(string nameValue)
        {
            NameValue = nameValue;
        }
    }

    public interface IChild
    {
        IName Name { get; }
        int Age { get; }
    }

    public interface IName
    {
        string NameValue { get; }
    }

    public class ParentConverter : JsonConverter<Parent>
    {
        public override Parent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            using (JsonDocument document = JsonDocument.ParseValue(ref reader))
            {
                JsonElement root = document.RootElement;

                if (root.TryGetProperty("Child", out JsonElement childElement))
                {
                    Child? child = JsonSerializer.Deserialize<Child>(childElement.GetRawText(), options);

                    return new Parent(child!);
                }
            }

            throw new JsonException("Invalid JSON data");
        }

        public override void Write(Utf8JsonWriter writer, Parent value, JsonSerializerOptions options)
        {
            writer.WriteStartObject();

            writer.WritePropertyName("Child");
            JsonSerializer.Serialize(writer, value.Child, options);

            writer.WriteEndObject();
        }
    }

    public class ChildConverter : JsonConverter<Child>
    {
        public override Child Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            using (JsonDocument document = JsonDocument.ParseValue(ref reader))
            {
                var name = new Name("Alex");
                var age = 20;

                JsonElement root = document.RootElement;

                if (root.TryGetProperty("Name", out JsonElement nameElement))
                {
                    name = JsonSerializer.Deserialize<Name>(nameElement.GetRawText(), options);
                }

                if (root.TryGetProperty("Age", out JsonElement ageElement))
                {
                    age = JsonSerializer.Deserialize<int>(ageElement.GetRawText(), options);
                }
                return new Child(name!, age);
            }

            throw new JsonException("Invalid JSON data");
        }

        public override void Write(Utf8JsonWriter writer, Child value, JsonSerializerOptions options)
        {
            writer.WriteStartObject();

            writer.WritePropertyName("Name");
            JsonSerializer.Serialize(writer, value.Name, options);

            writer.WritePropertyName("Age");
            JsonSerializer.Serialize(writer, value.Age, options);

            writer.WriteEndObject();
        }
    }

С этими пользовательскими конвертерами я получаю ожидаемое поведение. Но есть ли способ избежать написания собственного конвертера для каждого такого класса?

Ваши конструкторы берут конкретные типы, но затем отображают их как интерфейс. Поэтому вам следует попробовать использовать интерфейс также в конструкторе (передача IChild, а не Child и IName, а не Name), чтобы сигнатура конструктора соответствовала поверхности класса.

Ralf 22.06.2023 09:57

Десериализация типов интерфейса не поддерживается в System.Text.Json, так что это не поможет моей проблеме.

Steve 22.06.2023 10:55

Ах. Я понимаю. Это бы исправило выделенное сообщение об ошибке, которое вы показали. Возможно, вы захотите удалить его, так как он отвлекает от реальной проблемы.

Ralf 22.06.2023 11:15

Как вы просите что-то проще. Почему бы просто не использовать Json.Net. Насколько я понимаю, даже Microsoft рассматривает System.Text.Json как еще один вариант, а не как замену.

Ralf 22.06.2023 11:16

Основными причинами являются согласованность и отсутствие какого-либо нежелательного поведения при использовании нескольких разных библиотек. Мотивация перехода на System.Text.Json в основном связана с повышением производительности. Спасибо за ваш вклад!

Steve 22.06.2023 11:21
Стоит ли изучать 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
5
58
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Это называется полиморфной сериализацией. Библиотеке нужен какой-то способ аннотировать конкретную реализацию IChild свойства. Я думаю, что Newtonsoft добавляет свойство с полным именем типа, хотя это просто в использовании, у него есть некоторые потенциальные недостатки, если вы хотите переименовать свой класс.

System.Text.Json использует атрибуты для аннотирования производных типов и требует, чтобы вы предоставили идентификаторы. Подробнее о Дискриминаторах полиморфных типов.

    [JsonDerivedType(typeof(Child1), typeDiscriminator: "Child1")]
    [JsonDerivedType(typeof(Child2), typeDiscriminator: "Child2")]
    public interface IChild
    {
        int Age { get; }
    }

    public class Child1 : IChild
    {
        public string PreferedToy { get; set; }
        public int Age { get; set; }
    }
    public class Child2 : IChild
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

Если у вас есть только одна реализация каждого интерфейса, я бы подумал либо об избавлении от интерфейсов, либо о преобразовании ваших объектов в объекты передачи данных (DTO), которые максимально просты для сериализации. Использование DTO помогает отделить проблемы сериализации от любой логики в ваших классах.

Прежде всего, спасибо за ваш быстрый ответ. К сожалению, похоже, что эта функция недоступна для .NET 6. Я рассмотрю реструктуризацию кода (например, удаление ненужных интерфейсов), но если это невозможно, я думаю, мне придется создать обходной путь, аналогичный тому, который я опубликовано.

Steve 22.06.2023 11:01

@Steve Это должно быть доступно как минимум в System.Text.Json 7.0.2, так как я тестировал его там. И эта версия должна быть полностью совместима с .net 4.6.2. Однако в документации это не очень ясно.

JonasH 22.06.2023 11:34

Ах, хорошо, я, к сожалению, все еще получаю то же исключение при аннотации интерфейса IChild с помощью: [JsonDerivedType(typeof(Child), typeDiscriminator: "Child")] и интерфейса IName с помощью: [JsonDerivedType(typeof(Name), typeDiscriminator: "Name ")]

Steve 22.06.2023 12:04

Хорошо, при изменении типов параметров конструктора с Child на IChild и Name на IName это работает. Большое спасибо за Вашу помощь!

Steve 22.06.2023 12:14

При написании этого примера я забыл учесть одну вещь: в моем реальном приложении я получаю json из http-запроса и не сериализую его самостоятельно. Затем json не обогащается необходимыми метаданными для правильной десериализации. Любое решение для этого?

Steve 22.06.2023 14:04

@ Стив, я не уверен. Если у вас нет информации о типе, вы не можете поддерживать полиморфизм. Я бы просто использовал шаблон dto для таких случаев.

JonasH 22.06.2023 14:13

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