(De) 使用 System.Text.Json 在 C# .NET 8 中序列化包含多态泛型的对象列表

问题描述 投票:0回答:1

当尝试序列化对象是泛型和泛型基的派生类型的列表时,我最终遇到错误:

System.InvalidOperationException:'指定类型'CSharpLanguageTestingApp.CustomResult

1[T]' is not a supported derived type for the polymorphic type 'CSharpLanguageTestingApp.CustomResult
1[CSharpLanguageTestingApp.AggregateTwo]'。派生类型必须可分配给基类型,不能是泛型,也不能是抽象类或接口,除非指定“JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor”。'

FallBackToNearestAncestor
此时对我没有任何帮助。

示例类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace CSharpLanguageTestingApp
{
    public class AggregateOne
    {
        int iAmAggregateOne { get; set; } = 1;
    }

    public class AggregateTwo
    {
        public int iAmAggregateTwo { get; set; } = 2;
    }

    [JsonPolymorphic]
    [JsonDerivedType(typeof(CustomResult<>), "CustomResult")]
    [JsonDerivedType(typeof(dbCustomResult<>), "dbCustomResult")]
    [JsonDerivedType(typeof(sbCustomResult<>), "sbCustomResult")]
    [JsonDerivedType(typeof(apiCustomResult<>), "apiCustomResult")]
    public class  CustomResult<T>
    {
        public int iAmAResult { get; set; } = 0;
    }

    public class  dbCustomResult<T> : CustomResult<T>
    {
        public int iAmADbResult { get; set; } = 0;
    }

    public class sbCustomResult<T> : CustomResult<T>
    {
        public int iAmASbResult { get; set; } = 0;
    }

    public class apiCustomResult<T> : CustomResult<T>
    {
        public int iAmAnApiResult { get; set; } = 0;
    }

    public class CompositeResult
    {
        public string Name { get; set; } = "CompositeResult";

        [JsonConverter(typeof(PolymorphicListConverter))]
        public List<object> IndividualResults { get; set; } = new();
    }
}

转换器:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Threading.Tasks;

namespace CSharpLanguageTestingApp
{
    public class PolymorphicListConverter : JsonConverter<List<object>>
    {
        public override List<object> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var results = new List<object>();

            // Ensure we're at the start of the array
            if (reader.TokenType == JsonTokenType.StartArray)
            {
                while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
                {
                    if (reader.TokenType == JsonTokenType.StartObject)
                    {
                        using (JsonDocument doc = JsonDocument.ParseValue(ref reader))
                        {
                            JsonElement root = doc.RootElement;

                            if (root.TryGetProperty("$type", out JsonElement typeElement))
                            {
                                string typeName = typeElement.GetString();
                                Type type = Type.GetType(typeName);

                                if (type != null)
                                {
                                    var json = root.GetProperty("Data").GetRawText();
                                    var obj = JsonSerializer.Deserialize(json, type, options);
                                    results.Add(obj);
                                }
                            }
                        }
                    }
                }
            }

            return results;
        }

        public override void Write(Utf8JsonWriter writer, List<object> value, JsonSerializerOptions options)
        {
            writer.WriteStartArray();

            foreach (var result in value)
            {
                if (result != null)
                {
                    try
                    {
                        // Get the runtime type of the object
                        var resultType = result.GetType();

                        writer.WriteStartObject();
                        writer.WriteString("$type", resultType.AssemblyQualifiedName);
                        writer.WritePropertyName("Data");

                        // Serialize the object as is
                        string json = "";

                        try
                        {
                            json = JsonSerializer.Serialize(result, options); // try 1
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine($"Error serializing object of type {result.GetType()}: {ex.Message}");
                        }

                        try
                        {
                            JsonSerializer.Serialize(writer, result, resultType, options); // try 2
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine($"Error serializing object of type {result.GetType()}: {ex.Message}");
                        }

                        writer.WriteEndObject();
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"Error serializing object of type {result.GetType()}: {ex.Message}");
                        throw; // Rethrow or handle the exception as needed
                    }
                }
            }

            writer.WriteEndArray();
        }
    }
}

要测试的程序:

CompositeResult compositeResult = new CompositeResult();

compositeResult.IndividualResults.Add(new dbCustomResult<AggregateOne>());
compositeResult.IndividualResults.Add(new CustomResult<AggregateTwo>());
compositeResult.IndividualResults.Add(new sbCustomResult<AggregateTwo>());
compositeResult.IndividualResults.Add(new apiCustomResult<AggregateOne>());

var options = new JsonSerializerOptions();
options.Converters.Add(new PolymorphicListConverter());

string json = "";

