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

发布于 2025-01-19 15:33:04 字数 151 浏览 0 评论 0原文

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

I try to migrate from Newtonsoft.Json to System.Text.Json.
I want to deserialize abstract class. Newtonsoft.Json has TypeNameHandling for this.
Is there any way to deserialize abstract class via System.Text.Json on .net core 3.0?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(18

吃不饱 2025-01-26 15:33:05

我最终得到了这个解决方案。它对我来说很轻量级且足够通用。

类型鉴别器转换器

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

I ended up with that solution. It's lightwight and a generic enough for me.

The type discriminator converter

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

The interface

public interface ITypeDiscriminator
{
    string TypeDiscriminator { get; }
}

And the example models

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

And here is the test method

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);
    }
梦断已成空 2025-01-26 15:33:05

目前,借助 .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, ... }

我希望这可以帮助你

currently with new feature of .NET 7 we can do this without write handy codes to implement this.
see here: 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, ... }

i hope this can help you

铁轨上的流浪者 2025-01-26 15:33:05

请尝试我写的这个库作为System.Text.json提供多态性的扩展:
https://github.com/dahomey-technologies/dahomomey.json.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; }
}

继承的类:必须手动注册到Incistiminator junterion注册表中,以便让框架知道歧视器值和类型之间的映射:

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
}

Please try this library I wrote as an extension to System.Text.Json to offer polymorphism:
https://github.com/dahomey-technologies/Dahomey.Json

If the actual type of a reference instance differs from the declared type, the discriminator property will be automatically added to the output 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; }
}

Inherited classes must be manually registered to the discriminator convention registry in order to let the framework know about the mapping between a discriminator value and a type:

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

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

Result:

{
  "$type": "Tests.WeatherForecastDerived, Tests",
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot",
  "WindSpeed": 35
}
拧巴小姐 2025-01-26 15:33:05

多数民众赞成在我的所有抽象类型:

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

Thats my JsonConverter for all abstract types:

        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);
        }
九命猫 2025-01-26 15:33:05

基于接受的答案,但是使用newtypeattribute发现类型(通常列举所有类型都会导致不需要的类型加载异常),并在转换器中添加歧视属性,而不是让类实现该类别:

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
{ 
    //..
}

Basing on the accepted answer, but using KnownTypeAttribute to discover the types (often enumerating all types can lead to unwanted type load exceptions) , and adding the discriminator property in the converter instead of having the class implement it itself:

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

which you can use like this:

[JsonConverter(typeof(JsonInheritanceConverter))]
[KnownType(typeof(DerivedA))]
[KnownType(typeof(DerivedB))]
public abstract class BaseClass
{ 
    //..
}
香草可樂 2025-01-26 15:33:05

我真的很喜欢 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);
    }

}

disciminatorAttribute:

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

}

IndiciminatorValueAttribute:

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

}

和...voilà!

剩下要做的就是注册工厂:

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

I really liked the answer of Demetrius, but I think you can go even further in terms of re-usability. I came up with the following solution:

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

}

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

}

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

}

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

}

And finally, an example of how to use it on classes:

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

    public virtual IdentityType Type { get; protected set; }

}

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

}

And... Voilà!

All that is left to do is to register the factory:

 this.Services.AddControllersWithViews()
            .AddJsonOptions(options => 
            {
                options.JsonSerializerOptions.Converters.Add(new AbstractClassConverterFactory(options.JsonSerializerOptions.PropertyNamingPolicy));
            });
月棠 2025-01-26 15:33:05

将此选项扔出去:使用源代码生成器为具有特殊属性标记的属性的对象自动生成JSONCONVERTER,

您可以使用此软件包尝试一下,但是它需要.net5

https://github.com/wivuu/wivuu/wivuu.jsonpolymormorphism

发电机看着具有歧视属性的属性的类型,然后看上去对于从持有歧视器的类型继承的类型,以与枚举源的每种情况相匹配

https://github.com/wivuu/wivuu.jsonpolymorphism/blob/blob/master/wivuu.jsonpolymorphism/jsonconconvertertertergergenerator.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");

Throwing this option out there: Using a source code generator to generate a JsonConverter automatically for objects with a property marked with a special attribute

You can try it with this package, but it requires .net5

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

The generator looks at the type of the property marked with a discriminator attribute, and then looks for types inheriting from the type holding the discriminator to match up with each case of the enum

Source here: 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");
空‖城人不在 2025-01-26 15:33:05

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

