当尝试序列化对象是泛型和泛型基的派生类型的列表时,我最终遇到错误:
System.InvalidOperationException:'指定类型'CSharpLanguageTestingApp.CustomResult
1[CSharpLanguageTestingApp.AggregateTwo]'。派生类型必须可分配给基类型,不能是泛型,也不能是抽象类或接口,除非指定“JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor”。'1[T]' is not a supported derived type for the polymorphic type 'CSharpLanguageTestingApp.CustomResult
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 }
}
]
}
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 易受攻击?
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>
限制为某种预期的多态类型。 这样做后,您将不太可能允许狡猾的类型被反序列化。