.NetCore 中支持Json多态序列化【System.Text.Json】

.NetCore 中支持Json多态序列化【System.Text.Json】

zeee 247 2024-05-13

在.NET中,我们经常需要在序列化和反序列化时处理多态。这意味着我们有一个基类或接口,以及一些实现或继承这个基类或接口的类,我们希望能够透明地序列化和反序列化它们。在.NET 7+和.NET 6以下,实现这一点的方式略有不同。

例如对下面这样的关系,需要根据父类中定义 Type 的不同分别使用不同的子类进行序列化/反序列化:

public abstract class Parent
{
    public string Type { get; set; }
}

// Type = "ChildA"
public class ChildA : Parent
{
    public string PropertyA { get; set; }
}

// Type="ChildB"
public class ChildB : Parent
{
    public string PropertyB { get; set; }
}

.NET 7+ 的实现方法

.NET 7 之后,微软提供了官方的多态序列化支持,JsonPolymorphicAttribute,JsonDerivedTypeAttribute(文档).

这两个特性使用起来非常方便,直接在父类中指定序列化的逻辑即可,具体如下:

using System.Text.Json.Serialization;

[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
[JsonDerivedType(typeof(ChildA), "ChildA")]
[JsonDerivedType(typeof(ChildB), "ChildB")]
public abstract class Parent
{
    public string Type { get; set; }
}

上面这段代码中,JsonPolymorphic 标识区分子类的字段,这个字段不一定在 C# 中定义,但在 Json 中要有,并且一定要在第一字段, 否则会报错。

JsonPolymorphic 如果省略,默认会以 $type 为标识字段

JsonDerivedType 则定义了根据标识字段的值,来选择不同的值对象进行反序列化。

.NET 6 以下的实现方法

而在.NET 6以下,System.Text.Json 没有官方的多态序列化的实现,所以只能另寻他路:

  1. 寻找第三方开源库
  2. 自定实现转换器

先说第一种,使用开源的库:JsonSubTypes
这个库实现了强大的配置功能,包括类似 JsonPolymorphic/JsonDerivedType 的用法, 具体使用方法在仓库文档中写得比较详细,这里不再赘述。

下面说说自定义实现转换器的方法
通过自定义 Converter, 可以对指定的类型进行自定义的转换逻辑,这样在 ParentConverter 中实现多态转换的逻辑,只需要在父类中指定转换器即可:

[JsonConverter(typeof(ParentConverter))]
public abstract class Parent
{
    public string CommonProperty { get; set; }
}

这个方法缺点是灵活性差,每个类型都需要定义针对的 Converter (当然可以抽象,不过就和上面两种方法一样了,没必要重新造轮子),当然也有其优点,那就是功能强大,可以实现任意的转换逻辑,多态序列化只是其中一个具体的场景。

在一些具体的案例中,还是有必要学习这种实现思路的。 比如需要附加额外的转换逻辑,或者项目对第三方库的引用限制较多,又或者整个项目需要转换的类型只有一两处等。这些场景下是否有必要引入一个第三方库还是值得商讨的。

ParentConverter 的实现:
在微软的 官方文档 中提供了自定义转换器的实现示例, 但是该示例中每个子类属性都要单独处理,使用比较不方便,且扩展性很差。 所以我们可以牺牲一点点性能实现一个更加通用和优雅的方法:

using System.Text.Json;
using System.Text.Json.Serialization;

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

        if (!root.TryGetProperty("type", out var typeElement) || typeElement.ValueKind != JsonValueKind.String)
        {
            throw new JsonException("Type property should be a string");
        }

        var type = typeElement.GetString();
        return type switch
        {
            "ChildA" => JsonSerializer.Deserialize<ChildA>(root.GetRawText(), options),
            "ChildB" => JsonSerializer.Deserialize<ChildB>(root.GetRawText(), options),
            _ => throw new JsonException("Unknown type"),
        };
    }

    public override void Write(Utf8JsonWriter writer, Parent value, JsonSerializerOptions options)
    {
        JsonSerializer.Serialize(writer, value, value.GetType(), options);
    }
}

参考链接:


# csharp # dotnet # json