以下警告

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

I want to throw in another implementation suitable for hierarchical, secure, bi-directional, generic usage.

The following caveats

  • It is a performance and memory "nightmare" but good enough for most scenarios (why: because you need to read ahead $type and then would need to go back on the reader).
  • It works only if the polymorphic base is abstract / never serialized as instance itself (why: because otherwise the regular converter cannot work on the derived classes as it goes into stack overflow).
  • Works under .NET 6 ... will not in 3.1.

Example

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

// ...

Converter

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(
quot;{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(
quot;{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(
quot;{type.Name} with matching base type {typeof(TBaseType).Name} is not registered.");
        }
    }
}

If you find something, shoot me a comment.

Some kudos to 1.

想你只要分分秒秒 2025-01-26 15:33:05

我根据

我个人喜欢这种方式,因为客户端可以将其对象提供给服务器。
但是,“类型”属性必须首先在对象中。

基类和派生类:

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;,该> 在序列化时读取和检查“ type”属性。它将使用它来找出应进行的类型。
自从我们将第一个属性视为类型以来,必须复制读者。然后,我们必须再次读取完整的对象(将其传递到避免方法)。

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
}

编辑:

编辑了读取方法,以便能够处理不在一阶的属性名称。现在,它只是通过JSON读取并停止,直到发现“类型”属性名称

 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..
        ....


是诚实的,我认为设置此自定义系统的方式。

I changed a couple things based on ahsonkhan's answer.

Personally I like this way since the client can just give their object to the server.
However, the 'Type' property must be first in the object.

Base class and derived classes:

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
}

You can create JsonConverter<IBaseClass> that reads and checks the 'Type' property while serializing. It will use that to figure out which type to deserialize.
The reader has to be copied since we read the first property as the type. And then we have to read the full object again (pass it to the Deserialize method).

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

The client is now able to send objects as follows:

// DerivedA
{
    "Type": 0,
    "Str": "Hello world!"
}

// DerivedB
{
    "Type": 1,
    "Bool": false
}

EDIT:

Edited the Read method to be able to deal with the property name not being in the first order. Now it just reads through the json and stops until it finds the 'Type' property name

 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..
        ....


To be honest, I think the way this custom System.Text JsonConverter is set up is unneccesary complex and I prefer the Newtonsoft JsonConverter.

一袭白衣梦中忆 2025-01-26 15:33:05

请不要这样写,

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

如果您的课堂包含大本产期属性, 那么您可以像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();
    }
}

但是,您的基本镜不得包含带有类型的基本或继承器的属性。

Don't write like this

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

If you class contain baseClass property then you deserialize him like baseClass.
If you baseClass is abstract and contain baseClass property then you got Exception.

It's safer to write like this:

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

But you BaseClass don't must contain property with type BaseClass or inheritor.

嘿看小鸭子会跑 2025-01-26 15:33:05

多态性支持作为预览版本(v7) 发布。

https://github.com/dotnet/runtime/issues/63747

Polymorphism support is released as preview versions(v7).

https://github.com/dotnet/runtime/issues/63747

梦里泪两行 2025-01-26 15:33:05

对于接口属性反序列化,我创建了一个简单的 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);

For interface property deserialization I've created a simple 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);
        }
    }

You can use it like this:

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

                var config = JsonSerializer.Deserialize<Config>(configContentJson, jsonSerializerOptions);
夜司空 2025-01-26 15:33:05

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

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

Not very elegant or efficient, but quick to code for a small number of child types:

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;
    }
}
绿萝 2025-01-26 15:33:05

我喜欢与您分享我使用System.Text.json发现的问题。我遵循该方法typediscriminatorConverter demetrius axenowski> demetrius axenowski 。它运行良好。

当我为JSON添加一些注释时,我的问题始于我的问题。例如:

[JsonPropertyName("name")]

我整天都迷路了,以了解为什么代码不起作用。我创建了一些虚拟代码来了解问题在哪里。现在,所有源代码都在 github 上。

因此,问题是在jsonpropertyname我在转换器中检查的属性。例如,这是

public class Radiobutton : ElementBase
{
    [JsonPropertyName("type")]
    public string Type => "Radiobutton";
    public ElementType ElementType = ElementType.Radiobutton;

    public List<string>? Choices { get; set; }
}

您可以看到的类,我设置了jsonpropertyname,因为我喜欢在较低的情况下查看type。现在,如果我使用此转换器转换课程:

