Сериализовать объекты, реализующие интерфейс с System.Text.Json

У меня есть мастер-класс, который содержит общую коллекцию. Элементы в коллекции имеют разные типы, и каждый реализует интерфейс.

Мастер класс:

public class MasterClass
{
    public ICollection<IElement> ElementCollection { get; set; }
}

Контракт на элементы:

public interface IElement
{
    string Key { get; set; }
}

Два образца для элементов:

public class ElementA : IElement
{
    public string Key { get; set; }

    public string AValue { get; set; }
}

public class ElementB : IElement
{
    public string Key { get; set; }

    public string BValue { get; set; }
}

Мне нужно сериализовать экземпляр объекта MasterClass, используя новую библиотеку System.Text.Json в Json. Используя следующий код,

public string Serialize(MasterClass masterClass)
{
    var options = new JsonSerializerOptions
    {
        WriteIndented = true,
    };
    return JsonSerializer.Serialize(masterClass, options);
}

Я получаю следующий JSON:

{
    "ElementCollection":
    [
        {
            "Key": "myElementAKey1"
        },
        {
            "Key": "myElementAKey2"
        },
        {
            "Key": "myElementBKey1"
        }
    ]
}

вместо того:

{
    "ElementCollection":
    [
        {
            "Key": "myElementAKey1",
            "AValue": "MyValueA-1"
        },
        {
            "Key": "myElementAKey2",
            "AValue": "MyValueA-2"
        },
        {
            "Key": "myElementBKey1",
            "AValue": "MyValueB-1"
        }
    ]
}

Какой класс (преобразователь, писатель, ...) я должен реализовать, чтобы получить полный JSON?

Заранее спасибо за помощь.


person Rom Eh    schedule 14.10.2019    source источник


Ответы (4)


Это работает для меня:

public class TypeMappingConverter<TType, TImplementation> : JsonConverter<TType>
  where TImplementation : TType
{
  [return: MaybeNull]
  public override TType Read(
    ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
      JsonSerializer.Deserialize<TImplementation>(ref reader, options);

  public override void Write(
    Utf8JsonWriter writer, TType value, JsonSerializerOptions options) =>
      JsonSerializer.Serialize(writer, (TImplementation)value!, options);
}

Использование:

var options =
   new JsonSerializerOptions 
   {
     Converters = 
     {
       new TypeMappingConverter<BaseType, ImplementationType>() 
     }
   };

JsonSerializer.Deserialize<Wrapper>(value, options);

Тесты:

[Fact]
public void Should_serialize_references()
{
  // arrange
  var inputEntity = new Entity
  {
    References =
    {
      new Reference
      {
        MyProperty = "abcd"
      },
      new Reference
      {
        MyProperty = "abcd"
      }
    }
  };

  var options = new JsonSerializerOptions
  {
    WriteIndented = true,
    Converters =
    {
      new TypeMappingConverter<IReference, Reference>()
    }
  };

      var expectedOutput =
@"{
  ""References"": [
    {
      ""MyProperty"": ""abcd""
    },
    {
      ""MyProperty"": ""abcd""
    }
  ]
}";

  // act
  var actualOutput = JsonSerializer.Serialize(inputEntity, options);

  // assert
  Assert.Equal(expectedOutput, actualOutput);
}

[Fact]
public void Should_deserialize_references()
{
  // arrange

  var inputJson =
@"{
  ""References"": [
    {
      ""MyProperty"": ""abcd""
    },
    {
      ""MyProperty"": ""abcd""
    }
  ]
}";

  var expectedOutput = new Entity
  {
    References =
    {
      new Reference
      {
        MyProperty = "abcd"
      },
      new Reference
      {
        MyProperty = "abcd"
      }
    }
  };

  var options = new JsonSerializerOptions
  {
    WriteIndented = true
  };

  options.Converters.AddTypeMapping<IReference, Reference>();

  // act
  var actualOutput = JsonSerializer.Deserialize<Entity>(inputJson, options);

  // assert
  actualOutput
      .Should()
      .BeEquivalentTo(expectedOutput);
}


