Есть ли простой способ вручную сериализовать / десериализовать дочерние объекты в пользовательском конвертере в System.Text. Json? - PullRequest
3 голосов
/ 15 января 2020

ПРИМЕЧАНИЕ. Я использую новый 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 для чтения имен свойств (например, 'Car' и 'Bicycle' и может соответственно написать оператор switch, то, что я не знаю, это как отступить к конвертерам Car и Bicycle по умолчанию (т. е. стандартным конвертерам JSON) , поскольку я не вижу в считывателе какого-либо метода для чтения в указанном c типизированном объекте.

Так как же вручную десериализовать вложенные объекты, подобные этой?

Ответы [ 2 ]

2 голосов
/ 15 января 2020

Я понял это. Вы просто передаёте свое устройство чтения / записи другому экземпляру 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();        
    }
}
0 голосов
/ 15 января 2020

Вот упрощенный c подход, который, я надеюсь, сработает для вас.

Вы можете использовать dynamici c переменную

Я заметил в комментариях, что вы не хотели использовать NetwonSoft. Json, вы можете использовать этот код: dynamic car = Json.Decode(json);

Класс Json происходит от здесь

...