System.Text.Json 中是不是可以进行多态反序列化?

Posted

技术标签:

【中文标题】System.Text.Json 中是不是可以进行多态反序列化?【英文标题】:Is polymorphic deserialization possible in System.Text.Json?System.Text.Json 中是否可以进行多态反序列化? 【发布时间】:2019-09-24 06:31:51 【问题描述】:

我尝试从 Newtonsoft.Json 迁移到 System.Text.Json。 我想反序列化抽象类。 Newtonsoft.Json 为此具有 TypeNameHandling。 有没有办法通过.net core 3.0 上的 System.Text.Json 反序列化抽象类?

【问题讨论】:

根据定义,抽象类不能被实例化。这意味着无论 JSON 解析器如何,您都无法反序列化抽象类。 JSON.NET 也无法做到这一点。您必须指定具体类型。 TypeNameHandling 在 JSON 字符串中发出自定义类型信息,它处理抽象类。在链接文档示例中,JSON 字符串包含来自具体 Hotel 类的数据,而不是抽象 Business 类。 我的意思是抽象类的变量,里面有具体类的实例。 看一看。 abstact class A class B:A class C:A。 Api 有参数 IEnumerable。客户端发送new A[]new B(), new C()。 Asp 通过 Newtonsoft json 反序列化。是工作。如何通过 system.text.json 在 asp.net core 3.0 上执行此操作? 您的问题是关于多态性,而不是抽象类。如果Business 是一个接口,它根本不会改变。如果您搜索System.Text.Json polymorphism,您会发现this issue,这说明目前不支持多态de序列化 【参考方案1】:

System.Text.Json 中是否可以进行多态反序列化?

答案是肯定的否定的,这取决于你所说的“可能”

没有多态反序列化(相当于 Newtonsoft.Json 的TypeNameHandling)支持内置System.Text.Json。这是因为不推荐读取 JSON 负载中指定为字符串的 .NET 类型名称(例如 $type 元数据属性)来创建对象,因为它会引入潜在的安全问题(请参阅 @987654321 @了解更多信息)。

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

但是,一种方法可以通过创建 JsonConverter<T> 添加您自己的多态反序列化支持,因此从这个意义上说,这是可能的。

文档展示了如何使用 type discriminator 属性的示例: https://docs.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());

这是另一个 *** 问题,展示了如何使用接口(而不是抽象类)支持多态反序列化,但类似的解决方案适用于任何多态: Is there a simple way to manually serialize/deserialize child objects in a custom converter in System.Text.Json?

【讨论】:

如果我想让鉴别器成为对象的一部分怎么办?现在这将DerivedA 嵌套在TypeValue 对象中。 这对于一些简单的事情来说似乎需要做很多工作,我找到了一个更简单的解决方案(见我的回答:***.com/a/65019747/1671558),但我不确定它是否有任何明显的缺点。跨度> 如果判别器值不是json中的第一个属性,如何重置阅读器? @HerSta,阅读器是一个结构,因此您可以创建一个本地副本以恢复到以前的状态或“重置”它。因此,您可以通过在副本上的循环中完全读取子对象来查找鉴别器值,然后在完成后更新转换器的输入参数,以便让反序列化器知道您已读取整个对象以及在哪里继续阅读。 @ahsonkhan 请问***.com/q/66073750/5113188是怎么做到的?【参考方案2】:

我最终得到了那个解决方案。对我来说,它很轻巧,也很通用。

类型鉴别器转换器

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

【讨论】:

我并不热衷于使用类型鉴别器属性“污染”我的模型,但这是一个很好的解决方案,可以在 System.Text.Json 允许的范围内工作 我们需要模型内部的类型鉴别器,因为我们需要它直到数据库的所有级别。相同的逻辑用于反序列化 cosmos db 中的正确类型。 @Cocowalla 我最终得到了类似的解决方案。但是,如何验证模型? @DemetriusAxenowski 如果您不从“选项”中删除此转换器,Write 方法将陷入无限递归。你是怎么测试这个的? ? ... Read 方法也是如此。您必须从转换器列表中删除当前转换器或以其他方式调整当前转换器以避免这种情况。【参考方案3】:

请尝试我编写的这个库作为 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

【讨论】:

不幸的是,这在 $type 周围存在与 Newtonsoft.Json 的 TypeNameHandling 功能类似的安全问题。见:github.com/dotnet/corefx/issues/41347#issuecomment-535779492 @ahsonkhan 我提交了一个关于改善 Dahomey.Json 安全性步骤的问题:github.com/dahomey-technologies/Dahomey.Json/issues/22 问题github.com/dahomey-technologies/Dahomey.Json/issues/22 已修复【参考方案4】:

