System.Text.Json是否支持多态反序列化?

117

我尝试从Newtonsoft.Json迁移到System.Text.Json。 我想要反序列化抽象类。Newtonsoft.Json可以使用TypeNameHandling来实现。 在.NET Core 3.0上,是否有方法可以通过System.Text.Json反序列化抽象类?


1
根据定义,抽象类无法被实例化。这意味着无论使用什么 JSON 解析器,都不能对抽象类进行反序列化操作。使用 JSON.NET 也是如此。您必须指定一个具体类型。 - Panagiotis Kanavos
1
TypeNameHandling 在 JSON 字符串中发出自定义类型信息,但它不会处理抽象类。在链接的文档示例中,JSON 字符串包含来自具体 Hotel 类而非抽象 Business 类的数据。 - Panagiotis Kanavos
4
我的意思是,在抽象类中使用具体类的实例作为变量。 - SkyStorm
1
看一下。abstract class A {} class B:A{} class C:A{}。API 有参数 IEnumerable<A>。客户端发送 new A[]{new B(), new C()}。ASP 通过 Newtonsoft json 进行反序列化,它可以工作。如何在 asp.net core 3.0 中使用 system.text.json 实现这个功能? - SkyStorm
2
你的问题是关于多态性,而不是抽象类。如果Business是一个接口,它也不会改变。如果你搜索System.Text.Json polymorphism,你会发现这个问题,它解释了目前不支持多态反序列化。 - Panagiotis Kanavos
显示剩余6条评论
16个回答

105
在System.Text.Json中能否进行多态反序列化?答案是取决于你对“可能”的理解。

System.Text.Json内置不支持多态反序列化(相当于Newtonsoft.Json的TypeNameHandling)。这是因为在JSON有效负载中读取指定为字符串的.NET类型名称(如$type元数据属性)以创建对象不被建议,因为它会引入潜在的安全问题(有关更多信息,请参见https://github.com/dotnet/corefx/issues/41347#issuecomment-535779492)。

允许负载指定其自己的类型信息是Web应用程序漏洞的常见来源。

然而,通过创建JsonConverter<T>,你可以添加你自己的多态反序列化支持,所以从这个意义上说,是可行的。

文档展示了如何使用类型鉴别器属性进行操作的示例:https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to#support-polymorphic-deserialization

让我们看一个例子。

假设你有一个基类和几个派生类:

public class BaseClass
{
    public int Int { get; set; }
}
public class DerivedA : BaseClass
{
    public string Str { get; set; }
}
public class DerivedB : BaseClass
{
    public bool Bool { get; set; }
}

您可以创建以下 JsonConverter<BaseClass>,在序列化时编写类型鉴别器并读取它以确定要反序列化的类型。 您可以在JsonSerializerOptions上注册该转换器。

public class BaseClassConverter : JsonConverter<BaseClass>
{
    private enum TypeDiscriminator
    {
        BaseClass = 0,
        DerivedA = 1,
        DerivedB = 2
    }

    public override bool CanConvert(Type type)
    {
        return typeof(BaseClass).IsAssignableFrom(type);
    }

    public override BaseClass Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException();
        }

        if (!reader.Read()
                || reader.TokenType != JsonTokenType.PropertyName
                || reader.GetString() != "TypeDiscriminator")
        {
            throw new JsonException();
        }

        if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
        {
            throw new JsonException();
        }

        BaseClass baseClass;
        TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
        switch (typeDiscriminator)
        {
            case TypeDiscriminator.DerivedA:
                if (!reader.Read() || reader.GetString() != "TypeValue")
                {
                    throw new JsonException();
                }
                if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException();
                }
                baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));
                break;
            case TypeDiscriminator.DerivedB:
                if (!reader.Read() || reader.GetString() != "TypeValue")
                {
                    throw new JsonException();
                }
                if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException();
                }
                baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));
                break;
            default:
                throw new NotSupportedException();
        }

        if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
        {
            throw new JsonException();
        }

        return baseClass;
    }

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

        if (value is DerivedA derivedA)
        {
            writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedA);
            writer.WritePropertyName("TypeValue");
            JsonSerializer.Serialize(writer, derivedA);
        }
        else if (value is DerivedB derivedB)
        {
            writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);
            writer.WritePropertyName("TypeValue");
            JsonSerializer.Serialize(writer, derivedB);
        }
        else
        {
            throw new NotSupportedException();
        }

        writer.WriteEndObject();
    }
}

以下展示了序列化和反序列化的示例(包括与Newtonsoft.Json的比较):

