System.Text.Json 中的自定义转换器中是否有手动 serialize/deserialize 子对象的简单方法?
Is there a simple way to manually serialize/deserialize child objects in a custom converter in System.Text.Json?
NOTE: I am using Microsoft's new System.Text.Json
and not Json.NET
so make sure answers address this accordingly.
考虑这些简单的 POCO:
interface Vehicle {}
class Car : Vehicle {
string make { get; set; }
int numberOfDoors { get; set; }
}
class Bicycle : Vehicle {
int frontGears { get; set; }
int backGears { get; set; }
}
汽车可以这样表示JSON...
{
"make": "Smart",
"numberOfDoors": 2
}
自行车可以这样表示...
{
"frontGears": 3,
"backGears": 6
}
非常简单。现在考虑这个 JSON.
[
{
"Car": {
"make": "Smart",
"numberOfDoors": 2
}
},
{
"Car": {
"make": "Lexus",
"numberOfDoors": 4
}
},
{
"Bicycle" : {
"frontGears": 3,
"backGears": 6
}
}
]
这是一个对象数组,其中 属性 名称是了解相应嵌套对象所指类型的关键。
虽然我知道如何编写使用 UTF8JsonReader
读取 属性 名称(例如 'Car' 和 'Bicycle' 并且可以编写 switch 语句的自定义转换器因此,我不知道如何退回到默认的 Car
和 Bicycle
转换器(即标准 JSON 转换器) 因为我在 reader 上没有看到任何方法来读取特定类型的对象。
那么如何手动反序列化这样的嵌套对象?
我明白了。您只需将 reader/writer 传递给 JsonSerializer 的另一个实例,它就会像处理本机对象一样处理它。
这是一个完整的示例,您可以将其粘贴到 RoslynPad 之类的东西中 运行。
这是实现...
using System;
using System.Collections.ObjectModel;
using System.Text.Json;
using System.Text.Json.Serialization;
public class HeterogenousListConverter<TItem, TList> : JsonConverter<TList>
where TItem : notnull
where TList : IList<TItem>, new() {
public HeterogenousListConverter(params (string key, Type type)[] mappings){
foreach(var (key, type) in mappings)
KeyTypeLookup.Add(key, type);
}
public ReversibleLookup<string, Type> KeyTypeLookup = new ReversibleLookup<string, Type>();
public override bool CanConvert(Type typeToConvert)
=> typeof(TList).IsAssignableFrom(typeToConvert);
public override TList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options){
// Helper function for validating where you are in the JSON
void validateToken(Utf8JsonReader reader, JsonTokenType tokenType){
if(reader.TokenType != tokenType)
throw new JsonException($"Invalid token: Was expecting a '{tokenType}' token but received a '{reader.TokenType}' token");
}
validateToken(reader, JsonTokenType.StartArray);
var results = new TList();
reader.Read(); // Advance to the first object after the StartArray token. This should be either a StartObject token, or the EndArray token. Anything else is invalid.
while(reader.TokenType == JsonTokenType.StartObject){ // Start of 'wrapper' object
reader.Read(); // Move to property name
validateToken(reader, JsonTokenType.PropertyName);
var typeKey = reader.GetString();
reader.Read(); // Move to start of object (stored in this property)
validateToken(reader, JsonTokenType.StartObject); // Start of vehicle
if(KeyTypeLookup.TryGetValue(typeKey, out var concreteItemType)){
var item = (TItem)JsonSerializer.Deserialize(ref reader, concreteItemType, options);
results.Add(item);
}
else{
throw new JsonException($"Unknown type key '{typeKey}' found");
}
reader.Read(); // Move past end of item object
reader.Read(); // Move past end of 'wrapper' object
}
validateToken(reader, JsonTokenType.EndArray);
return results;
}
public override void Write(Utf8JsonWriter writer, TList items, JsonSerializerOptions options){
writer.WriteStartArray();
foreach (var item in items){
var itemType = item.GetType();
writer.WriteStartObject();
if(KeyTypeLookup.ReverseLookup.TryGetValue(itemType, out var typeKey)){
writer.WritePropertyName(typeKey);
JsonSerializer.Serialize(writer, item, itemType, options);
}
else{
throw new JsonException($"Unknown type '{itemType.FullName}' found");
}
writer.WriteEndObject();
}
writer.WriteEndArray();
}
}
这是演示代码...
#nullable disable
public interface IVehicle { }
public class Car : IVehicle {
public string make { get; set; } = null;
public int numberOfDoors { get; set; } = 0;
public override string ToString()
=> $"{make} with {numberOfDoors} doors";
}
public class Bicycle : IVehicle{
public int frontGears { get; set; } = 0;
public int backGears { get; set; } = 0;
public override string ToString()
=> $"{nameof(Bicycle)} with {frontGears * backGears} gears";
}
string json = @"[
{
""Car"": {
""make"": ""Smart"",
""numberOfDoors"": 2
}
},
{
""Car"": {
""make"": ""Lexus"",
""numberOfDoors"": 4
}
},
{
""Bicycle"": {
""frontGears"": 3,
""backGears"": 6
}
}
]";
var converter = new HeterogenousListConverter<IVehicle, ObservableCollection<IVehicle>>(
(nameof(Car), typeof(Car)),
(nameof(Bicycle), typeof(Bicycle))
);
var options = new JsonSerializerOptions();
options.Converters.Add(converter);
var vehicles = JsonSerializer.Deserialize<ObservableCollection<IVehicle>>(json, options);
Console.Write($"{vehicles.Count} Vehicles: {String.Join(", ", vehicles.Select(v => v.ToString())) }");
var json2 = JsonSerializer.Serialize(vehicles, options);
Console.WriteLine(json2);
Console.WriteLine($"Completed at {DateTime.Now}");
这是上面使用的支持双向查找...
using System.Collections.ObjectModel;
using System.Diagnostics;
public class ReversibleLookup<T1, T2> : ReadOnlyDictionary<T1, T2>
where T1 : notnull
where T2 : notnull {
public ReversibleLookup(params (T1, T2)[] mappings)
: base(new Dictionary<T1, T2>()){
ReverseLookup = new ReadOnlyDictionary<T2, T1>(reverseLookup);
foreach(var mapping in mappings)
Add(mapping.Item1, mapping.Item2);
}
private readonly Dictionary<T2, T1> reverseLookup = new Dictionary<T2, T1>();
public ReadOnlyDictionary<T2, T1> ReverseLookup { get; }
[DebuggerHidden]
public void Add(T1 value1, T2 value2) {
if(ContainsKey(value1))
throw new InvalidOperationException($"{nameof(value1)} is not unique");
if(ReverseLookup.ContainsKey(value2))
throw new InvalidOperationException($"{nameof(value2)} is not unique");
Dictionary.Add(value1, value2);
reverseLookup.Add(value2, value1);
}
public void Clear(){
Dictionary.Clear();
reverseLookup.Clear();
}
}
这是一个简单的方法,希望对您有用。
您可以使用 dynamic variable
我在评论中注意到您不喜欢使用 NetwonSoft.Json,您可以使用此代码:dynamic car = Json.Decode(json);
Jsonclass来自here
这是一个适用于单个对象的解决方案(不需要对象数组)。这是 的副本,修改后可以在没有 IList 的情况下工作。
这是主要的class
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Shared.DataAccess
{
/// <summary>
/// Enables System.Text.Json to handle polymorphic classes
/// The polymorphic classes must be explicitly mapped
/// </summary>
/// <example>
/// Mapping
/// TradeStrategy (base) to
/// TradeStrategyNone and TradeStrategyRandom (derived)
///
/// var converter = new JsonPolymorphicConverter<TradeStrategy>(
/// (nameof(TradeStrategyNone), typeof(TradeStrategyNone)),
/// (nameof(TradeStrategyRandom), typeof(TradeStrategyRandom)));
/// var options = new JsonSerializerOptions();
/// var options.Converters.Add(converter);
/// </example>
/// <typeparam name="TItem">Base class type</typeparam>
public class JsonPolymorphicConverter<TItem> : JsonConverter<TItem>
where TItem : notnull
{
public JsonPolymorphicConverter(params (string key, Type type)[] mappings)
{
foreach (var (key, type) in mappings)
KeyTypeLookup.Add(key, type);
}
public ReversibleLookup<string, Type> KeyTypeLookup = new ReversibleLookup<string, Type>();
public override bool CanConvert(Type typeToConvert)
=> typeof(TItem).IsAssignableFrom(typeToConvert);
public override TItem Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Helper function for validating where you are in the JSON
void validateToken(Utf8JsonReader reader, JsonTokenType tokenType)
{
if (reader.TokenType != tokenType)
throw new JsonException($"Invalid token: Was expecting a '{tokenType}' token but received a '{reader.TokenType}' token");
}
TItem result = default(TItem);
reader.Read(); // Move to property name
validateToken(reader, JsonTokenType.PropertyName);
var typeKey = reader.GetString();
reader.Read(); // Move to start of object (stored in this property)
validateToken(reader, JsonTokenType.StartObject); // Start of vehicle
if (KeyTypeLookup.TryGetValue(typeKey, out var concreteItemType))
{
// WORKAROUND - stop cyclic look up
// If we leave our converter in the options then will get infinite cycling
// We create a temp options with our converter removed to stop the cycle
JsonSerializerOptions tempOptions = new JsonSerializerOptions(options);
tempOptions.Converters.Remove(this);
// Use normal deserialization
result = (TItem)JsonSerializer.Deserialize(ref reader, concreteItemType, tempOptions);
}
else
{
throw new JsonException($"Unknown type key '{typeKey}' found");
}
reader.Read(); // Move past end of item object
return result;
}
public override void Write(Utf8JsonWriter writer, TItem item, JsonSerializerOptions options)
{
var itemType = item.GetType();
writer.WriteStartObject();
if (KeyTypeLookup.ReverseLookup.TryGetValue(itemType, out var typeKey))
{
writer.WritePropertyName(typeKey);
// WORKAROUND - stop cyclic look up
// If we leave our converter in the options then will get infinite cycling
// We create a temp options with our converter removed to stop the cycle
JsonSerializerOptions tempOptions = new JsonSerializerOptions(options);
tempOptions.Converters.Remove(this);
// Use normal serialization
JsonSerializer.Serialize(writer, item, itemType, tempOptions);
}
else
{
throw new JsonException($"Unknown type '{itemType.FullName}' found");
}
writer.WriteEndObject();
}
}
}
这也依赖于 的 ReversibleLookup class。为了方便,我在这里复制。代码是一样的,我只是在顶部添加了注释。
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
namespace Shared.DataAccess
{
/// <summary>
/// Helper class used with JsonPolymorphicConverter and HeterogenousListConverter
/// </summary>
/// <typeparam name="T1">First class type</typeparam>
/// <typeparam name="T2">Second class type</typeparam>
public class ReversibleLookup<T1, T2> : ReadOnlyDictionary<T1, T2>
where T1 : notnull
where T2 : notnull
{
public ReversibleLookup(params (T1, T2)[] mappings)
: base(new Dictionary<T1, T2>())
{
ReverseLookup = new ReadOnlyDictionary<T2, T1>(reverseLookup);
foreach (var mapping in mappings)
Add(mapping.Item1, mapping.Item2);
}
private readonly Dictionary<T2, T1> reverseLookup = new Dictionary<T2, T1>();
public ReadOnlyDictionary<T2, T1> ReverseLookup { get; }
[DebuggerHidden]
public void Add(T1 value1, T2 value2)
{
if (ContainsKey(value1))
throw new InvalidOperationException($"{nameof(value1)} is not unique");
if (ReverseLookup.ContainsKey(value2))
throw new InvalidOperationException($"{nameof(value2)} is not unique");
Dictionary.Add(value1, value2);
reverseLookup.Add(value2, value1);
}
public void Clear()
{
Dictionary.Clear();
reverseLookup.Clear();
}
}
}
用法示例
public class TradeStrategy {
public string name;
public TradeStrategy() : this("Unknown") { }
public TradeStrategy(string name) { this.name = name; }
public virtual double CalcAdjustments(double stockPrice) => 0.0;
}
public class TradeStrategyNone : TradeStrategy {
public TradeStrategyNone() : base("None") { }
public override double CalcAdjustments(double stockPrice) => 0.0;
}
public class TradeStrategyRandom : TradeStrategy {
private Random random { get; set; }
public TradeStrategyRandom() : base("Random") { random = new Random(); }
public override double CalcAdjustments(double stockPrice) => random.NextDouble();
}
public class Portfolio {
public TradeStrategy strategy;
}
var converter = new JsonPolymorphicConverter<TradeStrategy>(
(nameof(TradeStrategyNone), typeof(TradeStrategyNone)),
(nameof(TradeStrategyRandom), typeof(TradeStrategyRandom)));
var options = new JsonSerializerOptions();
options.Converters.Add(converter);
Portfolio port1 = new Portfolio();
port1.strategy = new TradeStrategyRandom();
// port1Json will contain "TradeStrategyRandom" type info for "TradeStrategy" strategy variable
var port1Json = JsonSerializer.Serialize(port1, options);
// port1Copy will properly create "TradeStrategyRandom" from the port1Json
Portfolio port1Copy = JsonSerializer.Deserialize<Portfolio>(port1Json, options);
// Without "options" the JSON will end up stripping down TradeStrategyRandom to TradeStrategy
如果您想弄清楚此解决方案与另一个解决方案之间的区别,请知道另一个解决方案要求您创建要转换的项目的数组。此解决方案适用于单个对象。
这是另一个基于之前解决方案的解决方案(JSON 结构略有不同)。
显着差异:
- 鉴别器是对象的一部分(不需要使用包装器对象)
- 令我惊讶的是,没有必要通过递归(反)序列化调用删除转换器 (.NET 6)
- 我没有添加自定义查找,请参阅以前的答案
代码:
var foo = new[] {
new Foo
{
Inner = new Bar
{
Value = 42,
},
},
new Foo
{
Inner = new Baz
{
Value = "Hello",
},
},
};
var opts = new JsonSerializerOptions
{
Converters =
{
new PolymorphicJsonConverterWithDiscriminator<Base>(typeof(Bar), typeof(Baz)),
},
};
var json = JsonSerializer.Serialize(foo, opts);
var foo2 = JsonSerializer.Deserialize<Foo[]>(json, opts);
Console.WriteLine(foo2 is not null && foo2.SequenceEqual(foo));
Console.ReadLine();
public static class Constants
{
public const string DiscriminatorPropertyName = "$type";
}
public record Foo
{
public Base? Inner { get; set; }
}
public abstract record Base();
public record Bar : Base
{
[JsonPropertyName(DiscriminatorPropertyName)]
[JsonPropertyOrder(int.MinValue)]
public string TypeDiscriminator { get => nameof(Bar); init { if (value != nameof(Bar)) throw new ArgumentException(); } }
public int Value { get; set; }
}
public record Baz : Base
{
[JsonPropertyName(DiscriminatorPropertyName)]
[JsonPropertyOrder(int.MinValue)]
public string TypeDiscriminator { get => nameof(Baz); init { if (value != nameof(Baz)) throw new ArgumentException(); } }
public string? Value { get; set; }
}
public class PolymorphicJsonConverterWithDiscriminator<TBase> : JsonConverter<TBase>
where TBase : class
{
private readonly Type[] supportedTypes;
public PolymorphicJsonConverterWithDiscriminator(params Type[] supportedTypes)
{
this.supportedTypes = supportedTypes;
}
public override TBase? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Clone the reader so we can pass the original to Deserialize.
var readerClone = reader;
if (readerClone.TokenType == JsonTokenType.Null)
{
return null;
}
if (readerClone.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
readerClone.Read();
if (readerClone.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
var propertyName = readerClone.GetString();
if (propertyName != DiscriminatorPropertyName)
{
throw new JsonException();
}
readerClone.Read();
if (readerClone.TokenType != JsonTokenType.String)
{
throw new JsonException();
}
var typeIdentifier = readerClone.GetString();
var specificType = supportedTypes.FirstOrDefault(t => t.Name == typeIdentifier)
?? throw new JsonException();
return (TBase?)JsonSerializer.Deserialize(ref reader, specificType, options);
}
public override void Write(Utf8JsonWriter writer, TBase value, JsonSerializerOptions options)
{
// Cast to object which forces the serializer to use runtime type.
JsonSerializer.Serialize(writer, value, typeof(object), options);
}
}
样本JSON:
[
{
"Inner": {
"$type": "Bar",
"Value": 42
}
},
{
"Inner": {
"$type": "Baz",
"Value": "Hello"
}
}
]
NOTE: I am using Microsoft's new
System.Text.Json
and notJson.NET
so make sure answers address this accordingly.
考虑这些简单的 POCO:
interface Vehicle {}
class Car : Vehicle {
string make { get; set; }
int numberOfDoors { get; set; }
}
class Bicycle : Vehicle {
int frontGears { get; set; }
int backGears { get; set; }
}
汽车可以这样表示JSON...
{
"make": "Smart",
"numberOfDoors": 2
}
自行车可以这样表示...
{
"frontGears": 3,
"backGears": 6
}
非常简单。现在考虑这个 JSON.
[
{
"Car": {
"make": "Smart",
"numberOfDoors": 2
}
},
{
"Car": {
"make": "Lexus",
"numberOfDoors": 4
}
},
{
"Bicycle" : {
"frontGears": 3,
"backGears": 6
}
}
]
这是一个对象数组,其中 属性 名称是了解相应嵌套对象所指类型的关键。
虽然我知道如何编写使用 UTF8JsonReader
读取 属性 名称(例如 'Car' 和 'Bicycle' 并且可以编写 switch 语句的自定义转换器因此,我不知道如何退回到默认的 Car
和 Bicycle
转换器(即标准 JSON 转换器) 因为我在 reader 上没有看到任何方法来读取特定类型的对象。
那么如何手动反序列化这样的嵌套对象?
我明白了。您只需将 reader/writer 传递给 JsonSerializer 的另一个实例,它就会像处理本机对象一样处理它。
这是一个完整的示例,您可以将其粘贴到 RoslynPad 之类的东西中 运行。
这是实现...
using System;
using System.Collections.ObjectModel;
using System.Text.Json;
using System.Text.Json.Serialization;
public class HeterogenousListConverter<TItem, TList> : JsonConverter<TList>
where TItem : notnull
where TList : IList<TItem>, new() {
public HeterogenousListConverter(params (string key, Type type)[] mappings){
foreach(var (key, type) in mappings)
KeyTypeLookup.Add(key, type);
}
public ReversibleLookup<string, Type> KeyTypeLookup = new ReversibleLookup<string, Type>();
public override bool CanConvert(Type typeToConvert)
=> typeof(TList).IsAssignableFrom(typeToConvert);
public override TList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options){
// Helper function for validating where you are in the JSON
void validateToken(Utf8JsonReader reader, JsonTokenType tokenType){
if(reader.TokenType != tokenType)
throw new JsonException($"Invalid token: Was expecting a '{tokenType}' token but received a '{reader.TokenType}' token");
}
validateToken(reader, JsonTokenType.StartArray);
var results = new TList();
reader.Read(); // Advance to the first object after the StartArray token. This should be either a StartObject token, or the EndArray token. Anything else is invalid.
while(reader.TokenType == JsonTokenType.StartObject){ // Start of 'wrapper' object
reader.Read(); // Move to property name
validateToken(reader, JsonTokenType.PropertyName);
var typeKey = reader.GetString();
reader.Read(); // Move to start of object (stored in this property)
validateToken(reader, JsonTokenType.StartObject); // Start of vehicle
if(KeyTypeLookup.TryGetValue(typeKey, out var concreteItemType)){
var item = (TItem)JsonSerializer.Deserialize(ref reader, concreteItemType, options);
results.Add(item);
}
else{
throw new JsonException($"Unknown type key '{typeKey}' found");
}
reader.Read(); // Move past end of item object
reader.Read(); // Move past end of 'wrapper' object
}
validateToken(reader, JsonTokenType.EndArray);
return results;
}
public override void Write(Utf8JsonWriter writer, TList items, JsonSerializerOptions options){
writer.WriteStartArray();
foreach (var item in items){
var itemType = item.GetType();
writer.WriteStartObject();
if(KeyTypeLookup.ReverseLookup.TryGetValue(itemType, out var typeKey)){
writer.WritePropertyName(typeKey);
JsonSerializer.Serialize(writer, item, itemType, options);
}
else{
throw new JsonException($"Unknown type '{itemType.FullName}' found");
}
writer.WriteEndObject();
}
writer.WriteEndArray();
}
}
这是演示代码...
#nullable disable
public interface IVehicle { }
public class Car : IVehicle {
public string make { get; set; } = null;
public int numberOfDoors { get; set; } = 0;
public override string ToString()
=> $"{make} with {numberOfDoors} doors";
}
public class Bicycle : IVehicle{
public int frontGears { get; set; } = 0;
public int backGears { get; set; } = 0;
public override string ToString()
=> $"{nameof(Bicycle)} with {frontGears * backGears} gears";
}
string json = @"[
{
""Car"": {
""make"": ""Smart"",
""numberOfDoors"": 2
}
},
{
""Car"": {
""make"": ""Lexus"",
""numberOfDoors"": 4
}
},
{
""Bicycle"": {
""frontGears"": 3,
""backGears"": 6
}
}
]";
var converter = new HeterogenousListConverter<IVehicle, ObservableCollection<IVehicle>>(
(nameof(Car), typeof(Car)),
(nameof(Bicycle), typeof(Bicycle))
);
var options = new JsonSerializerOptions();
options.Converters.Add(converter);
var vehicles = JsonSerializer.Deserialize<ObservableCollection<IVehicle>>(json, options);
Console.Write($"{vehicles.Count} Vehicles: {String.Join(", ", vehicles.Select(v => v.ToString())) }");
var json2 = JsonSerializer.Serialize(vehicles, options);
Console.WriteLine(json2);
Console.WriteLine($"Completed at {DateTime.Now}");
这是上面使用的支持双向查找...
using System.Collections.ObjectModel;
using System.Diagnostics;
public class ReversibleLookup<T1, T2> : ReadOnlyDictionary<T1, T2>
where T1 : notnull
where T2 : notnull {
public ReversibleLookup(params (T1, T2)[] mappings)
: base(new Dictionary<T1, T2>()){
ReverseLookup = new ReadOnlyDictionary<T2, T1>(reverseLookup);
foreach(var mapping in mappings)
Add(mapping.Item1, mapping.Item2);
}
private readonly Dictionary<T2, T1> reverseLookup = new Dictionary<T2, T1>();
public ReadOnlyDictionary<T2, T1> ReverseLookup { get; }
[DebuggerHidden]
public void Add(T1 value1, T2 value2) {
if(ContainsKey(value1))
throw new InvalidOperationException($"{nameof(value1)} is not unique");
if(ReverseLookup.ContainsKey(value2))
throw new InvalidOperationException($"{nameof(value2)} is not unique");
Dictionary.Add(value1, value2);
reverseLookup.Add(value2, value1);
}
public void Clear(){
Dictionary.Clear();
reverseLookup.Clear();
}
}
这是一个简单的方法,希望对您有用。
您可以使用 dynamic variable
我在评论中注意到您不喜欢使用 NetwonSoft.Json,您可以使用此代码:dynamic car = Json.Decode(json);
Jsonclass来自here
这是一个适用于单个对象的解决方案(不需要对象数组)。这是
这是主要的class
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Shared.DataAccess
{
/// <summary>
/// Enables System.Text.Json to handle polymorphic classes
/// The polymorphic classes must be explicitly mapped
/// </summary>
/// <example>
/// Mapping
/// TradeStrategy (base) to
/// TradeStrategyNone and TradeStrategyRandom (derived)
///
/// var converter = new JsonPolymorphicConverter<TradeStrategy>(
/// (nameof(TradeStrategyNone), typeof(TradeStrategyNone)),
/// (nameof(TradeStrategyRandom), typeof(TradeStrategyRandom)));
/// var options = new JsonSerializerOptions();
/// var options.Converters.Add(converter);
/// </example>
/// <typeparam name="TItem">Base class type</typeparam>
public class JsonPolymorphicConverter<TItem> : JsonConverter<TItem>
where TItem : notnull
{
public JsonPolymorphicConverter(params (string key, Type type)[] mappings)
{
foreach (var (key, type) in mappings)
KeyTypeLookup.Add(key, type);
}
public ReversibleLookup<string, Type> KeyTypeLookup = new ReversibleLookup<string, Type>();
public override bool CanConvert(Type typeToConvert)
=> typeof(TItem).IsAssignableFrom(typeToConvert);
public override TItem Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Helper function for validating where you are in the JSON
void validateToken(Utf8JsonReader reader, JsonTokenType tokenType)
{
if (reader.TokenType != tokenType)
throw new JsonException($"Invalid token: Was expecting a '{tokenType}' token but received a '{reader.TokenType}' token");
}
TItem result = default(TItem);
reader.Read(); // Move to property name
validateToken(reader, JsonTokenType.PropertyName);
var typeKey = reader.GetString();
reader.Read(); // Move to start of object (stored in this property)
validateToken(reader, JsonTokenType.StartObject); // Start of vehicle
if (KeyTypeLookup.TryGetValue(typeKey, out var concreteItemType))
{
// WORKAROUND - stop cyclic look up
// If we leave our converter in the options then will get infinite cycling
// We create a temp options with our converter removed to stop the cycle
JsonSerializerOptions tempOptions = new JsonSerializerOptions(options);
tempOptions.Converters.Remove(this);
// Use normal deserialization
result = (TItem)JsonSerializer.Deserialize(ref reader, concreteItemType, tempOptions);
}
else
{
throw new JsonException($"Unknown type key '{typeKey}' found");
}
reader.Read(); // Move past end of item object
return result;
}
public override void Write(Utf8JsonWriter writer, TItem item, JsonSerializerOptions options)
{
var itemType = item.GetType();
writer.WriteStartObject();
if (KeyTypeLookup.ReverseLookup.TryGetValue(itemType, out var typeKey))
{
writer.WritePropertyName(typeKey);
// WORKAROUND - stop cyclic look up
// If we leave our converter in the options then will get infinite cycling
// We create a temp options with our converter removed to stop the cycle
JsonSerializerOptions tempOptions = new JsonSerializerOptions(options);
tempOptions.Converters.Remove(this);
// Use normal serialization
JsonSerializer.Serialize(writer, item, itemType, tempOptions);
}
else
{
throw new JsonException($"Unknown type '{itemType.FullName}' found");
}
writer.WriteEndObject();
}
}
}
这也依赖于
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
namespace Shared.DataAccess
{
/// <summary>
/// Helper class used with JsonPolymorphicConverter and HeterogenousListConverter
/// </summary>
/// <typeparam name="T1">First class type</typeparam>
/// <typeparam name="T2">Second class type</typeparam>
public class ReversibleLookup<T1, T2> : ReadOnlyDictionary<T1, T2>
where T1 : notnull
where T2 : notnull
{
public ReversibleLookup(params (T1, T2)[] mappings)
: base(new Dictionary<T1, T2>())
{
ReverseLookup = new ReadOnlyDictionary<T2, T1>(reverseLookup);
foreach (var mapping in mappings)
Add(mapping.Item1, mapping.Item2);
}
private readonly Dictionary<T2, T1> reverseLookup = new Dictionary<T2, T1>();
public ReadOnlyDictionary<T2, T1> ReverseLookup { get; }
[DebuggerHidden]
public void Add(T1 value1, T2 value2)
{
if (ContainsKey(value1))
throw new InvalidOperationException($"{nameof(value1)} is not unique");
if (ReverseLookup.ContainsKey(value2))
throw new InvalidOperationException($"{nameof(value2)} is not unique");
Dictionary.Add(value1, value2);
reverseLookup.Add(value2, value1);
}
public void Clear()
{
Dictionary.Clear();
reverseLookup.Clear();
}
}
}
用法示例
public class TradeStrategy {
public string name;
public TradeStrategy() : this("Unknown") { }
public TradeStrategy(string name) { this.name = name; }
public virtual double CalcAdjustments(double stockPrice) => 0.0;
}
public class TradeStrategyNone : TradeStrategy {
public TradeStrategyNone() : base("None") { }
public override double CalcAdjustments(double stockPrice) => 0.0;
}
public class TradeStrategyRandom : TradeStrategy {
private Random random { get; set; }
public TradeStrategyRandom() : base("Random") { random = new Random(); }
public override double CalcAdjustments(double stockPrice) => random.NextDouble();
}
public class Portfolio {
public TradeStrategy strategy;
}
var converter = new JsonPolymorphicConverter<TradeStrategy>(
(nameof(TradeStrategyNone), typeof(TradeStrategyNone)),
(nameof(TradeStrategyRandom), typeof(TradeStrategyRandom)));
var options = new JsonSerializerOptions();
options.Converters.Add(converter);
Portfolio port1 = new Portfolio();
port1.strategy = new TradeStrategyRandom();
// port1Json will contain "TradeStrategyRandom" type info for "TradeStrategy" strategy variable
var port1Json = JsonSerializer.Serialize(port1, options);
// port1Copy will properly create "TradeStrategyRandom" from the port1Json
Portfolio port1Copy = JsonSerializer.Deserialize<Portfolio>(port1Json, options);
// Without "options" the JSON will end up stripping down TradeStrategyRandom to TradeStrategy
如果您想弄清楚此解决方案与另一个解决方案之间的区别,请知道另一个解决方案要求您创建要转换的项目的数组。此解决方案适用于单个对象。
这是另一个基于之前解决方案的解决方案(JSON 结构略有不同)。
显着差异:
- 鉴别器是对象的一部分(不需要使用包装器对象)
- 令我惊讶的是,没有必要通过递归(反)序列化调用删除转换器 (.NET 6)
- 我没有添加自定义查找,请参阅以前的答案
代码:
var foo = new[] {
new Foo
{
Inner = new Bar
{
Value = 42,
},
},
new Foo
{
Inner = new Baz
{
Value = "Hello",
},
},
};
var opts = new JsonSerializerOptions
{
Converters =
{
new PolymorphicJsonConverterWithDiscriminator<Base>(typeof(Bar), typeof(Baz)),
},
};
var json = JsonSerializer.Serialize(foo, opts);
var foo2 = JsonSerializer.Deserialize<Foo[]>(json, opts);
Console.WriteLine(foo2 is not null && foo2.SequenceEqual(foo));
Console.ReadLine();
public static class Constants
{
public const string DiscriminatorPropertyName = "$type";
}
public record Foo
{
public Base? Inner { get; set; }
}
public abstract record Base();
public record Bar : Base
{
[JsonPropertyName(DiscriminatorPropertyName)]
[JsonPropertyOrder(int.MinValue)]
public string TypeDiscriminator { get => nameof(Bar); init { if (value != nameof(Bar)) throw new ArgumentException(); } }
public int Value { get; set; }
}
public record Baz : Base
{
[JsonPropertyName(DiscriminatorPropertyName)]
[JsonPropertyOrder(int.MinValue)]
public string TypeDiscriminator { get => nameof(Baz); init { if (value != nameof(Baz)) throw new ArgumentException(); } }
public string? Value { get; set; }
}
public class PolymorphicJsonConverterWithDiscriminator<TBase> : JsonConverter<TBase>
where TBase : class
{
private readonly Type[] supportedTypes;
public PolymorphicJsonConverterWithDiscriminator(params Type[] supportedTypes)
{
this.supportedTypes = supportedTypes;
}
public override TBase? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Clone the reader so we can pass the original to Deserialize.
var readerClone = reader;
if (readerClone.TokenType == JsonTokenType.Null)
{
return null;
}
if (readerClone.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
readerClone.Read();
if (readerClone.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
var propertyName = readerClone.GetString();
if (propertyName != DiscriminatorPropertyName)
{
throw new JsonException();
}
readerClone.Read();
if (readerClone.TokenType != JsonTokenType.String)
{
throw new JsonException();
}
var typeIdentifier = readerClone.GetString();
var specificType = supportedTypes.FirstOrDefault(t => t.Name == typeIdentifier)
?? throw new JsonException();
return (TBase?)JsonSerializer.Deserialize(ref reader, specificType, options);
}
public override void Write(Utf8JsonWriter writer, TBase value, JsonSerializerOptions options)
{
// Cast to object which forces the serializer to use runtime type.
JsonSerializer.Serialize(writer, value, typeof(object), options);
}
}
样本JSON:
[
{
"Inner": {
"$type": "Bar",
"Value": 42
}
},
{
"Inner": {
"$type": "Baz",
"Value": "Hello"
}
}
]