public class Entity
{
  HashSet<IReference>? _References;
  public ICollection<IReference> References
  {
    get => _References ??= new HashSet<IReference>();
    set => _References = value?.ToHashSet();
  }
}

public interface IReference
{
  public string? MyProperty { get; set; }
}

public class Reference : IReference
{
  public string? MyProperty { get; set; }
}
person Shimmy Weitzhandler    schedule 01.11.2020
comment
Ваше решение сработало для меня, но можно ли использовать сопоставления, которые уже были созданы при настройке ioc, вместо их дублирования? - person YoyoS; 10.01.2021
comment
@YoyoS Вы можете зарегистрировать эти конвертеры с помощью настроек JSON, контролируемых IoC. - person Shimmy Weitzhandler; 11.01.2021
comment
Вы могли бы быть более ясными? Например, я уже зарегистрировал подобный приватный void ConfigureServices (ServiceCollection services) {services.AddScoped ‹IParentObject, ParentObject› (); services.AddScoped ‹IChildObject, ChildObject› (); } предотвращение дублирования с одними и теми же преобразователями = {new TypeMappingConverter ‹IParentObject, ParentObject› (), new TypeMappingConverter ‹IChildObject, ChildObject› (),} - person YoyoS; 11.01.2021
comment
У меня такая ошибка появляется с .NET5 и вашей реализацией Deserialize. System.InvalidOperationException: «Каждый параметр в конструкторе« Void .ctor (Lib.IChildA, Lib.IChildB) »типа« Lib.Parent »должен связываться со свойством объекта или полем при десериализации. Имя каждого параметра должно совпадать со свойством или полем объекта. Соответствие может быть нечувствительным к регистру. ' - person YoyoS; 17.01.2021
comment
В вашем примере у вас есть тип реализации obe, но в вопросе есть два типа, ElementA и ElementB - как мы можем создать преобразователь, который определяет реализацию на основе JSON, который он пытается преобразовать? - person Luke T O'Brien; 23.01.2021

Решение состоит в том, чтобы реализовать универсальный преобразователь (System.Text.Json.Serialization.JsonConverter):

public class ElementConverter : JsonConverter<IElement>
{
    public override IElement Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, IElement value, JsonSerializerOptions options)
    {
        if (value is ElementA)
            JsonSerializer.Serialize(writer, value as ElementA, typeof(ElementA), options);
        else if (value is ElementB)
            JsonSerializer.Serialize(writer, value as ElementB, typeof(ElementB), options);
        else
            throw new ArgumentOutOfRangeException(nameof(value), $"Unknown implementation of the interface {nameof(IElement)} for the parameter {nameof(value)}. Unknown implementation: {value?.GetType().Name}");
    }
}

Это просто требует доработки метода Read.

person Rom Eh    schedule 21.10.2019

То, что вы ищете, называется полиморфной сериализацией.

Вот статья документации Microsoft

Вот еще один вопрос об этом

Согласно документации, вам просто нужно преобразовать свой интерфейс в объект. Например:

public class TreeRow
{
    [JsonIgnore]
    public ICell[] Groups { get; set; } = new ICell[0];

    [JsonIgnore]
    public ICell[] Aggregates { get; set; } = new ICell[0];

    [JsonPropertyName("Groups")]
    public object[] JsonGroups => Groups;

    [JsonPropertyName("Aggregates")]
    public object[] JsonAggregates => Aggregates;


    public TreeRow[] Children { get; set; } = new TreeRow[0];
}
person Liam Kernighan    schedule 11.11.2020

У меня была такая же проблема, но моя проблема могла не быть связана с вашей. Оказывается, каждый объект, в который должны быть сериализованы входящие данные JSON, требует конструктора без аргументов. У всех моих объектов были конструкторы со всеми аргументами (чтобы упростить их создание и заполнение из базы данных).

person Tom Khoury    schedule 05.03.2021
comment
Вы пробовали добавить еще один аргумент без конструктора? - person Rom Eh; 17.03.2021