【问题标题】: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 反序列化抽象类?

【问题讨论】:

标签: c# json .net-core-3.0 system.text.json


【解决方案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());
}

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

【讨论】:

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

【讨论】:

【解决方案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);
        }

【讨论】:

【解决方案5】:

我真的很喜欢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:

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

}

鉴别器属性:

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2021-11-21
              • 2021-07-17
              • 1970-01-01
              • 1970-01-01
              • 2020-04-23
              相关资源
              最近更新 更多