有没有一种简单的方法可以在System.Text.Json的自定义转换器中手动序列化/反序列化子对象? [英] Is there a simple way to manually serialize/deserialize child objects in a custom converter in System.Text.Json?

查看:708
本文介绍了有没有一种简单的方法可以在System.Text.Json的自定义转换器中手动序列化/反序列化子对象?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

注意:我使用的是Microsoft的新版System.Text.Json而不是Json.NET,因此请确保相应地回答此问题.

NOTE: I am using Microsoft's new System.Text.Json and not Json.NET so make sure answers address this accordingly.

考虑以下简单的POCO:

Consider these simple POCOs:

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表示...

The car can be represented in JSON like this...

{
  "make": "Smart",
  "numberOfDoors": 2
}

自行车可以像这样...

and the bicycle can be represented like this...

{
  "frontGears": 3,
  "backGears": 6
}

挺直的.现在考虑使用此JSON.

Pretty straight forward. Now consider this JSON.

[
  {
    "Car": {
      "make": "Smart",
      "numberOfDoors": 2
    }
  },
  {
    "Car": {
      "make": "Lexus",
      "numberOfDoors": 4
    }
  },
  {
    "Bicycle" : {
      "frontGears": 3,
      "backGears": 6
    }
  }
]

这是一个对象数组,其中属性名称是知道相应嵌套对象所指类型的键.

This is an array of objects where the property name is the key to know which type the corresponding nested object refers to.

虽然我知道如何编写一个使用UTF8JsonReader读取属性名称的自定义转换器(例如"Car"和"Bicycle",并可以据此编写switch语句, 我不这样做)我不知道该如何使用默认的CarBicycle转换器(即标准JSON转换器) ,因为我看不到读取器上有任何方法可以读取特定类型的对象.

While I know how to write a custom converter that uses the UTF8JsonReader to read the property names (e.g. 'Car' and 'Bicycle' and can write a switch statement accordingly, what I don't know is how to fall back to the default Car and Bicycle converters (i.e. the standard JSON converters) since I don't see any method on the reader to read in a specific typed object.

那么如何才能手动反序列化嵌套对象呢?

So how can you manually deserialize nested objects like this?

推荐答案

我知道了.您只需将您的读取器/写入器传递给JsonSerializer的另一个实例,它就可以像对待本地对象一样对其进行处理.

I figured it out. You simply pass your reader/writer down to another instance of the JsonSerializer and it handles it as if it were a native object.

这是一个完整的示例,您可以将其粘贴到RoslynPad之类的文件中,然后运行它.

Here's a complete example you can paste into something like RoslynPad and just run it.

这是实现...

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

这是上面使用的支持双向查询的方法...

Here's the supporting two-way lookup used above...

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

这篇关于有没有一种简单的方法可以在System.Text.Json的自定义转换器中手动序列化/反序列化子对象?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