private static void PolymorphicSupportComparison()
{
    var objects = new List<BaseClass> { new DerivedA(), new DerivedB() };

    // Using: System.Text.Json
    var options = new JsonSerializerOptions
    {
        Converters = { new BaseClassConverter() },
        WriteIndented = true
    };

    string jsonString = JsonSerializer.Serialize(objects, options);
    Console.WriteLine(jsonString);
    /*
     [
      {
        "TypeDiscriminator": 1,
        "TypeValue": {
            "Str": null,
            "Int": 0
        }
      },
      {
        "TypeDiscriminator": 2,
        "TypeValue": {
            "Bool": false,
            "Int": 0
        }
      }
     ]
    */

    var roundTrip = JsonSerializer.Deserialize<List<BaseClass>>(jsonString, options);


    // Using: Newtonsoft.Json
    var settings = new Newtonsoft.Json.JsonSerializerSettings
    {
        TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,
        Formatting = Newtonsoft.Json.Formatting.Indented
    };

    jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(objects, settings);
    Console.WriteLine(jsonString);
    /*
     [
      {
        "$type": "PolymorphicSerialization.DerivedA, PolymorphicSerialization",
        "Str": null,
        "Int": 0
      },
      {
        "$type": "PolymorphicSerialization.DerivedB, PolymorphicSerialization",
        "Bool": false,
        "Int": 0
      }
     ]
    */

    var originalList = JsonConvert.DeserializeObject<List<BaseClass>>(jsonString, settings);

    Debug.Assert(originalList[0].GetType() == roundTrip[0].GetType());
}

这里有一个StackOverflow的问题,展示了如何使用接口(而不是抽象类)来支持多态反序列化,但类似的解决方案也适用于任何多态性: 在 System.Text.Json 中自定义转换器时,是否有一种简单的方法手动序列化/反序列化子对象?


3
要是我希望将“鉴别器”作为对象的一部分呢?目前这个代码将“DerivedA”嵌套在“TypeValue”对象中。 - macwier
3
如果鉴别器的值不是JSON中的第一个属性,该如何重置读取器? - HerSta
4
@HerSta,这个读者是一个结构体,因此你可以创建一个局部副本来返回到先前的状态或“重置”它。所以你可以在副本上循环完全读取子对象来查找鉴别器值,然后在完成后更新转换器的输入参数,以便让反序列化器知道你已经读取了整个对象以及从哪里继续读取。 - ahsonkhan
@ahsonkhan,请问您是如何完成这个任务的?https://dev59.com/L7_qa4cB1Zd3GeqPGF5t? - user5113188
你应该将转换器提供的 JsonSerializerOptions 传递给 JsonSerializer.SerializeJsonSerializer.Deserialize 的调用。 - V0ldek
显示剩余2条评论

43

.NET 7已经实现了继承白名单多态序列化,该功能在预览版6中可用。

从文档页.NET 7中System.Text.Json的新功能:类型层次结构

System.Text.Json现在支持用户定义类型层次结构的多态序列化和反序列化。这可以通过给类型层次结构的基类加上新的JsonDerivedTypeAttribute来启用。

首先,让我们考虑序列化。假设您有以下类型层次结构:

public abstract class BaseType { } // Properties omitted

public class DerivedType1 : BaseType { public string Derived1 { get; set; } } 
public class DerivedType2 : BaseType { public int Derived2 { get; set; } }

如果你的数据模型中包含一个值,其声明类型为BaseType,例如:

var list = new List<BaseType> { new DerivedType1 { Derived1 = "value 1" } };

在之前的版本中,System.Text.Json只会序列化声明类型BaseType的属性。现在,您可以通过将[JsonDerivedType(typeof(TDerivedType))]添加到所有派生类型的BaseType中,在序列化声明为BaseType的值时包括DerivedType1的属性。
[JsonDerivedType(typeof(DerivedType1))]
[JsonDerivedType(typeof(DerivedType2))]
public abstract class BaseType { } // Properties omitted

在这种方式下,将DerivedType1列入白名单后,对于您的模型进行序列化:
var json = JsonSerializer.Serialize(list);

结果

[{"Derived1" : "value 1"}]

演示Fiddle #1在这里

请注意,只有通过属性白名单(或在运行时通过设置JsonTypeInfo.PolymorphismOptions)列入白名单的派生类型可以通过此机制进行序列化。 如果您有其他未列入白名单的派生类型,例如:

public class DerivedType3 : BaseType { public string Derived3 { get; set; } } 

然后JsonSerializer.Serialize(new BaseType [] { new DerivedType3 { Derived3 = "value 3" } })会抛出一个System.NotSupportedException: Runtime type 'DerivedType3' is not supported by polymorphic type 'BaseType'的异常。点击此处查看演示。

这就是序列化的内容。如果您需要对类型层次结构进行往返传输,则需要为每个派生类型提供一个类型鉴别器属性值。可以通过为每个派生类型提供JsonDerivedTypeAttribute.TypeDiscriminator属性值来完成此操作:

[JsonDerivedType(typeof(DerivedType1), "DerivedType1")]
[JsonDerivedType(typeof(DerivedType2), "DerivedType2")]
public abstract class BaseType { } // Properties omitted

现在,当你序列化你的模型时。
var json = JsonSerializer.Serialize(list);

System.Text.Json将添加一个人工类型鉴别器属性"$type",表示已序列化的类型:

[{"$type" : "DerivedType1", "Derived1" : "value 1"}]