这是我所有抽象类型的 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);
        

【讨论】:

但似乎存在github.com/dotnet/runtime/issues/30969#issuecomment-535779492 或github.com/dahomey-technologies/Dahomey.Json/issues/22 之类的安全问题 @marcus-d 是否足以添加允许的程序集和/或类型的列表,并检查Read() 中的$type 令牌? 您假设每个数字都是 Int32。 case JsonTokenType.Number: - 也可以是浮点数【参考方案5】:

我真的很喜欢Demetrius 的答案,但我认为您可以在可重用性方面走得更远。我想出了以下解决方案:

JsonConverterFactory:

/// <summary>
/// Represents the <see cref="JsonConverterFactory"/> used to create <see cref="AbstractClassConverterT"/>
/// </summary>
public class AbstractClassConverterFactory
    : JsonConverterFactory


    /// <summary>
    /// Gets a <see cref="DictionaryTKey, 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:

/// <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="AbstractClassConverterT"/>
    /// </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="DictionaryTKey, 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);
    


鉴别器属性:

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


鉴别器值属性:

 /// <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 上的未知类型 TypeCacheUtil 是已知的,但是是内部的。它基本上是一个帮助类,用于在内存缓存或静态字段中查找类型和缓存结果以供进一步使用。在这个例子中,我推出了自己的实现。官方实现的示例,虽然不在 .NET 5.0 中:github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Mvc/…【参考方案6】:

抛出这个选项:使用源代码生成器为具有标记有特殊属性的属性的对象自动生成 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 的示例中一样,这有点过头了。 @Charlesd'Avernas 这不使用运行时反射,它只是为你生成 JsonConverter @eolary 确实。看起来也不错。 WP!【参考方案7】:

不要这样写

public override bool CanConvert(Type type)

    return typeof(BaseClass).IsAssignableFrom(type);

如果你的类包含 baseClass 属性,那么你像 baseClass 一样反序列化他。 如果你的 baseClass 是抽象的并且包含 baseClass 属性,那么你得到了异常。

这样写比较安全:

public class BaseClass

    public int Int  get; set; 

public class DerivedA : BaseClass

    public string Str  get; set; 
    public BaseClass derived  get; set; 

public class DerivedB : BaseClass

    public bool Bool  get; set; 
    public BaseClass derived  get; set; 




public class BaseClassConverter : JsonConverter<BaseClass>

    private enum TypeDiscriminator
    
        BaseClass = 0,
        DerivedA = 1,
        DerivedB = 2
    

    public override bool CanConvert(Type type)
    
        return typeof(BaseClass) == 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), options);
                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), options);
                break;
            case TypeDiscriminator.BaseClass:
                if (!reader.Read() || reader.GetString() != "TypeValue")
                
                    throw new JsonException();
                
                if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
                
                    throw new JsonException();
                
                baseClass = (BaseClass)JsonSerializer.Deserialize(ref reader,     typeof(BaseClass));
                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, options);
        
        else if (value is DerivedB derivedB)
        
            writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);
            writer.WritePropertyName("TypeValue");
            JsonSerializer.Serialize(writer, derivedB, options);
        
        else if (value is BaseClass baseClass)
        
            writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.BaseClass);
            writer.WritePropertyName("TypeValue");
            JsonSerializer.Serialize(writer, baseClass);
        
        else
        
            throw new NotSupportedException();
        

        writer.WriteEndObject();
    

但是您的 BaseClass 不一定包含 BaseClass 类型或继承者的属性。

【讨论】:

【参考方案8】:

我想引入另一个适合分层、安全、双向、通用使用的实现。

以下注意事项

