System.Text.Json Десериализовать вложенный полиморфный объект без дискриминатора типа

Предположим, что приведена ниже модель:

    public enum TypeAModels
    {
       A = 1,
       B = 2
    }

   [JsonDerivedType(typeof(TypeA_A), "aa")]
   [JsonDerivedType(typeof(TypeA_B), "ab")]

    public abstract class TypeA
    {
        public abstract TypeAModels TypeAModels { get; }
    }
    public class TypeA_A : TypeA
    {
        public int Age { get; set; }
    
        public TypeB TypeB { get; set; }
        public override TypeAModels TypeAModels => TypeAModels.A;
    }
    
    public class TypeA_B : TypeA
    {
        public string Name { get; set; }
        public override TypeAModels TypeAModels => TypeAModels.B;
    }
    
    public enum TypeBModels
    {
        A = 1,
        B = 2
    }
    
    [JsonDerivedType(typeof(TypeB_A), "ba")]
    [JsonDerivedType(typeof(TypeB_B), "bb")]
    public abstract class TypeB
    {
        public abstract TypeBModels TypeBModels { get; }
    }
    public class TypeB_A : TypeB
    {
        public int Year { get; set; }
        public override TypeBModels TypeBModels => TypeBModels.A;
    }
    
    public class TypeB_B : TypeB
    {
        public string UserName { get; set; }
        public override TypeBModels TypeBModels => TypeBModels.B;
    }

//create a TypeA object of type TypeA_A
TypeA typeAA = new TypeA_A() { Age = 30, TypeB = new TypeB_A { Year = 1982 } };

var jsonString = JsonSerializer.Serialize(typeAA, new JsonSerializerOptions());

/*{
    "$type":"aa",
    "Age":30,
    "TypeB":{"$type":"ba",
             "Year":1982,
             "TypeBModels":1},
    "TypeAModels":1}*/

var deserializedTypeAA = JsonSerializer.Deserialize<TypeA>(jsonString, new JsonSerializerOptions());

Когда я запускаю код JsonSerializer.Serialize, я получаю ожидаемый JSON, а когда я запускаю код JsonSerializer.Deserialize<TypeA>, ему также удается создать правильный объект из JSON.

Проблема начинается, когда у меня есть конечная точка MinimalApi, например:

app.MapPost("/", async (TypeA typeA).

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

{
  "Age": 30,
  "TypeB": {
    "Year": 1982,
    "TypeBModels": 1
   },
  "TypeAModels": 1
}

Для таких запросов сериализация JSON завершается с ошибкой с общим исключением: System.NotSupportedException: 'Deserialization of types without a parameterless constructor, a singular parameterized...

Единственный способ, который я могу придумать, - это прочитать и проанализировать «вручную» каждое поле JSON отдельно. Это возможно, но это не идеальный способ, ИМХО. Есть ли более элегантный способ?

Примечание. Я нашел это API-предложение, которое может стать хорошим решением, когда оно будет реализовано, но в настоящее время оно еще не реализовано.

Сообщение об ошибке вводит в заблуждение. Вам нужно применить атрибуты или использовать JsonConverter для десериализации полиморфной модели. См. этот ответ и другие вопросы Возможна ли полиморфная десериализация в System.Text.Json?. Достаточно ли этот общий вопрос отвечает на ваш вопрос или вам нужна более конкретная помощь?

dbc 05.08.2024 18:31

@dbc Спасибо за ваш комментарий! Я постарался прояснить, что мне нужно, пожалуйста, взгляните на измененный вопрос. Я видел ссылки, прежде чем спрашивать, но не нашел нужного решения. Спасибо!

Roni 07.08.2024 14:33

Есть ли у вас муравьиный контроль над сериализованным форматом JSON? Встроенная поддержка полиморфизма в System.Text.Json требует, чтобы дискриминатор типа был первым свойством объекта, а ваше, "TypeAModels": 1, — последним. Можно ли это изменить?

dbc 07.08.2024 17:56
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
3
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Пока ваши производные типы не запечатаны, вы можете применить к ним [JsonDerivedType(typeof(TSelf))], чтобы System.Text.Json выдавал самоидентификатор при сериализации. Таким образом, если я изменю ваши модели, например. следующее:

public enum TypeAModels
{
   A = 1,
   B = 2
}

[JsonPolymorphic(TypeDiscriminatorPropertyName = MyTypeDiscriminatorPropertyName)]  
[JsonDerivedType(typeof(TypeA_A), (int)TypeAModels.A)]
[JsonDerivedType(typeof(TypeA_B), (int)TypeAModels.B)]
public abstract class TypeA
{
    protected const string MyTypeDiscriminatorPropertyName = "TypeAModels";
}

[JsonPolymorphic(TypeDiscriminatorPropertyName = MyTypeDiscriminatorPropertyName)]  
[JsonDerivedType(typeof(TypeA_A), (int)TypeAModels.A)] // Self identifier
public class TypeA_A : TypeA
{
    public int Age { get; set; }
    public TypeB TypeB { get; set; }
}

[JsonPolymorphic(TypeDiscriminatorPropertyName = MyTypeDiscriminatorPropertyName)]  
[JsonDerivedType(typeof(TypeA_B), (int)TypeAModels.B)] // Self identifier
public class TypeA_B : TypeA
{
    public string Name { get; set; }
}

public enum TypeBModels
{
    A = 1,
    B = 2
}

[JsonPolymorphic(TypeDiscriminatorPropertyName = MyTypeDiscriminatorPropertyName)]  
[JsonDerivedType(typeof(TypeB_A), (int)TypeBModels.A)]
[JsonDerivedType(typeof(TypeB_B), (int)TypeBModels.B)]
public abstract class TypeB
{
    protected const string MyTypeDiscriminatorPropertyName = "TypeBModels";
}

[JsonPolymorphic(TypeDiscriminatorPropertyName = MyTypeDiscriminatorPropertyName)]  
[JsonDerivedType(typeof(TypeB_A), (int)TypeBModels.A)] // Self identifier
public class TypeB_A : TypeB
{
    public int Year { get; set; }
}

[JsonPolymorphic(TypeDiscriminatorPropertyName = MyTypeDiscriminatorPropertyName)]  
[JsonDerivedType(typeof(TypeB_B), (int)TypeBModels.B)] // Self identifier
public class TypeB_B : TypeB
{
    public string UserName { get; set; }
}

Теперь я могу сериализовать экземпляр TypeA_A и десериализовать как TypeA:

TypeA_A typeAA = new() { Age = 30, TypeB = new TypeB_A { Year = 1982 } };

var json = JsonSerializer.Serialize(typeAA, options);

var typeABack = JsonSerializer.Deserialize<TypeA>(json);

Примечания:

  1. Я исключил ваши свойства TypeAModels и TypeBModels в пользу использования JsonPolymorphicAttribute.TypeDiscriminatorPropertyName. Это предотвращает дублирование информации о типе в сериализованном JSON, но требует, чтобы дискриминатор типа появился первым в JSON:

    {
      "TypeAModels": 1,   // This must be the first property    
      "Age": 30,
      "TypeB": {
        "TypeBModels": 1,   // This must be the first property    
        "Year": 1982
      }
    }
    
  2. Microsoft отказывается разрешать отправку информации о дискриминаторе типов при сериализации запечатанного типа. См. System.Text.Json Polymorphic Type Resolve Issue #77532 , который был закрыт MSFT как «ответ», и [Предложение API]: Полиморфный атрибут System.Text.Json должен предоставлять возможность включать дискриминаторы типов в производные Types #93471, который был открыт специально для сериализации информации о типе для запечатанных производных типов.

    Если ваши производные типы запечатаны, вам может потребоваться принять совершенно другую стратегию, например, использовать ручной преобразователь, подобный одному из Возможна ли полиморфная десериализация в System.Text.Json? или переписать конечную точку для явной сериализации с использованием базового типа, а не конкретного типа.

  3. Если вы не хотите вручную применять параметры полиморфизма к производным типам, вы можете создать собственный модификатор typeinfo для копирования параметров полиморфизма из контрактов базового типа в контракты производного типа. См., например. этот ответ от Гуру Строна на Как я могу сериализовать многоуровневую иерархию полиморфных типов с помощью System.Text.Json в .NET 7?для одного примера. Возможно, его необходимо улучшить, например. чтобы скопировать имя дискриминатора типа.

Демо-рабочий пример здесь.

Это очень хороший способ решить проблему дискриминатора типов. Недостатком здесь является то, что: 1. Клиент должен сохранять порядок полей (что не является стандартом). 2. Модель не отражает структуру JSON. Я имею в виду, что клиент должен указать TypeAModels, но модель этого не отражает (это защищенное поле). Я предпочитаю в таких случаях выбирать ручной способ. В любом случае, я отмечу ваш ответ как принятый, поскольку вы описали все доступные подходы.

Roni 08.08.2024 12:17

@Roni - Пункт 1 действительно является недостатком. Не уверен, что понимаю пункт 2. Если вам все еще нужно свойство дискриминатора типа TypeAModels по другим причинам, вы можете сохранить его, пока пометите его и все его переопределения как [JsonIgnore]. Я удалил его, так как он показался лишним. См. dotnetfiddle.net/mbsJLJ. Должен ли я добавить эту информацию в вопрос?

dbc 08.08.2024 16:09

Я имею в виду, что если я взгляну на точку зрения клиента, предполагая, что он хочет POST нового TypeA_A. Клиент знает только модель TypeA_A(Age, TypeB(Year)), поэтому JSON, который он отправит, будет таким: { "age: 30, "typeB": { "year": 1982 }} . Но на самом деле я должен получить и TypeAModels, и TypeBModels. Это более понятно? Спасибо!!

Roni 08.08.2024 17:27

@Roni - В вашем исходном вопросе были указаны дискриминаторы типа, такие как "TypeAModels": 1, так почему же клиент о них не знает? Использует ли клиент те же модели C# + платформу, что и сервер? Разные модели, но все же C#/.NET? Или совершенно другая технология, например. JavaScript? Я ответил на ваш вопрос, предполагая, что вас устраивает показанный «клиентский JSON», разве это не так?

dbc 08.08.2024 17:31

@Roni - в любом случае, если вам вообще не нужны дискриминаторы типов, вам нужно как-то изменить свой подход. Одним из способов было бы добавить JsonConverter<TypeA>, который предварительно загружает JsonDocument и проверяет наличие Age, TypeB или Name. Другой вариант — устранить иерархию классов и добавить все возможные свойства к TypeA и TypeB и условно игнорировать их, если они не установлены.

dbc 08.08.2024 18:05

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