完成上述操作后,您现在可以像这样反序列化您的数据模型:

var list2 = JsonSerializer.Deserialize<List<BaseType>>(json);

实际的具体类型序列化将会被保留。Demo fiddle #3 在这里

还可以通过合同定制告知 System.Text.Json 在运行时中您的类型层级结构。当您的类型层级结构无法修改、某些派生类型位于不同的程序集且无法在编译时引用或者您正尝试在多个传统序列化程序之间进行交互时,可能需要这样做。基本的工作流程是实例化一个 DefaultJsonTypeInfoResolver 实例并添加一个修改器,该修改器为您的基础类型的JsonTypeInfo设置必要的PolymorphismOptions

例如,可以在运行时启用BaseType层次结构的多态序列化,方法如下:

var resolver = new DefaultJsonTypeInfoResolver
{
    Modifiers =
    {
        // Add an Action<JsonTypeInfo> modifier that sets up the polymorphism options for BaseType
        static typeInfo =>
        {
            if (typeInfo.Type != typeof(BaseType))
                return;
            typeInfo.PolymorphismOptions = new()
            {
                DerivedTypes =
                {
                    new JsonDerivedType(typeof(DerivedType1), "Derived1"),
                    new JsonDerivedType(typeof(DerivedType2), "Derived2")
                }
            };
        },
        // Add other modifiers as required.
    }
};
var options = new JsonSerializerOptions
{
    TypeInfoResolver = resolver,
    // Add other options as required
};
var json = JsonSerializer.Serialize(list, options);

这里是示例代码 #4,点此链接查看。

注:

  1. The whitelisting approach is consistent with the approach of the data contract serializers, which use the KnownTypeAttribute, and XmlSerializer, which uses XmlIncludeAttribute. It is inconsistent with Json.NET, whose TypeNameHandling serializes type information for all types unless explicitly filtered via a serialization binder.

    Allowing only whitelisted types to be deserialized prevents Friday the 13th: JSON Attacks type injection attacks including those detailed in TypeNameHandling caution in Newtonsoft Json and External json vulnerable because of Json.Net TypeNameHandling auto?.

  2. Integers as well as strings may be used for the type discriminator name. If you define your type hierarchy as follows:

    [JsonDerivedType(typeof(DerivedType1), 1)]
    [JsonDerivedType(typeof(DerivedType2), 2)]
    public abstract class BaseType { } // Properties omitted
    

    Then serializing the list above results in

    [{"$type" : 1, "Derived1" : "value 1"}]
    

    Numeric type discriminator values are not used by Newtonsoft however, so if you are interoperating with a legacy serializer you might want to avoid this.

  3. The default type discriminator property name, "$type", is the same type discriminator name used by Json.NET. If you would prefer to use a different property name, such as the name "__type" used by DataContractJsonSerializer, apply JsonPolymorphicAttribute to the base type and set TypeDiscriminatorPropertyName like so:

    [JsonPolymorphic(TypeDiscriminatorPropertyName = "__type")]
    [JsonDerivedType(typeof(DerivedType1), "DerivedType1")]
    [JsonDerivedType(typeof(DerivedType2), "DerivedType2")]
    public abstract class BaseType { } // Properties omitted
    
  4. If you are interoperating with Json.NET (or DataContractJsonSerializer), you may set the value of TypeDiscriminator equal to the type discriminator value used by the legacy serializer.

  5. If the serializer encounters a derived type that has not been whitelisted, you can control its behavior by setting JsonPolymorphicAttribute.UnknownDerivedTypeHandling to one of the following values:

    JsonUnknownDerivedTypeHandling Value Meaning
    FailSerialization 0 An object of undeclared runtime type will fail polymorphic serialization.
    FallBackToBaseType 1 An object of undeclared runtime type will fall back to the serialization contract of the base type.
    FallBackToNearestAncestor 2 An object of undeclared runtime type will revert to the serialization contract of the nearest declared ancestor type. Certain interface hierarchies are not supported due to diamond ambiguity constraints.

4
截至 .NET 7,这应该是当前的正确答案。 - dotNET
非常有帮助,但是你的示例只会将派生类型强制转换为基类型时添加$type。我该如何让它在未进行强制转换的派生类型上添加$type呢? - Ewan
啊,好的,我觉得我正在经历这个问题:https://github.com/dotnet/aspnetcore/issues/45548 - Ewan
1
@Ewan - 关于多级多态类型层次结构,请参考如何在.NET 7中使用System.Text.Json序列化多级多态类型层次结构?。您需要手动将DerivedModel添加[JsonDerivedType(typeof(DerivedDerivedModel), nameof(DerivedDerivedModel))],或编写自定义的TypeInfo修改器来自动完成此操作。 - dbc
啊,只有一部分。看起来如果你将类型传递为 object,它也会丢失 $type - Ewan

30
我采用了这个解决方案。它很轻量化且对我来说足够通用。
类型鉴别器转换器
public class TypeDiscriminatorConverter<T> : JsonConverter<T> where T : ITypeDiscriminator
{
    private readonly IEnumerable<Type> _types;

