System.Text.Json 中是否可以进行多态反序列化?
Is polymorphic deserialization possible in System.Text.Json?
我尝试从 Newtonsoft.Json 迁移到 System.Text.Json。
我想反序列化摘要 class。 Newtonsoft.Json 对此有 TypeNameHandling。
有什么方法可以在 .net core 3.0 上通过 System.Text.Json 反序列化抽象 class?
请试试我写的这个库作为 System.Text.Json 的扩展以提供多态性:
https://github.com/dahomey-technologies/Dahomey.Json
如果引用实例的实际类型与声明类型不同,鉴别器属性将自动添加到输出json:
public class WeatherForecast
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string Summary { get; set; }
}
public class WeatherForecastDerived : WeatherForecast
{
public int WindSpeed { get; set; }
}
Inherited 类 必须手动注册到鉴别器约定注册表,以便让框架了解鉴别器值和类型之间的映射:
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
}
Is polymorphic deserialization possible in System.Text.Json?
答案是而不是,取决于你的意思 "possible".
有没有多态反序列化(相当于Newtonsoft.Json的TypeNameHandling
)支持内置至 System.Text.Json
。这是因为读取 JSON 有效负载中指定为字符串的 .NET 类型名称(例如 $type
元数据 属性)来创建对象 不推荐 因为它引入了潜在的安全问题(有关详细信息,请参阅 https://github.com/dotnet/corefx/issues/41347#issuecomment-535779492)。
Allowing the payload to specify its own type information is a common source of vulnerabilities in web applications.
但是,是一种通过创建 JsonConverter<T>
添加您自己的多态反序列化支持的方法,所以从这个意义上说,这是可能的。
文档显示了如何使用 类型鉴别器 属性 执行此操作的示例:
https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to#support-polymorphic-deserialization
让我们看一个例子。
假设你有一个基础 class 和几个派生的 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; }
}
您可以创建以下 JsonConverter<BaseClass>
在序列化时写入类型鉴别器并读取它以确定要反序列化的类型。您可以在 JsonSerializerOptions
.
上注册该转换器
public class BaseClassConverter : JsonConverter<BaseClass>
{
private enum TypeDiscriminator
{
BaseClass = 0,
DerivedA = 1,
DerivedB = 2
}
public override bool CanConvert(Type type)
{
return typeof(BaseClass).IsAssignableFrom(type);
}
public override BaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!reader.Read()
|| reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "TypeDiscriminator")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
BaseClass baseClass;
TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
switch (typeDiscriminator)
{
case TypeDiscriminator.DerivedA:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));
break;
case TypeDiscriminator.DerivedB:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));
break;
default:
throw new NotSupportedException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
{
throw new JsonException();
}
return baseClass;
}
public override void Write(
Utf8JsonWriter writer,
BaseClass value,
JsonSerializerOptions options)
{
writer.WriteStartObject();
if (value is DerivedA derivedA)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedA);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedA);
}
else if (value is DerivedB derivedB)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedB);
}
else
{
throw new NotSupportedException();
}
writer.WriteEndObject();
}
}
这是序列化和反序列化的样子(包括与 Newtonsoft.Json 的比较):
private static void PolymorphicSupportComparison()
{
var objects = new List<BaseClass> { new DerivedA(), new DerivedB() };
// Using: System.Text.Json
var options = new JsonSerializerOptions
{
Converters = { new BaseClassConverter() },
WriteIndented = true
};
string jsonString = JsonSerializer.Serialize(objects, options);
Console.WriteLine(jsonString);
/*
[
{
"TypeDiscriminator": 1,
"TypeValue": {
"Str": null,
"Int": 0
}
},
{
"TypeDiscriminator": 2,
"TypeValue": {
"Bool": false,
"Int": 0
}
}
]
*/
var roundTrip = JsonSerializer.Deserialize<List<BaseClass>>(jsonString, options);
// Using: Newtonsoft.Json
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,
Formatting = Newtonsoft.Json.Formatting.Indented
};
jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(objects, settings);
Console.WriteLine(jsonString);
/*
[
{
"$type": "PolymorphicSerialization.DerivedA, PolymorphicSerialization",
"Str": null,
"Int": 0
},
{
"$type": "PolymorphicSerialization.DerivedB, PolymorphicSerialization",
"Bool": false,
"Int": 0
}
]
*/
var originalList = JsonConvert.DeserializeObject<List<BaseClass>>(jsonString, settings);
Debug.Assert(originalList[0].GetType() == roundTrip[0].GetType());
}
这是另一个 Whosebug 问题,展示了如何使用接口(而不是抽象 classes)支持多态反序列化,但类似的解决方案适用于任何多态:
我最终得到了那个解决方案。它很轻巧,对我来说足够通用。
类型鉴别器转换器
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);
}
不要这样写
public override bool CanConvert(Type type)
{
return typeof(BaseClass).IsAssignableFrom(type);
}
如果你 class 包含 baseClass 属性 那么你将他反序列化为 baseClass。
如果你的 baseClass 是抽象的并且包含 baseClass 属性 那么你会得到 Exception.
这样写比较稳妥:
public class BaseClass
{
public int Int { get; set; }
}
public class DerivedA : BaseClass
{
public string Str { get; set; }
public BaseClass derived { get; set; }
}
public class DerivedB : BaseClass
{
public bool Bool { get; set; }
public BaseClass derived { get; set; }
}
public class BaseClassConverter : JsonConverter<BaseClass>
{
private enum TypeDiscriminator
{
BaseClass = 0,
DerivedA = 1,
DerivedB = 2
}
public override bool CanConvert(Type type)
{
return typeof(BaseClass) == type;
}
public override BaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!reader.Read()
|| reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "TypeDiscriminator")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
BaseClass baseClass;
TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
switch (typeDiscriminator)
{
case TypeDiscriminator.DerivedA:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA), options);
break;
case TypeDiscriminator.DerivedB:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB), options);
break;
case TypeDiscriminator.BaseClass:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (BaseClass)JsonSerializer.Deserialize(ref reader, typeof(BaseClass));
break;
default:
throw new NotSupportedException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
{
throw new JsonException();
}
return baseClass;
}
public override void Write(
Utf8JsonWriter writer,
BaseClass value,
JsonSerializerOptions options)
{
writer.WriteStartObject();
if (value is DerivedA derivedA)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedA);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedA, options);
}
else if (value is DerivedB derivedB)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedB, options);
}
else if (value is BaseClass baseClass)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.BaseClass);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, baseClass);
}
else
{
throw new NotSupportedException();
}
writer.WriteEndObject();
}
}
但您的 BaseClass 不必包含 属性 类型为 BaseClass 或继承者。
这就是我的所有抽象类型的 JsonConverter:
private class AbstractClassConverter : JsonConverter<object>
{
public override object Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null) return null;
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException("JsonTokenType.StartObject not found.");
if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "$type")
throw new JsonException("Property $type not found.");
if (!reader.Read() || reader.TokenType != JsonTokenType.String)
throw new JsonException("Value at $type is invalid.");
string assemblyQualifiedName = reader.GetString();
var type = Type.GetType(assemblyQualifiedName);
using (var output = new MemoryStream())
{
ReadObject(ref reader, output, options);
return JsonSerializer.Deserialize(output.ToArray(), type, options);
}
}
private void ReadObject(ref Utf8JsonReader reader, Stream output, JsonSerializerOptions options)
{
using (var writer = new Utf8JsonWriter(output, new JsonWriterOptions
{
Encoder = options.Encoder,
Indented = options.WriteIndented
}))
{
writer.WriteStartObject();
var objectIntend = 0;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.None:
case JsonTokenType.Null:
writer.WriteNullValue();
break;
case JsonTokenType.StartObject:
writer.WriteStartObject();
objectIntend++;
break;
case JsonTokenType.EndObject:
writer.WriteEndObject();
if(objectIntend == 0)
{
writer.Flush();
return;
}
objectIntend--;
break;
case JsonTokenType.StartArray:
writer.WriteStartArray();
break;
case JsonTokenType.EndArray:
writer.WriteEndArray();
break;
case JsonTokenType.PropertyName:
writer.WritePropertyName(reader.GetString());
break;
case JsonTokenType.Comment:
writer.WriteCommentValue(reader.GetComment());
break;
case JsonTokenType.String:
writer.WriteStringValue(reader.GetString());
break;
case JsonTokenType.Number:
writer.WriteNumberValue(reader.GetInt32());
break;
case JsonTokenType.True:
case JsonTokenType.False:
writer.WriteBooleanValue(reader.GetBoolean());
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
writer.WriteStartObject();
var valueType = value.GetType();
var valueAssemblyName = valueType.Assembly.GetName();
writer.WriteString("$type", $"{valueType.FullName}, {valueAssemblyName.Name}");
var json = JsonSerializer.Serialize(value, value.GetType(), options);
using (var document = JsonDocument.Parse(json, new JsonDocumentOptions
{
AllowTrailingCommas = options.AllowTrailingCommas,
MaxDepth = options.MaxDepth
}))
{
foreach (var jsonProperty in document.RootElement.EnumerateObject())
jsonProperty.WriteTo(writer);
}
writer.WriteEndObject();
}
public override bool CanConvert(Type typeToConvert) =>
typeToConvert.IsAbstract && !EnumerableInterfaceType.IsAssignableFrom(typeToConvert);
}
我真的很喜欢 Demetrius 的回答,但我认为您可以在可重用性方面走得更远。我提出了以下解决方案:
JsonConverterFactory:
/// <summary>
/// Represents the <see cref="JsonConverterFactory"/> used to create <see cref="AbstractClassConverter{T}"/>
/// </summary>
public class AbstractClassConverterFactory
: JsonConverterFactory
{
/// <summary>
/// Gets a <see cref="Dictionary{TKey, TValue}"/> containing the mappings of types to their respective <see cref="JsonConverter"/>
/// </summary>
protected static Dictionary<Type, JsonConverter> Converters = new Dictionary<Type, JsonConverter>();
/// <summary>
/// Initializes a new <see cref="AbstractClassConverterFactory"/>
/// </summary>
/// <param name="namingPolicy">The current <see cref="JsonNamingPolicy"/></param>
public AbstractClassConverterFactory(JsonNamingPolicy namingPolicy)
{
this.NamingPolicy = namingPolicy;
}
/// <summary>
/// Gets the current <see cref="JsonNamingPolicy"/>
/// </summary>
protected JsonNamingPolicy NamingPolicy { get; }
/// <inheritdoc/>
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsClass && typeToConvert.IsAbstract && typeToConvert.IsDefined(typeof(DiscriminatorAttribute));
}
/// <inheritdoc/>
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
if(!Converters.TryGetValue(typeToConvert, out JsonConverter converter))
{
Type converterType = typeof(AbstractClassConverter<>).MakeGenericType(typeToConvert);
converter = (JsonConverter)Activator.CreateInstance(converterType, this.NamingPolicy);
Converters.Add(typeToConvert, converter);
}
return converter;
}
}
JsonConverter:
/// <summary>
/// Represents the <see cref="JsonConverter"/> used to convert to/from an abstract class
/// </summary>
/// <typeparam name="T">The type of the abstract class to convert to/from</typeparam>
public class AbstractClassConverter<T>
: JsonConverter<T>
{
/// <summary>
/// Initializes a new <see cref="AbstractClassConverter{T}"/>
/// </summary>
/// <param name="namingPolicy">The current <see cref="JsonNamingPolicy"/></param>
public AbstractClassConverter(JsonNamingPolicy namingPolicy)
{
this.NamingPolicy = namingPolicy;
DiscriminatorAttribute discriminatorAttribute = typeof(T).GetCustomAttribute<DiscriminatorAttribute>();
if (discriminatorAttribute == null)
throw new NullReferenceException($"Failed to find the required '{nameof(DiscriminatorAttribute)}'");
this.DiscriminatorProperty = typeof(T).GetProperty(discriminatorAttribute.Property, BindingFlags.Default | BindingFlags.Public | BindingFlags.Instance);
if (this.DiscriminatorProperty == null)
throw new NullReferenceException($"Failed to find the specified discriminator property '{discriminatorAttribute.Property}' in type '{typeof(T).Name}'");
this.TypeMappings = new Dictionary<string, Type>();
foreach (Type derivedType in TypeCacheUtil.FindFilteredTypes($"nposm:json-polymorph:{typeof(T).Name}",
(t) => t.IsClass && !t.IsAbstract && t.BaseType == typeof(T)))
{
DiscriminatorValueAttribute discriminatorValueAttribute = derivedType.GetCustomAttribute<DiscriminatorValueAttribute>();
if (discriminatorValueAttribute == null)
continue;
string discriminatorValue = null;
if (discriminatorValueAttribute.Value.GetType().IsEnum)
discriminatorValue = EnumHelper.Stringify(discriminatorValueAttribute.Value, this.DiscriminatorProperty.PropertyType);
else
discriminatorValue = discriminatorValueAttribute.Value.ToString();
this.TypeMappings.Add(discriminatorValue, derivedType);
}
}
/// <summary>
/// Gets the current <see cref="JsonNamingPolicy"/>
/// </summary>
protected JsonNamingPolicy NamingPolicy { get; }
/// <summary>
/// Gets the discriminator <see cref="PropertyInfo"/> of the abstract type to convert
/// </summary>
protected PropertyInfo DiscriminatorProperty { get; }
/// <summary>
/// Gets an <see cref="Dictionary{TKey, TValue}"/> containing the mappings of the converted type's derived types
/// </summary>
protected Dictionary<string, Type> TypeMappings { get; }
/// <inheritdoc/>
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException("Start object token type expected");
using (JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader))
{
string discriminatorPropertyName = this.NamingPolicy?.ConvertName(this.DiscriminatorProperty.Name);
if (!jsonDocument.RootElement.TryGetProperty(discriminatorPropertyName, out JsonElement discriminatorProperty))
throw new JsonException($"Failed to find the required '{this.DiscriminatorProperty.Name}' discriminator property");
string discriminatorValue = discriminatorProperty.GetString();
if (!this.TypeMappings.TryGetValue(discriminatorValue, out Type derivedType))
throw new JsonException($"Failed to find the derived type with the specified discriminator value '{discriminatorValue}'");
string json = jsonDocument.RootElement.GetRawText();
return (T)JsonSerializer.Deserialize(json, derivedType);
}
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, (object)value, options);
}
}
鉴别器属性:
/// <summary>
/// Represents the <see cref="Attribute"/> used to indicate the property used to discriminate derived types of the marked class
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class DiscriminatorAttribute
: Attribute
{
/// <summary>
/// Initializes a new <see cref="DiscriminatorAttribute"/>
/// </summary>
/// <param name="property">The name of the property used to discriminate derived types of the class marked by the <see cref="DiscriminatorAttribute"/></param>
public DiscriminatorAttribute(string property)
{
this.Property = property;
}
/// <summary>
/// Gets the name of the property used to discriminate derived types of the class marked by the <see cref="DiscriminatorAttribute"/>
/// </summary>
public string Property { get; }
}
鉴别器值属性:
/// <summary>
/// Represents the <see cref="Attribute"/> used to indicate the discriminator value of a derived type
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class DiscriminatorValueAttribute
: Attribute
{
/// <summary>
/// Initializes a new <see cref="DiscriminatorValueAttribute"/>
/// </summary>
/// <param name="value">The value used to discriminate the derived type marked by the <see cref="DiscriminatorValueAttribute"/></param>
public DiscriminatorValueAttribute(object value)
{
this.Value = value;
}
/// <summary>
/// Gets the value used to discriminate the derived type marked by the <see cref="DiscriminatorValueAttribute"/>
/// </summary>
public object Value { get; }
}
最后,一个如何在 类 上使用它的示例:
[Discriminator(nameof(Type))]
public abstract class Identity
{
public virtual IdentityType Type { get; protected set; }
}
[DiscriminatorValue(IdentityType.Person)]
public class Person
: Identity
{
}
然后...瞧!
剩下要做的就是注册工厂:
this.Services.AddControllersWithViews()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new AbstractClassConverterFactory(options.JsonSerializerOptions.PropertyNamingPolicy));
});
抛出这个选项:使用源代码生成器为带有 属性 标记特殊属性的对象自动生成 JsonConverter
你可以尝试使用这个包,但它需要 .net5
https://github.com/wivuu/Wivuu.JsonPolymorphism
生成器查看标有鉴别器属性的 属性 的类型,然后查找从持有鉴别器的类型继承的类型以匹配枚举的每个案例
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");
对于接口 属性 反序列化,我创建了一个简单的 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);
我根据 的回答更改了一些内容。
我个人喜欢这种方式,因为客户端可以直接将他们的对象提供给服务器。
但是,'Type' 属性 必须是对象中的第一个。
基础 class 和派生 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
}
您可以创建 JsonConverter<IBaseClass>
在序列化时读取和检查 'Type' 属性。它将使用它来确定要反序列化的类型。
reader 必须被复制,因为我们读取第一个 属性 作为类型。然后我们必须再次读取完整的对象(将其传递给 Deserialize 方法)。
public class BaseClassConverter : JsonConverter<IBaseClass>
{
public override IBaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
// Creating a copy of the reader (The derived deserialisation has to be done from the start)
Utf8JsonReader typeReader = reader;
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
IBaseClass baseClass = default;
DerivedType type= (DerivedType)reader.GetInt32();
switch (type)
{
case DerivedType.DerivedA:
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));
break;
case DerivedType.DerivedB:
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));
break;
default:
throw new NotSupportedException();
}
return baseClass;
}
public override void Write(
Utf8JsonWriter writer,
IBaseClass value,
JsonSerializerOptions options)
{
switch(value)
{
case DerivedA derivedA:
JsonSerializer.Serialize(writer, derivedA, options);
break;
case DerivedB derivedB:
JsonSerializer.Serialize(writer, derivedB, options);
break;
default:
throw new NotSupportedException();
}
}
}
客户端现在可以按如下方式发送对象:
// DerivedA
{
"Type": 0,
"Str": "Hello world!"
}
// DerivedB
{
"Type": 1,
"Bool": false
}
编辑:
编辑了 Read 方法,以便能够处理 属性 名称不在第一位的情况。现在它只是读取 json 并停止,直到找到 'Type' 属性 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..
....
老实说,我认为这个自定义 System.Text JsonConverter 的设置方式不必要地复杂,我更喜欢 Newtonsoft JsonConverter。
我想引入另一个适用于分层、安全、双向、通用用法的实现。
以下注意事项
- 这是一个性能和内存“噩梦”,但对于大多数场景来说已经足够好了(原因:因为您需要提前阅读
$type
然后需要返回 reader)。
- 它仅在多态基础是抽象的/从不序列化为实例本身时才有效(原因:因为否则常规转换器无法在派生的 类 上工作,因为它会进入堆栈溢出)。
- 在 .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 的一些荣誉。
基于已接受的答案,但使用 KnownTypeAttribute 来发现类型(通常枚举所有类型会导致不需要的类型加载异常),并在转换器中添加鉴别器 属性 而不是 class 自己实现:
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
{
//..
}
不是很优雅或高效,但可以快速编写少量子类型的代码:
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;
}
}
我尝试从 Newtonsoft.Json 迁移到 System.Text.Json。 我想反序列化摘要 class。 Newtonsoft.Json 对此有 TypeNameHandling。 有什么方法可以在 .net core 3.0 上通过 System.Text.Json 反序列化抽象 class?
请试试我写的这个库作为 System.Text.Json 的扩展以提供多态性: https://github.com/dahomey-technologies/Dahomey.Json
如果引用实例的实际类型与声明类型不同,鉴别器属性将自动添加到输出json:
public class WeatherForecast
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string Summary { get; set; }
}
public class WeatherForecastDerived : WeatherForecast
{
public int WindSpeed { get; set; }
}
Inherited 类 必须手动注册到鉴别器约定注册表,以便让框架了解鉴别器值和类型之间的映射:
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
}
Is polymorphic deserialization possible in System.Text.Json?
答案是而不是,取决于你的意思 "possible".
有没有多态反序列化(相当于Newtonsoft.Json的TypeNameHandling
)支持内置至 System.Text.Json
。这是因为读取 JSON 有效负载中指定为字符串的 .NET 类型名称(例如 $type
元数据 属性)来创建对象 不推荐 因为它引入了潜在的安全问题(有关详细信息,请参阅 https://github.com/dotnet/corefx/issues/41347#issuecomment-535779492)。
Allowing the payload to specify its own type information is a common source of vulnerabilities in web applications.
但是,是一种通过创建 JsonConverter<T>
添加您自己的多态反序列化支持的方法,所以从这个意义上说,这是可能的。
文档显示了如何使用 类型鉴别器 属性 执行此操作的示例: https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to#support-polymorphic-deserialization
让我们看一个例子。
假设你有一个基础 class 和几个派生的 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; }
}
您可以创建以下 JsonConverter<BaseClass>
在序列化时写入类型鉴别器并读取它以确定要反序列化的类型。您可以在 JsonSerializerOptions
.
public class BaseClassConverter : JsonConverter<BaseClass>
{
private enum TypeDiscriminator
{
BaseClass = 0,
DerivedA = 1,
DerivedB = 2
}
public override bool CanConvert(Type type)
{
return typeof(BaseClass).IsAssignableFrom(type);
}
public override BaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!reader.Read()
|| reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "TypeDiscriminator")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
BaseClass baseClass;
TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
switch (typeDiscriminator)
{
case TypeDiscriminator.DerivedA:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));
break;
case TypeDiscriminator.DerivedB:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));
break;
default:
throw new NotSupportedException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
{
throw new JsonException();
}
return baseClass;
}
public override void Write(
Utf8JsonWriter writer,
BaseClass value,
JsonSerializerOptions options)
{
writer.WriteStartObject();
if (value is DerivedA derivedA)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedA);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedA);
}
else if (value is DerivedB derivedB)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedB);
}
else
{
throw new NotSupportedException();
}
writer.WriteEndObject();
}
}
这是序列化和反序列化的样子(包括与 Newtonsoft.Json 的比较):
private static void PolymorphicSupportComparison()
{
var objects = new List<BaseClass> { new DerivedA(), new DerivedB() };
// Using: System.Text.Json
var options = new JsonSerializerOptions
{
Converters = { new BaseClassConverter() },
WriteIndented = true
};
string jsonString = JsonSerializer.Serialize(objects, options);
Console.WriteLine(jsonString);
/*
[
{
"TypeDiscriminator": 1,
"TypeValue": {
"Str": null,
"Int": 0
}
},
{
"TypeDiscriminator": 2,
"TypeValue": {
"Bool": false,
"Int": 0
}
}
]
*/
var roundTrip = JsonSerializer.Deserialize<List<BaseClass>>(jsonString, options);
// Using: Newtonsoft.Json
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,
Formatting = Newtonsoft.Json.Formatting.Indented
};
jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(objects, settings);
Console.WriteLine(jsonString);
/*
[
{
"$type": "PolymorphicSerialization.DerivedA, PolymorphicSerialization",
"Str": null,
"Int": 0
},
{
"$type": "PolymorphicSerialization.DerivedB, PolymorphicSerialization",
"Bool": false,
"Int": 0
}
]
*/
var originalList = JsonConvert.DeserializeObject<List<BaseClass>>(jsonString, settings);
Debug.Assert(originalList[0].GetType() == roundTrip[0].GetType());
}
这是另一个 Whosebug 问题,展示了如何使用接口(而不是抽象 classes)支持多态反序列化,但类似的解决方案适用于任何多态:
我最终得到了那个解决方案。它很轻巧,对我来说足够通用。
类型鉴别器转换器
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);
}
不要这样写
public override bool CanConvert(Type type)
{
return typeof(BaseClass).IsAssignableFrom(type);
}
如果你 class 包含 baseClass 属性 那么你将他反序列化为 baseClass。 如果你的 baseClass 是抽象的并且包含 baseClass 属性 那么你会得到 Exception.
这样写比较稳妥:
public class BaseClass
{
public int Int { get; set; }
}
public class DerivedA : BaseClass
{
public string Str { get; set; }
public BaseClass derived { get; set; }
}
public class DerivedB : BaseClass
{
public bool Bool { get; set; }
public BaseClass derived { get; set; }
}
public class BaseClassConverter : JsonConverter<BaseClass>
{
private enum TypeDiscriminator
{
BaseClass = 0,
DerivedA = 1,
DerivedB = 2
}
public override bool CanConvert(Type type)
{
return typeof(BaseClass) == type;
}
public override BaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!reader.Read()
|| reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "TypeDiscriminator")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
BaseClass baseClass;
TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
switch (typeDiscriminator)
{
case TypeDiscriminator.DerivedA:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA), options);
break;
case TypeDiscriminator.DerivedB:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB), options);
break;
case TypeDiscriminator.BaseClass:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (BaseClass)JsonSerializer.Deserialize(ref reader, typeof(BaseClass));
break;
default:
throw new NotSupportedException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
{
throw new JsonException();
}
return baseClass;
}
public override void Write(
Utf8JsonWriter writer,
BaseClass value,
JsonSerializerOptions options)
{
writer.WriteStartObject();
if (value is DerivedA derivedA)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedA);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedA, options);
}
else if (value is DerivedB derivedB)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedB, options);
}
else if (value is BaseClass baseClass)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.BaseClass);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, baseClass);
}
else
{
throw new NotSupportedException();
}
writer.WriteEndObject();
}
}
但您的 BaseClass 不必包含 属性 类型为 BaseClass 或继承者。
这就是我的所有抽象类型的 JsonConverter:
private class AbstractClassConverter : JsonConverter<object>
{
public override object Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null) return null;
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException("JsonTokenType.StartObject not found.");
if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "$type")
throw new JsonException("Property $type not found.");
if (!reader.Read() || reader.TokenType != JsonTokenType.String)
throw new JsonException("Value at $type is invalid.");
string assemblyQualifiedName = reader.GetString();
var type = Type.GetType(assemblyQualifiedName);
using (var output = new MemoryStream())
{
ReadObject(ref reader, output, options);
return JsonSerializer.Deserialize(output.ToArray(), type, options);
}
}
private void ReadObject(ref Utf8JsonReader reader, Stream output, JsonSerializerOptions options)
{
using (var writer = new Utf8JsonWriter(output, new JsonWriterOptions
{
Encoder = options.Encoder,
Indented = options.WriteIndented
}))
{
writer.WriteStartObject();
var objectIntend = 0;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.None:
case JsonTokenType.Null:
writer.WriteNullValue();
break;
case JsonTokenType.StartObject:
writer.WriteStartObject();
objectIntend++;
break;
case JsonTokenType.EndObject:
writer.WriteEndObject();
if(objectIntend == 0)
{
writer.Flush();
return;
}
objectIntend--;
break;
case JsonTokenType.StartArray:
writer.WriteStartArray();
break;
case JsonTokenType.EndArray:
writer.WriteEndArray();
break;
case JsonTokenType.PropertyName:
writer.WritePropertyName(reader.GetString());
break;
case JsonTokenType.Comment:
writer.WriteCommentValue(reader.GetComment());
break;
case JsonTokenType.String:
writer.WriteStringValue(reader.GetString());
break;
case JsonTokenType.Number:
writer.WriteNumberValue(reader.GetInt32());
break;
case JsonTokenType.True:
case JsonTokenType.False:
writer.WriteBooleanValue(reader.GetBoolean());
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
writer.WriteStartObject();
var valueType = value.GetType();
var valueAssemblyName = valueType.Assembly.GetName();
writer.WriteString("$type", $"{valueType.FullName}, {valueAssemblyName.Name}");
var json = JsonSerializer.Serialize(value, value.GetType(), options);
using (var document = JsonDocument.Parse(json, new JsonDocumentOptions
{
AllowTrailingCommas = options.AllowTrailingCommas,
MaxDepth = options.MaxDepth
}))
{
foreach (var jsonProperty in document.RootElement.EnumerateObject())
jsonProperty.WriteTo(writer);
}
writer.WriteEndObject();
}
public override bool CanConvert(Type typeToConvert) =>
typeToConvert.IsAbstract && !EnumerableInterfaceType.IsAssignableFrom(typeToConvert);
}
我真的很喜欢 Demetrius 的回答,但我认为您可以在可重用性方面走得更远。我提出了以下解决方案:
JsonConverterFactory:
/// <summary>
/// Represents the <see cref="JsonConverterFactory"/> used to create <see cref="AbstractClassConverter{T}"/>
/// </summary>
public class AbstractClassConverterFactory
: JsonConverterFactory
{
/// <summary>
/// Gets a <see cref="Dictionary{TKey, TValue}"/> containing the mappings of types to their respective <see cref="JsonConverter"/>
/// </summary>
protected static Dictionary<Type, JsonConverter> Converters = new Dictionary<Type, JsonConverter>();
/// <summary>
/// Initializes a new <see cref="AbstractClassConverterFactory"/>
/// </summary>
/// <param name="namingPolicy">The current <see cref="JsonNamingPolicy"/></param>
public AbstractClassConverterFactory(JsonNamingPolicy namingPolicy)
{
this.NamingPolicy = namingPolicy;
}
/// <summary>
/// Gets the current <see cref="JsonNamingPolicy"/>
/// </summary>
protected JsonNamingPolicy NamingPolicy { get; }
/// <inheritdoc/>
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsClass && typeToConvert.IsAbstract && typeToConvert.IsDefined(typeof(DiscriminatorAttribute));
}
/// <inheritdoc/>
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
if(!Converters.TryGetValue(typeToConvert, out JsonConverter converter))
{
Type converterType = typeof(AbstractClassConverter<>).MakeGenericType(typeToConvert);
converter = (JsonConverter)Activator.CreateInstance(converterType, this.NamingPolicy);
Converters.Add(typeToConvert, converter);
}
return converter;
}
}
JsonConverter:
/// <summary>
/// Represents the <see cref="JsonConverter"/> used to convert to/from an abstract class
/// </summary>
/// <typeparam name="T">The type of the abstract class to convert to/from</typeparam>
public class AbstractClassConverter<T>
: JsonConverter<T>
{
/// <summary>
/// Initializes a new <see cref="AbstractClassConverter{T}"/>
/// </summary>
/// <param name="namingPolicy">The current <see cref="JsonNamingPolicy"/></param>
public AbstractClassConverter(JsonNamingPolicy namingPolicy)
{
this.NamingPolicy = namingPolicy;
DiscriminatorAttribute discriminatorAttribute = typeof(T).GetCustomAttribute<DiscriminatorAttribute>();
if (discriminatorAttribute == null)
throw new NullReferenceException($"Failed to find the required '{nameof(DiscriminatorAttribute)}'");
this.DiscriminatorProperty = typeof(T).GetProperty(discriminatorAttribute.Property, BindingFlags.Default | BindingFlags.Public | BindingFlags.Instance);
if (this.DiscriminatorProperty == null)
throw new NullReferenceException($"Failed to find the specified discriminator property '{discriminatorAttribute.Property}' in type '{typeof(T).Name}'");
this.TypeMappings = new Dictionary<string, Type>();
foreach (Type derivedType in TypeCacheUtil.FindFilteredTypes($"nposm:json-polymorph:{typeof(T).Name}",
(t) => t.IsClass && !t.IsAbstract && t.BaseType == typeof(T)))
{
DiscriminatorValueAttribute discriminatorValueAttribute = derivedType.GetCustomAttribute<DiscriminatorValueAttribute>();
if (discriminatorValueAttribute == null)
continue;
string discriminatorValue = null;
if (discriminatorValueAttribute.Value.GetType().IsEnum)
discriminatorValue = EnumHelper.Stringify(discriminatorValueAttribute.Value, this.DiscriminatorProperty.PropertyType);
else
discriminatorValue = discriminatorValueAttribute.Value.ToString();
this.TypeMappings.Add(discriminatorValue, derivedType);
}
}
/// <summary>
/// Gets the current <see cref="JsonNamingPolicy"/>
/// </summary>
protected JsonNamingPolicy NamingPolicy { get; }
/// <summary>
/// Gets the discriminator <see cref="PropertyInfo"/> of the abstract type to convert
/// </summary>
protected PropertyInfo DiscriminatorProperty { get; }
/// <summary>
/// Gets an <see cref="Dictionary{TKey, TValue}"/> containing the mappings of the converted type's derived types
/// </summary>
protected Dictionary<string, Type> TypeMappings { get; }
/// <inheritdoc/>
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException("Start object token type expected");
using (JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader))
{
string discriminatorPropertyName = this.NamingPolicy?.ConvertName(this.DiscriminatorProperty.Name);
if (!jsonDocument.RootElement.TryGetProperty(discriminatorPropertyName, out JsonElement discriminatorProperty))
throw new JsonException($"Failed to find the required '{this.DiscriminatorProperty.Name}' discriminator property");
string discriminatorValue = discriminatorProperty.GetString();
if (!this.TypeMappings.TryGetValue(discriminatorValue, out Type derivedType))
throw new JsonException($"Failed to find the derived type with the specified discriminator value '{discriminatorValue}'");
string json = jsonDocument.RootElement.GetRawText();
return (T)JsonSerializer.Deserialize(json, derivedType);
}
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, (object)value, options);
}
}
鉴别器属性:
/// <summary>
/// Represents the <see cref="Attribute"/> used to indicate the property used to discriminate derived types of the marked class
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class DiscriminatorAttribute
: Attribute
{
/// <summary>
/// Initializes a new <see cref="DiscriminatorAttribute"/>
/// </summary>
/// <param name="property">The name of the property used to discriminate derived types of the class marked by the <see cref="DiscriminatorAttribute"/></param>
public DiscriminatorAttribute(string property)
{
this.Property = property;
}
/// <summary>
/// Gets the name of the property used to discriminate derived types of the class marked by the <see cref="DiscriminatorAttribute"/>
/// </summary>
public string Property { get; }
}
鉴别器值属性:
/// <summary>
/// Represents the <see cref="Attribute"/> used to indicate the discriminator value of a derived type
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class DiscriminatorValueAttribute
: Attribute
{
/// <summary>
/// Initializes a new <see cref="DiscriminatorValueAttribute"/>
/// </summary>
/// <param name="value">The value used to discriminate the derived type marked by the <see cref="DiscriminatorValueAttribute"/></param>
public DiscriminatorValueAttribute(object value)
{
this.Value = value;
}
/// <summary>
/// Gets the value used to discriminate the derived type marked by the <see cref="DiscriminatorValueAttribute"/>
/// </summary>
public object Value { get; }
}
最后,一个如何在 类 上使用它的示例:
[Discriminator(nameof(Type))]
public abstract class Identity
{
public virtual IdentityType Type { get; protected set; }
}
[DiscriminatorValue(IdentityType.Person)]
public class Person
: Identity
{
}
然后...瞧!
剩下要做的就是注册工厂:
this.Services.AddControllersWithViews()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new AbstractClassConverterFactory(options.JsonSerializerOptions.PropertyNamingPolicy));
});
抛出这个选项:使用源代码生成器为带有 属性 标记特殊属性的对象自动生成 JsonConverter
你可以尝试使用这个包,但它需要 .net5
https://github.com/wivuu/Wivuu.JsonPolymorphism
生成器查看标有鉴别器属性的 属性 的类型,然后查找从持有鉴别器的类型继承的类型以匹配枚举的每个案例
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");
对于接口 属性 反序列化,我创建了一个简单的 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);
我根据
我个人喜欢这种方式,因为客户端可以直接将他们的对象提供给服务器。 但是,'Type' 属性 必须是对象中的第一个。
基础 class 和派生 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
}
您可以创建 JsonConverter<IBaseClass>
在序列化时读取和检查 'Type' 属性。它将使用它来确定要反序列化的类型。
reader 必须被复制,因为我们读取第一个 属性 作为类型。然后我们必须再次读取完整的对象(将其传递给 Deserialize 方法)。
public class BaseClassConverter : JsonConverter<IBaseClass>
{
public override IBaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
// Creating a copy of the reader (The derived deserialisation has to be done from the start)
Utf8JsonReader typeReader = reader;
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
IBaseClass baseClass = default;
DerivedType type= (DerivedType)reader.GetInt32();
switch (type)
{
case DerivedType.DerivedA:
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));
break;
case DerivedType.DerivedB:
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));
break;
default:
throw new NotSupportedException();
}
return baseClass;
}
public override void Write(
Utf8JsonWriter writer,
IBaseClass value,
JsonSerializerOptions options)
{
switch(value)
{
case DerivedA derivedA:
JsonSerializer.Serialize(writer, derivedA, options);
break;
case DerivedB derivedB:
JsonSerializer.Serialize(writer, derivedB, options);
break;
default:
throw new NotSupportedException();
}
}
}
客户端现在可以按如下方式发送对象:
// DerivedA
{
"Type": 0,
"Str": "Hello world!"
}
// DerivedB
{
"Type": 1,
"Bool": false
}
编辑:
编辑了 Read 方法,以便能够处理 属性 名称不在第一位的情况。现在它只是读取 json 并停止,直到找到 'Type' 属性 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..
....
老实说,我认为这个自定义 System.Text JsonConverter 的设置方式不必要地复杂,我更喜欢 Newtonsoft JsonConverter。
我想引入另一个适用于分层、安全、双向、通用用法的实现。
以下注意事项
- 这是一个性能和内存“噩梦”,但对于大多数场景来说已经足够好了(原因:因为您需要提前阅读
$type
然后需要返回 reader)。 - 它仅在多态基础是抽象的/从不序列化为实例本身时才有效(原因:因为否则常规转换器无法在派生的 类 上工作,因为它会进入堆栈溢出)。
- 在 .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 的一些荣誉。
基于已接受的答案,但使用 KnownTypeAttribute 来发现类型(通常枚举所有类型会导致不需要的类型加载异常),并在转换器中添加鉴别器 属性 而不是 class 自己实现:
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
{
//..
}
不是很优雅或高效,但可以快速编写少量子类型的代码:
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;
}
}