public class ElementTypeConverter<T> : JsonConverter<T> where T : IElementType
{
    private readonly IEnumerable<Type> _types;

    public ElementTypeConverter()
    {
        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(IElementType.Type), 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);
    }
}

我会收到以下错误:

测试方法soundusexamplenetStardard21.tests.unittest1.testconversionjson_systemtextjson_3textbox_1radiobutton丢下了例外:

system.text.json.jsonexception:json值无法转换为system.collections.generic.list'1 [SurvusionexamplenetStardArd21.interfaces.ielement]。路径:$。元素[3] |亚麻布:42 | Bytepositioninline:5。

我删除了jsonpropertyname,并且可以正常工作。我试图设置

[JsonPropertyName("Type")]

(基本上与变量相同),并且效果很好。因此,不要更改名称。转换器正在两种方式工作(对json和json对象对象)。这是测试代码:

var jsonSerializerOptions = new JsonSerializerOptions()
{
    Converters = { new ElementTypeConverter<IElement>() },
    WriteIndented = true
};
var json = JsonSerializer.Serialize(form, jsonSerializerOptions);

var back = JsonSerializer.Deserialize<Form>(json, jsonSerializerOptions);

var json2 = JsonSerializer.Serialize(back, jsonSerializerOptions);

另一个注释与newtonsoft.json:我将对象转换为JSON,并且没有任何特定的配置,它很好。当我尝试将结果json转换为对象中时,我会在转换中出现问题。

I like to share with you an issue I found using System.Text.Json. I followed the approach TypeDiscriminatorConverter that Demetrius Axenowski. It works very well.

My problems started when I added some annotations for the JSON. For example:

[JsonPropertyName("name")]

I have lost all day to understand why the code didn't work. I created some dummy code to understand where the problem was. All the source code is now on GitHub.

So, the problem was in the JsonPropertyName for the property I check in the converter. For example, this is a class

public class Radiobutton : ElementBase
{
    [JsonPropertyName("type")]
    public string Type => "Radiobutton";
    public ElementType ElementType = ElementType.Radiobutton;

    public List<string>? Choices { get; set; }
}

As you can see, I set the JsonPropertyName because I like to see type in lower case. Now, if I convert the class with this converter:

public class ElementTypeConverter<T> : JsonConverter<T> where T : IElementType
{
    private readonly IEnumerable<Type> _types;

    public ElementTypeConverter()
    {
        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(IElementType.Type), 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);
    }
}

I get the following error:

Test method SurveyExampleNetStardard21.Tests.UnitTest1.TestConversionJson_SystemTextJson_3Textbox_1radiobutton threw exception:

System.Text.Json.JsonException: The JSON value could not be converted to System.Collections.Generic.List`1[SurveyExampleNetStardard21.Interfaces.IElement]. Path: $.Elements[3] | LineNumber: 42 | BytePositionInLine: 5.

I removed the JsonPropertyName and it works fine. I tried to set

[JsonPropertyName("Type")]

(basically, the same as the variable) and it works fine. So, don't change the name. The converter is working both ways (object to Json and Json to object). This is the test code:

var jsonSerializerOptions = new JsonSerializerOptions()
{
    Converters = { new ElementTypeConverter<IElement>() },
    WriteIndented = true
};
var json = JsonSerializer.Serialize(form, jsonSerializerOptions);

var back = JsonSerializer.Deserialize<Form>(json, jsonSerializerOptions);

var json2 = JsonSerializer.Serialize(back, jsonSerializerOptions);

Another annotation is related to Newtonsoft.Json: I converted the object to Json and it was good without any particular configuration. When I tried to convert the result Json in the object, I got issues in the conversion.

趁年轻赶紧闹 2025-01-26 15:33:05

如果我正确地说,您想显示派生类对象的所有成员,并具有对基类的对象的引用。请检查此示例是否适合您的需求:

using System.Text.Json;

BaseClass product1 = new Product
{
    Id = 1,
    Name = "Apple",
    Price = 999.00m
};

var jsonOptions = new JsonSerializerOptions
{
    WriteIndented = true
};
Console.WriteLine("Serialize BaseClass:");
Console.WriteLine(JsonSerializer.Serialize(product1, jsonOptions));
Console.WriteLine("Serialize (object)BaseClass:");
Console.WriteLine(JsonSerializer.Serialize((object)product1, jsonOptions));

class BaseClass
{
    public int Id { get; set; }
}

class Product : BaseClass
{
    public required string Name { get; set; }
    public required decimal Price { get; set; }
}

输出将是:

Serialize BaseClass:
{
  "Id": 1
}
Serialize (object)BaseClass:
{
  "Name": "Apple",
  "Price": 999.00,
  "Id": 1
}

因此,您只需要将基类实例投射为object

If i got you right, you want to display all members of the object of derived class, having the reference to the object of the base class. Please check this example if that will suit your needs:

using System.Text.Json;

BaseClass product1 = new Product
{
    Id = 1,
    Name = "Apple",
    Price = 999.00m
};

var jsonOptions = new JsonSerializerOptions
{
    WriteIndented = true
};
Console.WriteLine("Serialize BaseClass:");
Console.WriteLine(JsonSerializer.Serialize(product1, jsonOptions));
Console.WriteLine("Serialize (object)BaseClass:");
Console.WriteLine(JsonSerializer.Serialize((object)product1, jsonOptions));

class BaseClass
{
    public int Id { get; set; }
}

class Product : BaseClass
{
    public required string Name { get; set; }
    public required decimal Price { get; set; }
}

The output will be:

Serialize BaseClass:
{
  "Id": 1
}
Serialize (object)BaseClass:
{
  "Name": "Apple",
  "Price": 999.00,
  "Id": 1
}

So you just need to cast your instance of the base class to object

无可置疑 2025-01-26 15:33:05

最近遇到了这一点,并提出了一个非常简单的转换器。我有一个creditreport类,可以根据报告成功完成,失败或仍在待处理,可以返回不同的属性。

我不喜欢添加$ type属性作为类型歧视器,因为我的模型已经具有一个称为status的属性,该属性具有相同的目的。但是,由于utf8jsonreader是唯一的读者,因此使用它是一个挑战。我最终要做的是将JSON解析为jsondocument,检查status列,然后对jsondocument本身进行挑选,将。

public class CreditReportConverter : JsonConverter<Result>
{
    public override bool CanConvert(Type type) =>
        type.IsAssignableTo(typeof(Result));

    public override Result Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        using JsonDocument json = JsonDocument.ParseValue(ref reader);

        if (!json.RootElement.TryGetProperty("status", out JsonElement element))
            throw new JsonException("CreditReport is missing 'status' property");

        if (!Enum.TryParse(element.GetString(), out Status status))
            throw new JsonException($"Unrecognized CreditReport status: {element.GetString()}");

        Result? result = status switch
        {
            Status.Complete => json.Deserialize<Success>(options),
            Status.Failed => json.Deserialize<Failure>(options),
            _ => json.Deserialize<Result>(options)
        };

        return result ?? throw new JsonException("Could not deserialize CreditReport");
    }

    public override void Write(Utf8JsonWriter writer, Result report, JsonSerializerOptions options)
    {
        string json = report switch
        {
            Success success => JsonSerializer.Serialize(success, options),
            Failure failure => JsonSerializer.Serialize(failure, options),
            _ => JsonSerializer.Serialize(report, options)
        };

        writer.WriteRawValue(json);
    }
}

Recently came across this and came up with a very simple type converter. I have a CreditReport class that can return different properties depending on whether the report completed successfully, failed, or is still pending.

I didn't love adding a $type property as a type discriminator, because my model already has a property called status that serves the same purpose. It's a challenge to use it though, due to Utf8JsonReader being a forward only reader. What I ended up doing is parsing the json as a JsonDocument, checking the status column, and then deserializing the JsonDocument itself into the correct derived class.

public class CreditReportConverter : JsonConverter<Result>
{
    public override bool CanConvert(Type type) =>
        type.IsAssignableTo(typeof(Result));

    public override Result Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        using JsonDocument json = JsonDocument.ParseValue(ref reader);

        if (!json.RootElement.TryGetProperty("status", out JsonElement element))
            throw new JsonException("CreditReport is missing 'status' property");

        if (!Enum.TryParse(element.GetString(), out Status status))
            throw new JsonException(
quot;Unrecognized CreditReport status: {element.GetString()}");

        Result? result = status switch
        {
            Status.Complete => json.Deserialize<Success>(options),
            Status.Failed => json.Deserialize<Failure>(options),
            _ => json.Deserialize<Result>(options)
        };

        return result ?? throw new JsonException("Could not deserialize CreditReport");
    }

    public override void Write(Utf8JsonWriter writer, Result report, JsonSerializerOptions options)
    {
        string json = report switch
        {
            Success success => JsonSerializer.Serialize(success, options),
            Failure failure => JsonSerializer.Serialize(failure, options),
            _ => JsonSerializer.Serialize(report, options)
        };

        writer.WriteRawValue(json);
    }
}
鲜肉鲜肉永远不皱 2025-01-26 15:33:04

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

答案是“是”和“否”,具体取决于您所说的“可能”的含义。

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

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

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

该文档展示了如何使用类型鉴别器属性来做到这一点的示例:
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,它在序列化时写入类型鉴别器并读取它以找出要反序列化的类型。您可以在 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 的自定义转换器中手动序列化/反序列化子对象的简单方法?

Is polymorphic deserialization possible in System.Text.Json?

The answer is yes and no, depending on what you mean by "possible".

There is no polymorphic deserialization (equivalent to Newtonsoft.Json's TypeNameHandling) support built-in to System.Text.Json prior to .NET 7. This is because reading the .NET type name specified as a string within the JSON payload (such as $type metadata property) to create your objects is not recommended since it introduces potential security concerns (see https://github.com/dotnet/corefx/issues/41347#issuecomment-535779492 for more info).

Allowing the payload to specify its own type information is a common source of vulnerabilities in web applications.

However, there is a way to add your own support for polymorphic deserialization by creating a JsonConverter<T>, so in that sense, it is possible.

The docs show an example of how to do that using a type discriminator property:
https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to#support-polymorphic-deserialization

Let's look at an example.

Say you have a base class and a couple of derived classes:

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

You can create the following JsonConverter<BaseClass> that writes the type discriminator while serializing and reads it to figure out which type to deserialize. You can register that converter on the 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();
    }
}

This is what serialization and deserialization would look like (including comparison with 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());
}

Here's another StackOverflow question that shows how to support polymorphic deserialization with interfaces (rather than abstract classes), but a similar solution would apply for any polymorphism:
Is there a simple way to manually serialize/deserialize child objects in a custom converter in System.Text.Json?

氛圍 2025-01-26 15:33:04

白名单继承类型的多态序列化已在 .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 的值时,您将能够包含 DerivedType1 的属性为所有派生类型添加到 BaseType

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

以这种方式将 DerivedType1 列入白名单,模型的序列化:

var json = JsonSerializer.Serialize(list);

导致

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

Demo fiddle #1 此处

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

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

那么 JsonSerializer.Serialize(new BaseType [] { new DerivedType3 { Derived3 = "value 3" } }) 将抛出 System .NotSupportedException:多态类型“BaseType”不支持运行时类型“DerivedType3” 异常。演示小提琴 #2 此处

这涵盖了序列化。如果您需要往返类型层次结构,则需要提供类型鉴别器属性值以用于每个派生类型。这可以通过为 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);

并且序列化的实际具体类型将被保留。演示小提琴 #3 此处

还可以通过 合同定制。当您的类型层次结构无法修改时,或者某些派生类型位于不同的程序集中并且无法在编译时引用时,或者您尝试在多个旧序列化程序之间进行互操作时,您可能需要执行此操作。这里的基本工作流程是实例化 DefaultJsonTypeInfoResolver 并添加 修饰符,用于设置必要的 JsonTypeInfo 作为您的基本类型。

例如,可以在运行时启用 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);

Demo fiddle #4 此处

注意:

  1. 白名单方法与数据契约序列化程序的方法一致,后者使用KnownTypeAttributeXmlSerializer,它使用 XmlIncludeAttribute。它与 Json.NET 不一致,其 TypeNameHandling 序列化所有类型的类型信息,除非通过 序列化绑定器

    仅允许反序列化白名单类型可防止 13 号星期五:JSON 攻击 类型注入攻击,包括 Newtonsoft Json 中的 TypeNameHandling 小心由于 Json.Net TypeNameHandling auto 导致外部 json 易受攻击?

  2. 整数和字符串都可以用于类型鉴别器名称。如果您按如下方式定义类型层次结构:

    [JsonDerivedType(typeof(DerivedType1), 1)]
    [JsonDerivedType(typeof(DerivedType2), 2)]
    public abstract class BaseType { } // 省略属性
    

    然后序列化上面的列表结果

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

    但是 Newtonsoft 不使用数字类型鉴别器值,因此如果您与旧序列化器进行互操作,您可能希望避免这种情况。

  3. 默认类型鉴别器属性名称 "$type" 与 Json.NET 使用的类型鉴别器名称相同。如果您希望使用不同的属性名称,例如名称 "__type"DataContractJsonSerializer 使用,应用 JsonPolymorphicAttribute 到基本类型和设置 TypeDiscriminatorPropertyName 像这样:

    [JsonPolymorphic(TypeDiscriminatorPropertyName = "__type")]
    [JsonDerivedType(typeof(DerivedType1), "DerivedType1")]
    [JsonDerivedType(typeof(DerivedType2), "DerivedType2")]
    public abstract class BaseType { } // 省略属性
    
  4. 如果您与 Json.NET(或 DataContractJsonSerializer)进行互操作,则可以设置 TypeDiscriminator 等于旧序列化器使用的类型鉴别器值。

  5. 如果序列化程序遇到未列入白名单的派生类型,您可以通过设置 JsonPolymorphicAttribute.UnknownDerivedTypeHandling 为以下之一

    <块引用>
    <表类=“s-表”>
    <标题>

    JsonUnknownDerivedTypeHandling

    含义


    <正文>

    序列化失败
    0
    未声明运行时类型的对象将无法进行多态序列化。

    FallBackToBaseType
    1
    未声明的运行时类型的对象将回退到基本类型的序列化协定。

    回退到最近的祖先
    2
    未声明的运行时类型的对象将恢复为最近声明的祖先类型的序列化协定。由于钻石模糊性限制,某些接口层次结构不受支持。


Polymorphic serialization of whitelisted inherited types has been implemented in .NET 7, and is available in Preview 6.

From the documentation page What’s new in System.Text.Json in .NET 7: Type Hierarchies:

System.Text.Json now supports polymorphic serialization and deserialization of user-defined type hierarchies. This can be enabled by decorating the base class of a type hierarchy with the new JsonDerivedTypeAttribute.

First, let's consider serialization. Say you have the following type hierarchy:

public abstract class BaseType { } // Properties omitted

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

And you have a data model that includes a value whose declared type is BaseType, e.g.

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

In previous versions, System.Text.Json would only serialize the properties of the declared type BaseType. Now you will be able to include the properties of DerivedType1 when serializing a value declared as BaseType by adding [JsonDerivedType(typeof(TDerivedType))] to BaseType for all derived types:

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

Having whitelisted DerivedType1 in this manner, serialization of your model:

var json = JsonSerializer.Serialize(list);

Results in

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

Demo fiddle #1 here.

Do note that only derived types whitelisted via attribute (or through setting JsonTypeInfo.PolymorphismOptions in runtime) can be serialized via this mechanism. If you have some other derived type which is not whitelisted, e.g.:

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

Then JsonSerializer.Serialize(new BaseType [] { new DerivedType3 { Derived3 = "value 3" } }) will throw a System.NotSupportedException: Runtime type 'DerivedType3' is not supported by polymorphic type 'BaseType' exception. Demo fiddle #2 here.

That covers serialization. If you need to round-trip your type hierarchy, you will need to supply a type discriminator property value to use for each derived type. This may be done providing a value for JsonDerivedTypeAttribute.TypeDiscriminator for each derived type:

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

Now when you serialize your model

var json = JsonSerializer.Serialize(list);

System.Text.Json will add an artificial type discriminator property "$type" indicating the type that was serialized:

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

Having done so, you can now deserialize your data model like so:

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

And the actual, concrete type(s) serialized will be preserved. Demo fiddle #3 here.

It is also possible to inform System.Text.Json of your type hierarchy in runtime via Contract Customization. You might need to do this when your type hierarchy cannot be modified, or when some derived types are in different assemblies and cannot be referenced at compile time, or you are trying to interoperate between multiple legacy serializers. The basic workflow here will be to instantiate an instance of DefaultJsonTypeInfoResolver and add a modifier which sets up the necessary PolymorphismOptions for the JsonTypeInfo for your base type.

For example, polymorphic serialization for the BaseType hierarchy can be enabled in runtime like so:

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

Demo fiddle #4 here.

Notes:

  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:

    JsonUnknownDerivedTypeHandlingValueMeaning
    FailSerialization0An object of undeclared runtime type will fail polymorphic serialization.
    FallBackToBaseType1An object of undeclared runtime type will fall back to the serialization contract of the base type.
    FallBackToNearestAncestor2An 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.
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文