使用 System.Text.Json 序列化实现接口的对象

Serialize objects implementing interface with System.Text.Json

我有一个母版 class,其中包含一个通用集合。集合中的元素有不同的类型,每个元素都实现了一个接口。

硕士class:

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; }
}

我需要使用 Json 中的新 System.Text.Json 库序列化 MasterClass 对象的一个​​实例。使用以下代码,

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"
        }
    ]
}

我应该实施哪个 class(转换器、写入器...)以获得完整的 JSON?

在此先感谢您的帮助。

解决方案是实现通用转换器 (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 方法做更多的工作。

这对我有用:

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; }
}

您要查找的内容称为多态序列化

Here's Microsoft documentation article

根据文档,您只需将界面转换为对象即可。 例如:

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];
}

我遇到过同样的问题,但我的问题可能与你的问题无关。事实证明,传入的 JSON 数据必须序列化到的每个对象都需要一个不带参数的构造函数。我所有的对象都有带有所有参数的构造函数(以便更容易地从数据库创建和填充它们)。

我目前在 Blazor 应用程序中遇到了同样的问题,所以我无法轻松切换到 Newtonsoft.Json。我找到了两种方法。一个是现实黑客。您可以创建自定义转换器,在 Read/Write 方法中使用 Newtonsoft.Json,而不是 System.Text.Json。但这不是我想要的。所以我制作了一些自定义接口转换器。我有一些可行的解决方案,尚未经过广泛测试,但它可以满足我的需要。

情况

我有一个 List<TInterface> 对象实现 TInterface。但是有很多不同的实现。我需要在服务器上序列化数据,并在客户端 WASM 应用程序上反序列化所有数据。对于JavaScript反序列化,后面提到的自定义Write方法的实现就足够了。对于 C# 中的反序列化,我需要知道为列表中的每个项目序列化的对象的确切类型。

首先,我需要JsonConverterAttribute界面。所以我关注了这篇文章:https://khalidabuhakmeh.com/serialize-interface-instances-system-text-jsonWriter 的一些实现将处理接口类型。但是没有 Read 实现。所以我不得不自己做。

如何

  • 修改 Write 方法以将对象类型作为第一个 属性 写入 JSON 对象。使用 JsonDocument 从原始对象获取所有属性。
  • 在阅读 JSON 时,使用克隆 reader(如 Microsoft docs 中针对自定义 json 转换器的建议)找到第一个 属性 命名为 $type 带有类型信息。比创建该类型的实例并使用类型从原始 reader.
  • 反序列化数据

代码

接口和类:

[JsonInterfaceConverter(typeof(InterfaceConverter<ITest>))]
public interface ITest
{
    int Id { get; set; }
    string Name { get; set; }
}

public class ImageTest : ITest
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Image { get; set; } = string.Empty;
}

public class TextTest : ITest
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Text { get; set; } = string.Empty;
    public bool IsEnabled { get; set; }
}

接口转换器属性:

// Source: https://khalidabuhakmeh.com/serialize-interface-instances-system-text-json
[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)]
public class JsonInterfaceConverterAttribute : JsonConverterAttribute
{
    public JsonInterfaceConverterAttribute(Type converterType)
        : base(converterType)
    {
    }
}

转换器:

public class InterfaceConverter<T> : JsonConverter<T>
    where T : class
{
    public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        Utf8JsonReader readerClone = reader;
        if (readerClone.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException();
        }

        readerClone.Read();
        if (readerClone.TokenType != JsonTokenType.PropertyName)
        {
            throw new JsonException();
        }

        string propertyName = readerClone.GetString();
        if (propertyName != "$type")
        {
            throw new JsonException();
        }

        readerClone.Read();
        if (readerClone.TokenType != JsonTokenType.String)
        {
            throw new JsonException();
        }

        string typeValue = readerClone.GetString();
        var instance = Activator.CreateInstance(Assembly.GetExecutingAssembly().FullName, typeValue).Unwrap();
        var entityType = instance.GetType();

        var deserialized = JsonSerializer.Deserialize(ref reader, entityType, options);
        return (T)deserialized;
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        switch (value)
        {
            case null:
                JsonSerializer.Serialize(writer, (T)null, options);
                break;
            default:
                {
                    var type = value.GetType();
                    using var jsonDocument = JsonDocument.Parse(JsonSerializer.Serialize(value, type, options));
                    writer.WriteStartObject();
                    writer.WriteString("$type", type.FullName);

                    foreach (var element in jsonDocument.RootElement.EnumerateObject())
                    {
                        element.WriteTo(writer);
                    }

                    writer.WriteEndObject();
                    break;
                }
        }
    }
}

用法:

    var list = new List<ITest>
    {
        new ImageTest { Id = 1, Name = "Image test", Image = "some.url.here" },
        new TextTest { Id = 2, Name = "Text test", Text = "kasdglaskhdgl aksjdgl asd gasdg", IsEnabled = true },
        new TextTest { Id = 3, Name = "Text test 2", Text = "asd gasdg", IsEnabled = false },
        new ImageTest { Id = 4, Name = "Second image", Image = "diff.url.here" }
    };

    var json = JsonSerializer.Serialize(list);
    var data = JsonSerializer.Deserialize<List<ITest>>(json);

    // JSON data
    // [
    //   {
    //      "$type":"ConsoleApp1.ImageTest",
    //      "Id":1,
    //      "Name":"Image test",
    //      "Image":"some.url.here"
    //   },
    //   {
    //      "$type":"ConsoleApp1.TextTest",
    //      "Id":2,
    //      "Name":"Text test",
    //      "Text":"kasdglaskhdgl aksjdgl asd gasdg",
    //      "IsEnabled":true
    //   },
    //   {
    //      "$type":"ConsoleApp1.TextTest",
    //      "Id":3,
    //      "Name":"Text test 2",
    //      "Text":"asd gasdg",
    //      "IsEnabled":false
    //   },
    //   {
    //      "$type":"ConsoleApp1.ImageTest",
    //      "Id":4,
    //      "Name":"Second image",
    //      "Image":"diff.url.here"
    //   }
    // ]