这是性能和内存的“噩梦”,但对于大多数场景来说已经足够了(原因:因为您需要提前阅读 $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。

【讨论】:

【参考方案9】:

我根据ahsonkhan 的回答更改了一些内容。

我个人喜欢这种方式,因为客户端可以将他们的对象提供给服务器。 但是,'Type' 属性必须在对象中的第一个。

基类和派生类:

public interface IBaseClass

    public DerivedType Type  get; set; 

public class DerivedA : IBaseClass

    public DerivedType Type => DerivedType.DerivedA;
    public string Str  get; set; 

public class DerivedB : IBaseClass

    public DerivedType Type => DerivedType.DerivedB;
    public bool Bool  get; set; 


private enum DerivedType

    DerivedA = 0,
    DerivedB = 1

您可以创建JsonConverter&lt;IBaseClass&gt;,在序列化时读取并检查“类型”属性。它将使用它来确定要反序列化的类型。 由于我们将第一个属性读取为类型,因此必须复制读取器。然后我们必须再次读取完整的对象(将其传递给 Deserialize 方法)。

public class BaseClassConverter : JsonConverter<IBaseClass>

    public override IBaseClass Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options)
    
        // Creating a copy of the reader (The derived deserialisation has to be done from the start)
        Utf8JsonReader typeReader = reader;

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

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

        IBaseClass baseClass = default;
        DerivedType type= (DerivedType)reader.GetInt32();

        switch (type)
        
            case DerivedType.DerivedA:
                baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));
                break;
            case DerivedType.DerivedB:
                baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));
                break;
            default:
                throw new NotSupportedException();
        

        return baseClass;
    

    public override void Write(
        Utf8JsonWriter writer,
        IBaseClass value,
        JsonSerializerOptions options) 
    
        switch(value)
        
            case DerivedA derivedA:
                JsonSerializer.Serialize(writer, derivedA, options);
                break;
            case DerivedB derivedB:
                JsonSerializer.Serialize(writer, derivedB, options);
                break;
            default:
                throw new NotSupportedException();
        
    

客户端现在可以按如下方式发送对象:

// DerivedA

    "Type": 0,
    "Str": "Hello world!"


// DerivedB

    "Type": 1,
    "Bool": false


编辑:

编辑了 Read 方法以能够处理不在第一顺序中的属性名称。现在它只是读取 json 并停止,直到找到 'Type' 属性名称

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

        while (typeReader.Read())
        
            if (typeReader.TokenType != JsonTokenType.PropertyName)
            
                throw new JsonException();
            

            string propertyName = typeReader.GetString();

            if (propertyName.Equals(nameof(IBaseClass.Type)))
            
                break;
            

            typeReader.Skip();
        

        if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.Number)
        
            throw new JsonException();
        

        IGraphOptions baseClass = default;
        GraphType type = (GraphType)typeReader.GetInt32();
        ....
        // The switch..
        ....


说实话,我认为这种自定义 System.Text JsonConverter 的设置方式过于复杂,我更喜欢 Newtonsoft JsonConverter。

【讨论】:

【参考方案10】:

对于接口属性反序列化,我创建了一个简单的 StaticTypeMapConverter

    public class StaticTypeMapConverter<SourceType, TargetType> : JsonConverter<SourceType> 
        where SourceType : class
        where TargetType : class, new()
    

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

            using (var jsonDocument = JsonDocument.ParseValue(ref reader))
            
                var jsonObject = jsonDocument.RootElement.GetRawText();
                var result = (SourceType)JsonSerializer.Deserialize(jsonObject, typeof(TargetType), options);

                return result;
            
        

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

你可以这样使用它:

                var jsonSerializerOptions = new JsonSerializerOptions()
                
                    Converters =  
                        new StaticTypeMapConverter<IMyInterface, MyImplementation>(),
                        new StaticTypeMapConverter<IMyInterface2, MyInterface2Class>(),
                    ,
                    WriteIndented = true
                ;

                var config = JsonSerializer.Deserialize<Config>(configContentJson, jsonSerializerOptions);

【讨论】:

【参考方案11】:

基于接受的答案,但使用 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
 
    //..

【讨论】:

【参考方案12】:

不是非常优雅或高效,但可以快速为少数子类型编写代码:

List<Dictionary<string, object>> generics = JsonSerializer.Deserialize<List<Dictionary<string, object>>>(json);
List<InputOutputInstanceDto> result = new List<ParentType>();
foreach (Dictionary<string, object> item in generics)

    switch(item["dataType"]) // use whatever field is in your parent/interface
    
        case "Type1":
            result.Add(JsonSerializer.Deserialize<Type1>(
                            JsonSerializer.Serialize(item)));
            break
        // add cases for each child type supported
        default:
            result.Add(JsonSerializer.Deserialize<ParentType>(
                            JsonSerializer.Serialize(item)));
            break;
    

【讨论】:

以上是关于System.Text.Json 中是不是可以进行多态反序列化?的主要内容,如果未能解决你的问题,请参考以下文章

在 System.Text.Json 中使用构造函数进行反序列化

不再可能使用 system.text.json 在 c# 中进行序列化/反序列化

使用 System.Text.Json 修改 JSON 文件

(de) 使用 System.Text.Json 序列化流

如何在 System.Text.Json 中使用 JsonConstructor 属性

是否可以在 System.Text.Json 中找到未映射的属性?