    public TypeDiscriminatorConverter()
    {
        var type = typeof(T);
        _types = AppDomain.CurrentDomain.GetAssemblies()
            .SelectMany(s => s.GetTypes())
            .Where(p => type.IsAssignableFrom(p) && p.IsClass && !p.IsAbstract)
            .ToList();
    }

    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException();
        }

        using (var jsonDocument = JsonDocument.ParseValue(ref reader))
        {
            if (!jsonDocument.RootElement.TryGetProperty(nameof(ITypeDiscriminator.TypeDiscriminator), out var typeProperty))
            {
                throw new JsonException();
            }

            var type = _types.FirstOrDefault(x => x.Name == typeProperty.GetString());
            if (type == null)
            {
                throw new JsonException();
            }

            var jsonObject = jsonDocument.RootElement.GetRawText();
            var result = (T) JsonSerializer.Deserialize(jsonObject, type, options);

            return result;
        }
    }

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

接口
public interface ITypeDiscriminator
{
    string TypeDiscriminator { get; }
}

还有示例模型

public interface ISurveyStepResult : ITypeDiscriminator
{
    string Id { get; set; }
}

public class BoolStepResult : ISurveyStepResult
{
    public string Id { get; set; }
    public string TypeDiscriminator => nameof(BoolStepResult);

    public bool Value { get; set; }
}

public class TextStepResult : ISurveyStepResult
{
    public string Id { get; set; }
    public string TypeDiscriminator => nameof(TextStepResult);

    public string Value { get; set; }
}

public class StarsStepResult : ISurveyStepResult
{
    public string Id { get; set; }
    public string TypeDiscriminator => nameof(StarsStepResult);

    public int Value { get; set; }
}

以下是测试方法:

public void SerializeAndDeserializeTest()
    {
        var surveyResult = new SurveyResultModel()
        {
            Id = "id",
            SurveyId = "surveyId",
            Steps = new List<ISurveyStepResult>()
            {
                new BoolStepResult(){ Id = "1", Value = true},
                new TextStepResult(){ Id = "2", Value = "some text"},
                new StarsStepResult(){ Id = "3", Value = 5},
            }
        };

        var jsonSerializerOptions = new JsonSerializerOptions()
        {
            Converters = { new TypeDiscriminatorConverter<ISurveyStepResult>()},
            WriteIndented = true
        };
        var result = JsonSerializer.Serialize(surveyResult, jsonSerializerOptions);

        var back = JsonSerializer.Deserialize<SurveyResultModel>(result, jsonSerializerOptions);

        var result2 = JsonSerializer.Serialize(back, jsonSerializerOptions);
        
        Assert.IsTrue(back.Steps.Count == 3 
                      && back.Steps.Any(x => x is BoolStepResult)
                      && back.Steps.Any(x => x is TextStepResult)
                      && back.Steps.Any(x => x is StarsStepResult)
                      );
        Assert.AreEqual(result2, result);
    }

2
我不太喜欢在我的模型中添加类型鉴别器属性的想法,但这是一个很好的解决方案,可以在System.Text.Json允许的范围内工作。 - Cocowalla
1
我们需要在模型内部添加类型鉴别器,因为我们需要在所有级别上使用它直到数据库。相同的逻辑用于从cosmos db反序列化正确的类型。@Cocowalla - Demetrius Axenowski
我最终得出了类似的解决方案。但是,我该如何验证这些模型呢? - el peregrino
1
@DemetriusAxenowski 如果您不从“选项”中删除此转换器,则Write方法将陷入无限递归。 您是如何测试这个的? - Paul Michalik
1
在.NET 6中,您可以直接使用JsonSerializer.Deserialize(this JsonDocument document, Type returnType, JsonSerializerOptions? options = null)JsonDocument反序列化,如System.Text.Json.JsonElement ToObject workaround所示。这避免了对RootElement.GetRawText();的调用。 - dbc
显示剩余7条评论

8

在新的.NET 7功能中,我们可以不用编写繁琐的代码来实现这个功能。详情请参见:https://devblogs.microsoft.com/dotnet/announcing-dotnet-7-preview-5/

[JsonDerivedType(typeof(Derived1), 0)]
[JsonDerivedType(typeof(Derived2), 1)]
[JsonDerivedType(typeof(Derived3), 2)]
public class Base { }

JsonSerializer.Serialize<Base>(new Derived2()); // { "$type" : 1, ... }

希望这可以帮助到您。


7
请尝试使用我编写的库作为扩展到System.Text.Json,以提供多态性: https://github.com/dahomey-technologies/Dahomey.Json 如果引用实例的实际类型与声明的类型不同,则鉴别器属性将自动添加到输出的json中。
public class WeatherForecast
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string Summary { get; set; }
}

public class WeatherForecastDerived : WeatherForecast
{
    public int WindSpeed { get; set; }
}

为了让框架知道鉴别器值和类型之间的映射关系,必须手动将继承类注册到鉴别器约定注册表中:

JsonSerializerOptions options = new JsonSerializerOptions();
options.SetupExtensions();
DiscriminatorConventionRegistry registry = options.GetDiscriminatorConventionRegistry();
registry.RegisterType<WeatherForecastDerived>();

string json = JsonSerializer.Serialize<WeatherForecast>(weatherForecastDerived, options);

结果:

{
  "$type": "Tests.WeatherForecastDerived, Tests",
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot",
  "WindSpeed": 35
}

不幸的是,这个问题与Newtonsoft.Json的TypeNameHandling功能存在类似的$type安全问题。请参见:https://github.com/dotnet/corefx/issues/41347#issuecomment-535779492 - ahsonkhan
1
@ahsonkhan 我已经提交了一个问题,其中包含改进Dahomey.Json安全性的步骤:https://github.com/dahomey-technologies/Dahomey.Json/issues/22 - Michaël Catanzariti
1
问题 https://github.com/dahomey-technologies/Dahomey.Json/issues/22 已解决。 - Michaël Catanzariti

5

这是适用于所有抽象类型的JsonConverter:

        private class AbstractClassConverter : JsonConverter<object>
        {
            public override object Read(ref Utf8JsonReader reader, Type typeToConvert,
                JsonSerializerOptions options)
            {
                if (reader.TokenType == JsonTokenType.Null) return null;

                if (reader.TokenType != JsonTokenType.StartObject)
                    throw new JsonException("JsonTokenType.StartObject not found.");

                if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName
                                   || reader.GetString() != "$type")
                    throw new JsonException("Property $type not found.");

                if (!reader.Read() || reader.TokenType != JsonTokenType.String)
                    throw new JsonException("Value at $type is invalid.");

                string assemblyQualifiedName = reader.GetString();

                var type = Type.GetType(assemblyQualifiedName);
                using (var output = new MemoryStream())
                {
                    ReadObject(ref reader, output, options);
                    return JsonSerializer.Deserialize(output.ToArray(), type, options);
                }
            }

            private void ReadObject(ref Utf8JsonReader reader, Stream output, JsonSerializerOptions options)
            {
                using (var writer = new Utf8JsonWriter(output, new JsonWriterOptions
                {
                    Encoder = options.Encoder,
                    Indented = options.WriteIndented
                }))
                {
                    writer.WriteStartObject();
                    var objectIntend = 0;

                    while (reader.Read())
                    {
                        switch (reader.TokenType)
                        {
                            case JsonTokenType.None:
                            case JsonTokenType.Null:
                                writer.WriteNullValue();
                                break;
                            case JsonTokenType.StartObject:
                                writer.WriteStartObject();
                                objectIntend++;
                                break;
                            case JsonTokenType.EndObject:
                                writer.WriteEndObject();
                                if(objectIntend == 0)
                                {
                                    writer.Flush();
                                    return;
                                }
                                objectIntend--;
                                break;
                            case JsonTokenType.StartArray:
                                writer.WriteStartArray();
                                break;
                            case JsonTokenType.EndArray:
                                writer.WriteEndArray();
                                break;
                            case JsonTokenType.PropertyName:
                                writer.WritePropertyName(reader.GetString());
                                break;
                            case JsonTokenType.Comment:
                                writer.WriteCommentValue(reader.GetComment());
                                break;
                            case JsonTokenType.String:
                                writer.WriteStringValue(reader.GetString());
                                break;
                            case JsonTokenType.Number:
                                writer.WriteNumberValue(reader.GetInt32());
                                break;
                            case JsonTokenType.True:
                            case JsonTokenType.False:
                                writer.WriteBooleanValue(reader.GetBoolean());
                                break;
                            default:
                                throw new ArgumentOutOfRangeException();
                        }
                    }
                }
            }

            public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
            {
                writer.WriteStartObject();
                var valueType = value.GetType();
                var valueAssemblyName = valueType.Assembly.GetName();
                writer.WriteString("$type", $"{valueType.FullName}, {valueAssemblyName.Name}");

                var json = JsonSerializer.Serialize(value, value.GetType(), options);
                using (var document = JsonDocument.Parse(json, new JsonDocumentOptions
                {
                    AllowTrailingCommas = options.AllowTrailingCommas,
                    MaxDepth = options.MaxDepth
                }))
                {
                    foreach (var jsonProperty in document.RootElement.EnumerateObject())
                        jsonProperty.WriteTo(writer);
                }

                writer.WriteEndObject();
            }

            public override bool CanConvert(Type typeToConvert) => 
                typeToConvert.IsAbstract && !EnumerableInterfaceType.IsAssignableFrom(typeToConvert);
        }

1
但似乎存在安全问题,例如 https://github.com/dotnet/runtime/issues/30969#issuecomment-535779492 或 https://github.com/dahomey-technologies/Dahomey.Json/issues/22。 - Marcus.D
2
@marcus-d,添加允许程序集和/或类型列表,并在“Read()”中检查“$type”标记是否足够? - Cocowalla
你假设每个数字都是Int32类型。case JsonTokenType.Number: - 也可能是浮点数。 - Michal Dobrodenka