try
{
    json = System.Text.Json.JsonSerializer.Serialize(compositeResult, options);
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

Console.WriteLine(json);

尝试添加转换器,但我有很多聚合的方法,可以轻松管理 json 选项转换器。尝试修改转换器,但到目前为止还没有成功

到目前为止,在序列化/反序列化时,我无法让泛型派生类型和基类型在同一个列表中很好地发挥作用

从列表中删除

CustomResult
有效:

{
    "Name": "CompositeResult",
    "IndividualResults": [
          {
              "$type": "CSharpLanguageTestingApp.dbCustomResult\u00601[[CSharpLanguageTestingApp.AggregateOne, CSharpLanguageTestingApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], CSharpLanguageTestingApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
              "Data": { "iAmADbResult": 0, "iAmAResult": 0 }
          },
          {
              "$type": "CSharpLanguageTestingApp.sbCustomResult\u00601[[CSharpLanguageTestingApp.AggregateTwo, CSharpLanguageTestingApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], CSharpLanguageTestingApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
              "Data": { "iAmASbResult": 0, "iAmAResult": 0 } 
          },
          {
              "$type": "CSharpLanguageTestingApp.apiCustomResult\u00601[[CSharpLanguageTestingApp.AggregateOne, CSharpLanguageTestingApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], CSharpLanguageTestingApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
              "Data": { "iAmAnApiResult": 0, "iAmAResult": 0 }
        }
    ] 
}
c# json polymorphism system.text.json
1个回答
0
投票

您收到异常的原因是您应用于基类的

JsonDerivedType
属性指定了
开放泛型类型,如CustomResult<T>。  然而,属性中指定的派生类型应该是被序列化的对象的实际、具体类型——并且实例化的 POCO 
never
具有开放泛型作为其类型。 也许您希望 System.Text.Json 通过替换泛型参数来自动关闭开放的泛型类型 dbCustomResult<>,但是 MSFT 尚未实现此类功能。
那么,你有什么选择?

首先

,您可以按预期使用 System.Text.Json 中内置的多态性支持,指定列表中可能遇到的所有可能的封闭泛型类型。 最简单的方法是提取 T 的非泛型抽象基类型,将所有派生类型属性应用于它,然后在列表中使用它,如下所示:

CustomResult<T>

长长的派生类型属性列表有点难看,但通过这种方法,您可以完全放弃编写转换器。  您还可以为您的 
[JsonPolymorphic] [JsonDerivedType(typeof(CustomResult<AggregateOne>), "CustomResult<AggregateOne>")] [JsonDerivedType(typeof(dbCustomResult<AggregateOne>), "dbCustomResult<AggregateOne>")] [JsonDerivedType(typeof(sbCustomResult<AggregateOne>), "sbCustomResult<AggregateOne>")] [JsonDerivedType(typeof(apiCustomResult<AggregateOne>), "apiCustomResult<AggregateOne>")] [JsonDerivedType(typeof(CustomResult<AggregateTwo>), "CustomResult<AggregateTwo>")] [JsonDerivedType(typeof(dbCustomResult<AggregateTwo>), "dbCustomResult<AggregateTwo>")] [JsonDerivedType(typeof(sbCustomResult<AggregateTwo>), "sbCustomResult<AggregateTwo>")] [JsonDerivedType(typeof(apiCustomResult<AggregateTwo>), "apiCustomResult<AggregateTwo>")] public abstract class CustomResultBase; public class CustomResult<T> : CustomResultBase { public int iAmAResult { get; set; } = 0; } public class CompositeResult { public string Name { get; set; } = "CompositeResult"; public List<CustomResultBase> IndividualResults { get; set; } = new(); }

列表获得类型安全,这使其成为我认为的首选解决方案。

演示小提琴#1 

这里

或者

,您可以完全避免 System.Text.Json 的多态性支持,并编写自己的转换器,就像 System.Text.Json 中可以进行多态反序列化吗? 中的转换器之一一样。 但是,您当前的转换器尝试实例化“JSON 文件中指定的任何类型”。 这会在您的代码中引入一个众所周知的“严重”安全漏洞,即“第 13 号星期五 JSON 攻击”漏洞。 这是 Newtonsoft 报告的问题,System.Text.Json 通过仅允许反序列化白名单类型来避免该问题。 如果反序列化 IndividualResults 指定的任何类型,您将在代码中引入相同的漏洞,从而允许攻击者制作 JSON 有效负载,从而删除磁盘上的所有文件或运行任意代码。 更多请参见: 由于 Json.Net TypeNameHandling auto 导致外部 json 易受攻击?

Newtonsoft Json 中的 TypeName 处理小心
  • OWASP 备忘单系列反序列化备忘单
  • 如果您选择走这条路线,您将需要确保至少已知的攻击工具类型不会被反序列化。 我重写了您的代码以添加必要的检查,此外,我将列表转换器和多态项转换器分离为单独的类,避免将 JSON 预加载到 Type.GetType(typeName);
  • 中,并修复了一些未跳过已知属性的错误正确。 新的转换器看起来像:
  • JsonDocument 您的模型将进行如下修改,并删除所有 public class PolymorphicListItemConverter<TBase> : ListItemConverterDecorator<TBase, PolymorphicConverter<TBase>>; public class PolymorphicConverter<TBase> : JsonConverter<TBase> { const string TypeProperty = "$type"; const string DataProperty = "Data"; protected virtual Type? BindToType(string? typeName) { // TODO: You will need to think how to sanitize your types further if (string.IsNullOrEmpty(typeName)) return null; if (JsonExtensions.ContainsKnownDangerousTypeNames(typeName)) throw new JsonException($"Invalid type {typeName}"); return Type.GetType(typeName); } public override TBase? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var comparison = options.PropertyNameCaseInsensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; if (reader.TokenType == JsonTokenType.Null) return typeof(TBase).IsValueType && Nullable.GetUnderlyingType(typeof(TBase)) == null ? throw new JsonException() : default; else if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException(); if (!reader.TryScanForwardForPropertyValue(TypeProperty, comparison, out var readerCopy)) throw new JsonException("Missing type name"); var type = BindToType(readerCopy.GetString()); if (type == null || !type.IsAssignableTo(typeof(TBase))) throw new JsonException($"Invalid type name {readerCopy.GetString()}"); TBase? value = default; while (reader.ReadAndAssert().TokenType != JsonTokenType.EndObject) { var match = DataProperty.Equals(reader.AssertTokenType(JsonTokenType.PropertyName).GetString(), comparison); reader.ReadAndAssert(); if (match) value = (TBase?)JsonSerializer.Deserialize(ref reader, type, options); else reader.Skip(); } return value; } public override void Write(Utf8JsonWriter writer, TBase value, JsonSerializerOptions options) { if (value is null) { writer.WriteNullValue(); return; } writer.WriteStartObject(); var valueType = value.GetType(); writer.WriteString(TypeProperty, valueType.AssemblyQualifiedName); writer.WritePropertyName(DataProperty); JsonSerializer.Serialize(writer, value, valueType, options); writer.WriteEndObject(); } } public class ListItemConverterDecorator<TItem, TConverter> : JsonConverter<List<TItem>> where TConverter : JsonConverter, new() { readonly JsonConverter<TItem> itemConverter = (JsonConverter<TItem>)(new JsonSerializerOptions { Converters = { new TConverter() } }) .GetConverter(typeof(TItem)); public override List<TItem>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) return null; else if (reader.TokenType != JsonTokenType.StartArray) throw new JsonException(); var list = new List<TItem>(); while (reader.ReadAndAssert().TokenType != JsonTokenType.EndArray) list.Add(itemConverter.Read(ref reader, typeof(TItem), options)!); return list; } public override void Write(Utf8JsonWriter writer, List<TItem> value, JsonSerializerOptions options) { if (value is null) writer.WriteNullValue(); else { writer.WriteStartArray(); foreach (var item in value) itemConverter.Write(writer, item, options); writer.WriteEndArray(); } } } public static class JsonExtensions { // Taken from // https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html#known-net-rce-gadgets // As of November 2024 static readonly string [] KnownDangerousTypes = [ "System.CodeDom.Compiler.TempFileCollection", "System.Configuration.Install.AssemblyInstaller", "System.Activities.Presentation.WorkflowDesigner", "System.Windows.ResourceDictionary", "System.Windows.Data.ObjectDataProvider", "System.Windows.Forms.BindingSource", "Microsoft.Exchange.Management.SystemManager.WinForms.ExchangeSettingsProvider", "System.Data.DataViewManager", "System.Xml.XmlDocument", "System.Xml.XmlDataDocument", "System.Management.Automation.PSObject", ]; public static bool ContainsKnownDangerousTypeNames(string typeName) => KnownDangerousTypes.Any(n => typeName.Contains(n)); public static bool TryScanForwardForPropertyValue(ref readonly this Utf8JsonReader reader, string name, StringComparison comparison, out Utf8JsonReader copy) { copy = reader.AssertTokenType(JsonTokenType.StartObject); while (copy.ReadAndAssert().TokenType != JsonTokenType.EndObject) { var match = name.Equals(copy.AssertTokenType(JsonTokenType.PropertyName).GetString(), comparison); copy.ReadAndAssert(); if (match) return true; else copy.Skip(); } return false; } public static ref Utf8JsonReader ReadAndAssert(ref this Utf8JsonReader reader) { if (!reader.Read()) { throw new JsonException(); } return ref reader; } public static ref readonly Utf8JsonReader AssertTokenType(ref readonly this Utf8JsonReader reader, JsonTokenType type) { if (reader.TokenType != type) throw new JsonException(); return ref reader; } }
JsonDerivedType

属性:

JsonPolymorphic

您的 
public class CustomResult<T> { public int iAmAResult { get; set; } = 0; } public class CompositeResult { public string Name { get; set; } = "CompositeResult"; [JsonConverter(typeof(PolymorphicListItemConverter<object>))] public List<object> IndividualResults { get; set; } = new(); }

现在可以成功往返。 演示小提琴 #2

这里
.
您需要考虑如何确保 

CompositeResult
 只返回您期望的类型。  一种可能性是再次为 
protected virtual Type? BindToType(string? typeName)

引入一个非泛型基类,同时也将泛型参数

CustomResult<T>
限制为某种预期的多态类型。 这样做后,您将不太可能允许狡猾的类型被反序列化。

© www.soinside.com 2019 - 2024. All rights reserved.