【问题标题】:Is there a simple way to manually serialize/deserialize child objects in a custom converter in System.Text.Json?是否有一种简单的方法可以在 System.Text.Json 的自定义转换器中手动序列化/反序列化子对象?
【发布时间】:2020-01-14 23:48:20
【问题描述】:

注意:我使用的是 Microsoft 的新 System.Text.Json 而不是 Json.NET,因此请确保答案相应地解决了这个问题。

考虑这些简单的 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 来读取属性名称(例如“汽车”和“自行车”,并且可以相应地编写一个 switch 语句,我不这样做'不知道如何回退到默认的 CarBicycle 转换器(即标准 JSON 转换器),因为我没有看到读者有任何方法可以读取特定类型对象。

那么如何手动反序列化这样的嵌套对象呢?

【问题讨论】:

  • 一个值得挑战的好问题 :-) AutoMapper 对此有什么帮助吗?或者您是否正在寻找基于非库的转换/映射?我会把它当作面试编码练习......
  • 无库。但是...我想我在这里做点什么...马上回来!
  • 这看起来像是给 NewtonSoft.Json 的。我正在尝试使用 System.Text.Json

标签: c# json system.text.json


【解决方案1】:

我想通了。您只需将读取器/写入器传递给 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();        
    }
}

【讨论】:

  • docs.microsoft.com/en-us/dotnet/standard/serialization/… 该文档显示了一个类似的多态反序列化示例,但它直接创建类型并使用读取器填充属性值,而不是调用JsonSerializer。对于更复杂的类型(或者如果您不想要这种细粒度控制),一旦您知道类型,依赖 JsonSerializer 是有意义的。
  • 这就是我试图解决的问题......嵌套的具体类型。他们的示例适用于简单的对象,但是一旦您进入其他具体类,就没有理由重新发明轮子。我一直在寻找简单的“设置并忘记”的东西,只担心密钥(属性名称)和存储数据的类型。其他的事情不应该由我来处理。
  • 很好的解决方案,解决了我的很多问题。谢谢!
  • ?我不明白 ReversibleLookup 为什么要继承 ReadOnlyDictionary,如果它包含 Dictionary&lt;T2, T1&gt; reverseLookup 又名现在你有两个是道具,一个来自基地,这是没有意义的。你为什么有这个?肯定可以删除一个...我会删除ReadOnlyDictionary,您不会丢失任何东西,并且拥有两本字典没有意义,@MarkA.Donohoe
  • 非常感谢马克的回答。您的代码非常可重用,对我的项目很有用
【解决方案2】:

这是一个适用于单个对象的解决方案(不需要对象数组)。这是 https://stackoverflow.com/a/59744873 的副本,修改后可以在没有 IList 的情况下工作。

这是主类

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();
        }
    }
}

这也依赖于来自https://stackoverflow.com/a/59744873 的 ReversibleLookup 类。为了方便,我在这里复制。代码是一样的,我只是在顶部添加了注释。

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

如果您想弄清楚此解决方案与其他解决方案之间的区别,请知道其他解决方案要求您创建要转换的项目的数组。此解决方案适用于单个对象。

【讨论】:

    【解决方案3】:

    这是建立在先前解决方案基础上的另一种解决方案(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"
        }
      }
    ]
    

    【讨论】:

      【解决方案4】:

      这是一个简单的方法,希望对你有用。

      您可以使用dynamic variable

      我在 cmets 中注意到您并不热衷于使用 NetwonSoft.Json,您可以使用以下代码:dynamic car = Json.Decode(json);

      Json 类来自here

      【讨论】:

      • 这不是Json.NET吗?我正在使用System.Text.Json
      • @MarqueIV - 它已在文档中链接。程序集:System.Web.Helpers.dll
      • @MarqueIV 另外,重要的不是您使用的库,而是动态对象。您可以使用 System.Text.Json 序列化/反序列化对象的方式。
      • 但是动态对象不是具体类型!例如,如果您有一个仅使用动态的枚举,那么您最终会得到一个字符串而不是枚举值。这是因为 JSON 没有类型信息。我在下面提出的解决方案返回真实的具体类型,尽管是通过它们的基类,但您可以再次将它们强制转换回来,以及将它们用作 ItemsSources 在诸如绑定之类的事情中,它们将与它们的具体类型相匹配。动态不做任何这些。它不能,因为它没有所需的信息。
      • @MarqueIV - +1 是的,这是正确的,在某些情况下它仍然是一个选项,尽管因此认为值得将其放入此线程。
      猜你喜欢
      • 1970-01-01
      • 2019-08-24
      • 1970-01-01
      • 2020-03-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-07-22
      相关资源
      最近更新 更多