4
基于已接受的答案,但使用KnownTypeAttribute来发现类型(通常枚举所有类型可能会导致不想要的类型加载异常),并在转换器中添加鉴别器属性,而不是让该类自己实现它:
public class TypeDiscriminatorConverter<T> : JsonConverter<T> 
{
    private readonly IEnumerable<Type> _types;

    public TypeDiscriminatorConverter()
    {
        var type = typeof(T);
        var knownTypes = type.GetCustomAttributes(typeof(KnownTypeAttribute), false).OfType<KnownTypeAttribute>();
        _types = knownTypes.Select(x => x.Type).ToArray();
    }

    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException();
        }

        using (var jsonDocument = JsonDocument.ParseValue(ref reader))
        {
            if (!jsonDocument.RootElement.TryGetProperty("discriminator",
                out var typeProperty))
            {
                throw new JsonException();
            }

            var type = _types.FirstOrDefault(x => x.FullName == typeProperty.GetString());
            if (type == null)
            {
                throw new JsonException();
            }

            var jsonObject = jsonDocument.RootElement.GetRawText();
            var result = (T)JsonSerializer.Deserialize(jsonObject, type, options);

            return result;
        }
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        using (JsonDocument document = JsonDocument.Parse(JsonSerializer.Serialize(value)))
        {
            writer.WritePropertyName("discriminator");
            writer.WriteStringValue(value.GetType().FullName);
            foreach (var property in document.RootElement.EnumerateObject())
            {
                property.WriteTo(writer);
            }

        }
        writer.WriteEndObject();
    }
}

您可以像这样使用它:
[JsonConverter(typeof(JsonInheritanceConverter))]
[KnownType(typeof(DerivedA))]
[KnownType(typeof(DerivedB))]
public abstract class BaseClass
{ 
    //..
}

我的观点是:基类不应该知道它的派生类。 - thomasgalliker
似乎JsonConverter是来自Newtonsoft,但它与System.Text.Json无关。 - Evgeni Nabokov

4
我想提出另一种适用于分层、安全、双向、通用使用的实现。
以下是注意事项:
- 它在性能和内存方面会成为"噩梦",但对于大多数情况而言足够好(原因:因为您需要预读 `$type`,然后需要在读取器上返回)。 - 它仅在多态基类是抽象/从未作为实例序列化的情况下工作(原因:否则,常规转换器无法处理派生类,因为它会导致堆栈溢出)。 - 仅在 .NET 6 下运行...不会在 3.1 下运行。
例如:
public abstract record QueryClause(); // the abstract is kind of important
public record AndClause(QueryClause[] SubClauses) : QueryClause();
public record OrClause(QueryClause[] SubClauses) : QueryClause();

// ...

JsonSerializerOptions options = new JsonSerializerOptions();
options.Converters.Add(new BaseClassConverter<QueryClause>(
                    typeof(AndClause),
                    typeof(OrClause)));

// ...

转换器

public class BaseClassConverter<TBaseType> : JsonConverter<TBaseType>
    where TBaseType : class
{
    private readonly Type[] _types;
    private const string TypeProperty = "$type";

    public BaseClassConverter(params Type[] types)
    {
        _types = types;
    }

    public override bool CanConvert(Type type)
        => typeof(TBaseType) == type; // only responsible for the abstract base

    public override TBaseType Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options)
    {
        TBaseType result;

        if (JsonDocument.TryParseValue(ref reader, out var doc))
        {
            if (doc.RootElement.TryGetProperty(TypeProperty, out var typeProperty))
            {
                var typeName = typeProperty.GetString();
                var type = _types.FirstOrDefault(t => t.Name == typeName) ?? throw new JsonException($"{TypeProperty} specifies an invalid type");

                var rootElement = doc.RootElement.GetRawText();

                result = JsonSerializer.Deserialize(rootElement, type, options) as TBaseType ?? throw new JsonException("target type could not be serialized");
            }
            else
            {
                throw new JsonException($"{TypeProperty} missing");
            }
        }
        else
        {
            throw new JsonException("Failed to parse JsonDocument");
        }

        return result;
    }

    public override void Write(
        Utf8JsonWriter writer,
        TBaseType value,
        JsonSerializerOptions options)
    {
        var type = value.GetType();

        if (_types.Any(t => type.Name == t.Name))
        {
            var jsonElement = JsonSerializer.SerializeToElement(value, type, options);

            var jsonObject = JsonObject.Create(jsonElement) ?? throw new JsonException();
            jsonObject[TypeProperty] = type.Name;

            jsonObject.WriteTo(writer, options);
        }
        else
        {
            throw new JsonException($"{type.Name} with matching base type {typeof(TBaseType).Name} is not registered.");
        }
    }
}

如果你发现了什么问题,请在评论中告诉我。
1致以赞扬。

