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 语句的自定义转换器因此,我不知道如何退回到默认的 CarBicycle 转换器(即标准 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"
    }
  }
]