编辑: 我用这个逻辑做了一个 NuGet 包。您可以在这里下载:InterfaceConverter.SystemTextJson

编辑 26.3.2022: NuGet 包版本实现了更多逻辑,例如。在所有引用的程序集中查找类型。

改进@t00thy 解决方案

您的解决方案很好,但是如果在其他程序集中使用具体类型怎么办?

转换器Class

public class InterfaceConverter<T> : JsonConverter<T> where T : class
{
    public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        Utf8JsonReader readerClone = reader;
        if (readerClone.TokenType != JsonTokenType.StartObject)
            throw new JsonException("Problem in Start object! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        readerClone.Read();
        if (readerClone.TokenType != JsonTokenType.PropertyName)
            throw new JsonException("Token Type not equal to property name! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        string? propertyName = readerClone.GetString();
        if (string.IsNullOrWhiteSpace(propertyName) || propertyName != "$type")
            throw new JsonException("Unable to get $type! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        readerClone.Read();
        if (readerClone.TokenType != JsonTokenType.String)
            throw new JsonException("Token Type is not JsonTokenString! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        string? typeValue = readerClone.GetString();
        if(string.IsNullOrWhiteSpace(typeValue))
            throw new JsonException("typeValue is null or empty string! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        string? asmbFullName = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(ass => !string.IsNullOrEmpty(ass.GetName().Name) && ass.GetName().Name.Equals(typeValue.Split(" ")[1]))?.FullName;

        if (string.IsNullOrWhiteSpace(asmbFullName))
            throw new JsonException("Assembly name is null or empty string! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        ObjectHandle? instance = Activator.CreateInstance(asmbFullName, typeValue.Split(" ")[0]);
        if(instance == null)
            throw new JsonException("Unable to create object handler! Handler is null! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        object? unwrapedInstance = instance.Unwrap();
        if(unwrapedInstance == null)
            throw new JsonException("Unable to unwrap instance! Or instance is null! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        Type? entityType = unwrapedInstance.GetType();
        if(entityType == null)
            throw new JsonException("Instance type is null! Or instance is null! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        object? deserialized = JsonSerializer.Deserialize(ref reader, entityType, options);
        if(deserialized == null)
            throw new JsonException("De-Serialized object is null here!");

        return (T)deserialized;
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        switch (value)
        {
            case null:
                JsonSerializer.Serialize(writer, typeof(T) ,options);
                break;
            default:
                {
                    var type = value.GetType();
                    using var jsonDocument = JsonDocument.Parse(JsonSerializer.Serialize(value, type, options));
                    writer.WriteStartObject();
                    writer.WriteString("$type", type.FullName + " " + type.Assembly.GetName().Name);

                    foreach (var element in jsonDocument.RootElement.EnumerateObject())
                    {
                        element.WriteTo(writer);
                    }

                    writer.WriteEndObject();
                    break;
                }
        }
    }
}

转换器属性

[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)]
public class JsonInterfaceConverterAttribute : JsonConverterAttribute
{
    public JsonInterfaceConverterAttribute(Type converterType)
        : base(converterType)
    {
    }
}

接口和Classes

[JsonInterfaceConverter(typeof(InterfaceConverter<IUser>))]
public interface IUser
{
    int Id { get; set; }
    string Name { get; set; }
    IEnumerable<IRight> Rights { get; set; }
}

[JsonInterfaceConverter(typeof(InterfaceConverter<IRight>))]
public interface IRight
{
    int Id { get; set; }
    bool HasRight { get; set; }
}

public class User : IUser
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public IEnumerable<IRight> Rights { get; set; } = Enumerable.Empty<IRight>();
}

public class Right : IRight
{
    public int Id { get; set; }
    public bool HasRight { get; set; }
}

用法:

        //           your dependency injector
        IUser user = IServiceProvider.GetRequiredService<IUser>();
        user.Id = 1;
        user.Name = "Xyz";

        List<IRight> rights = new ();
        //           your dependency injector
        IRight right1 = IServiceProvider.GetRequiredService<IRight>();
        right1.Id = 1;
        right1.HasRight = true;
        rights.Add(right1);
        //           your dependency injector
        IRight right2 = IServiceProvider.GetRequiredService<IRight>();
        right2.Id = 2;
        right2.HasRight = true;
        rights.Add(right2);
        //           your dependency injector
        IRight right3 = IServiceProvider.GetRequiredService<IRight>();
        right3.Id = 1;
        right3.HasRight = true;
        rights.Add(right2);

        var serializedRights = JsonSerializer.Serialize(rights);

        user.Rights = rights;

        // Serialization is simple
        var serilizedUser = JsonSerializer.Serialize(user);

        //But for DeSerialization of single object you need to use it some thing like this
        //                                                    Ask your dependency injector to resolve and get type of object
        IUser usr = JsonSerializer.Deserialize(serilizedUser, IServiceProvider.GetRequiredService<IUser>().GetType());

        //DeSerialization of list or enumerable is simple
        IEnumerable<IRight>? rits = JsonSerializer.Deserialize<IEnumerable<IRight>>(serializedRights);