1
你不需要将RootElement序列化为文本,然后重新解析,你可以直接调用doc.Deserialize(type, options) - 这样会更有效率。另外,如果你反转测试,你可以展平嵌套的if(!JsonDocument.TryParseValue...) throw ... - Rafael Munitić

4

我非常喜欢Demetrius的答案,但是我认为在可重复使用性方面还可以更进一步。我想到了以下解决方案:

JsonConverterFactory:

/// <summary>
/// Represents the <see cref="JsonConverterFactory"/> used to create <see cref="AbstractClassConverter{T}"/>
/// </summary>
public class AbstractClassConverterFactory
    : JsonConverterFactory
{

    /// <summary>
    /// Gets a <see cref="Dictionary{TKey, TValue}"/> containing the mappings of types to their respective <see cref="JsonConverter"/>
    /// </summary>
    protected static Dictionary<Type, JsonConverter> Converters = new Dictionary<Type, JsonConverter>();

    /// <summary>
    /// Initializes a new <see cref="AbstractClassConverterFactory"/>
    /// </summary>
    /// <param name="namingPolicy">The current <see cref="JsonNamingPolicy"/></param>
    public AbstractClassConverterFactory(JsonNamingPolicy namingPolicy)
    {
        this.NamingPolicy = namingPolicy;
    }

    /// <summary>
    /// Gets the current <see cref="JsonNamingPolicy"/>
    /// </summary>
    protected JsonNamingPolicy NamingPolicy { get; }

    /// <inheritdoc/>
    public override bool CanConvert(Type typeToConvert)
    {
        return typeToConvert.IsClass && typeToConvert.IsAbstract && typeToConvert.IsDefined(typeof(DiscriminatorAttribute));
    }

    /// <inheritdoc/>
    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        if(!Converters.TryGetValue(typeToConvert, out JsonConverter converter))
        {
            Type converterType = typeof(AbstractClassConverter<>).MakeGenericType(typeToConvert);
            converter = (JsonConverter)Activator.CreateInstance(converterType, this.NamingPolicy);
            Converters.Add(typeToConvert, converter);
        }
        return converter;
    }

}

JsonConverter: Json转换器。
/// <summary>
/// Represents the <see cref="JsonConverter"/> used to convert to/from an abstract class
/// </summary>
/// <typeparam name="T">The type of the abstract class to convert to/from</typeparam>
public class AbstractClassConverter<T>
    : JsonConverter<T>
{

    /// <summary>
    /// Initializes a new <see cref="AbstractClassConverter{T}"/>
    /// </summary>
    /// <param name="namingPolicy">The current <see cref="JsonNamingPolicy"/></param>
    public AbstractClassConverter(JsonNamingPolicy namingPolicy)
    {
        this.NamingPolicy = namingPolicy;
        DiscriminatorAttribute discriminatorAttribute = typeof(T).GetCustomAttribute<DiscriminatorAttribute>();
        if (discriminatorAttribute == null)
            throw new NullReferenceException($"Failed to find the required '{nameof(DiscriminatorAttribute)}'");
        this.DiscriminatorProperty = typeof(T).GetProperty(discriminatorAttribute.Property, BindingFlags.Default | BindingFlags.Public | BindingFlags.Instance);
        if (this.DiscriminatorProperty == null)
            throw new NullReferenceException($"Failed to find the specified discriminator property '{discriminatorAttribute.Property}' in type '{typeof(T).Name}'");
        this.TypeMappings = new Dictionary<string, Type>();
        foreach (Type derivedType in TypeCacheUtil.FindFilteredTypes($"nposm:json-polymorph:{typeof(T).Name}", 
            (t) => t.IsClass && !t.IsAbstract && t.BaseType == typeof(T)))
        {
            DiscriminatorValueAttribute discriminatorValueAttribute = derivedType.GetCustomAttribute<DiscriminatorValueAttribute>();
            if (discriminatorValueAttribute == null)
                continue;
            string discriminatorValue = null;
            if (discriminatorValueAttribute.Value.GetType().IsEnum)
                discriminatorValue = EnumHelper.Stringify(discriminatorValueAttribute.Value, this.DiscriminatorProperty.PropertyType);
            else
                discriminatorValue = discriminatorValueAttribute.Value.ToString();
            this.TypeMappings.Add(discriminatorValue, derivedType);
        }
    }

    /// <summary>
    /// Gets the current <see cref="JsonNamingPolicy"/>
    /// </summary>
    protected JsonNamingPolicy NamingPolicy { get; }

    /// <summary>
    /// Gets the discriminator <see cref="PropertyInfo"/> of the abstract type to convert
    /// </summary>
    protected PropertyInfo DiscriminatorProperty { get; }

    /// <summary>
    /// Gets an <see cref="Dictionary{TKey, TValue}"/> containing the mappings of the converted type's derived types
    /// </summary>
    protected Dictionary<string, Type> TypeMappings { get; }

    /// <inheritdoc/>
    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException("Start object token type expected");
        using (JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader))
        {
            string discriminatorPropertyName = this.NamingPolicy?.ConvertName(this.DiscriminatorProperty.Name);
            if (!jsonDocument.RootElement.TryGetProperty(discriminatorPropertyName, out JsonElement discriminatorProperty))
                throw new JsonException($"Failed to find the required '{this.DiscriminatorProperty.Name}' discriminator property");
            string discriminatorValue = discriminatorProperty.GetString();
            if (!this.TypeMappings.TryGetValue(discriminatorValue, out Type derivedType))
                throw new JsonException($"Failed to find the derived type with the specified discriminator value '{discriminatorValue}'");
            string json = jsonDocument.RootElement.GetRawText();
            return (T)JsonSerializer.Deserialize(json, derivedType);
        }
    }

    /// <inheritdoc/>
    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        JsonSerializer.Serialize(writer, (object)value, options);
    }

}

DiscriminatorAttribute:

/// <summary>
/// Represents the <see cref="Attribute"/> used to indicate the property used to discriminate derived types of the marked class
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class DiscriminatorAttribute
    : Attribute
{

    /// <summary>
    /// Initializes a new <see cref="DiscriminatorAttribute"/>
    /// </summary>
    /// <param name="property">The name of the property used to discriminate derived types of the class marked by the <see cref="DiscriminatorAttribute"/></param>
    public DiscriminatorAttribute(string property)
    {
        this.Property = property;
    }

    /// <summary>
    /// Gets the name of the property used to discriminate derived types of the class marked by the <see cref="DiscriminatorAttribute"/>
    /// </summary>
    public string Property { get; }

}

DiscriminatorValueAttribute:

 /// <summary>
/// Represents the <see cref="Attribute"/> used to indicate the discriminator value of a derived type
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class DiscriminatorValueAttribute
    : Attribute
{

    /// <summary>
    /// Initializes a new <see cref="DiscriminatorValueAttribute"/>
    /// </summary>
    /// <param name="value">The value used to discriminate the derived type marked by the <see cref="DiscriminatorValueAttribute"/></param>
    public DiscriminatorValueAttribute(object value)
    {
        this.Value = value;
    }

    /// <summary>
    /// Gets the value used to discriminate the derived type marked by the <see cref="DiscriminatorValueAttribute"/>
    /// </summary>
    public object Value { get; }

}

最后,这是一个有关如何在类上使用它的示例:

[Discriminator(nameof(Type))]
public abstract class Identity
{

    public virtual IdentityType Type { get; protected set; }

}

[DiscriminatorValue(IdentityType.Person)]
public class Person
   : Identity
{

}

好的,马上开始翻译。

接下来要做的就是注册工厂:

完成了!

 this.Services.AddControllersWithViews()
            .AddJsonOptions(options => 
            {
                options.JsonSerializerOptions.Converters.Add(new AbstractClassConverterFactory(options.JsonSerializerOptions.PropertyNamingPolicy));
            });

什么是TypeCacheUtil?在net5.0上出现未知类型。 - Toddams
2
TypeCacheUtil是已知的,但是它是内部的。它基本上是一个帮助类,用于查找类型并将结果缓存到内存缓存或静态字段中以供进一步使用。在这个例子中,我自己实现了一个。一个官方实现的例子,虽然不是在.NET 5.0中:https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Mvc/TypeCacheUtil.cs - Charles d'Avernas

4
使用源代码生成器为带有特殊属性标记的对象自动生成JsonConverter是一种可行的选择。
您可以尝试使用这个包,但需要 .net5。

https://github.com/wivuu/Wivuu.JsonPolymorphism

生成器会查看标有鉴别器属性的属性类型,然后寻找继承自持有鉴别器类型的类型来与枚举的每个情况相匹配。
源代码在这里:https://github.com/wivuu/Wivuu.JsonPolymorphism/blob/master/Wivuu.JsonPolymorphism/JsonConverterGenerator.cs
enum AnimalType
{
    Insect,
    Mammal,
    Reptile,
    Bird // <- This causes an easy to understand build error if it's missing a corresponding inherited type!
}

// My base type is 'Animal'
abstract partial record Animal( [JsonDiscriminator] AnimalType type, string Name );

// Animals with type = 'Insect' will automatically deserialize as `Insect`
record Insect(int NumLegs = 6, int NumEyes=4) : Animal(AnimalType.Insect, "Insectoid");

record Mammal(int NumNipples = 2) : Animal(AnimalType.Mammal, "Mammalian");

record Reptile(bool ColdBlooded = true) : Animal(AnimalType.Reptile, "Reptilian");

有趣的是,这是熟悉全新源代码生成器功能的好方法。然而,我认为对于可以轻松使用自定义JsonConverter实现并且可重复使用属性注入和/或标记(就像在我的示例或Demetrius的示例中一样)的东西来说,这有点过度设计了。 - Charles d'Avernas
@Charlesd'Avernas 这并不使用运行时反射,它只是为您生成JsonConverter。 - eoleary
@eolary 确实。看起来不错。干得好! - Charles d'Avernas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接