Как десериализовать BitArray с помощью JsonSerializer? - PullRequest
1 голос
/ 08 октября 2019

Я пытаюсь сериализовать и десериализовать объекты в файлы Json в словарь с помощью Json.Net. Сериализация прекрасно работает, и я вижу все данные в файле. Но когда я пытаюсь десериализовать, произойдет сбой при заполнении System.Collections.BitArray. BitArrays не поддерживаются должным образом?

Файл Json имеет правильные значения и правильную форму. Я также прошел через код, и он правильно строит объект, не устанавливая значение BitArray. До сих пор он работал корректно для всех целей, но только после того, как я представил объект с BitArray.

Failing Object

    [DataContract]
    public class Chip
    {
        [DataMember]
        public Guid ID { get; set; }

        [DataMember]
        public BitArray Input { get; set; } //Failing on setting this value
        [DataMember]
        public BitArray Output { get; set; }

        [DataMember]
        public List<Gate> Gates { get; set; }
        [DataMember]
        public List<Chip> Chips { get; set; }
        [DataMember]
        public Dictionary<Guid, List<Wire>> WireDict { get; set; }

        [DataMember]
        protected BitArray Dirty { get; set; }

        protected Chip(int inputs, int outputs)
        {
            ID = Guid.NewGuid();

            Input = new BitArray(inputs, false);
            Output = new BitArray(outputs, false);
            Dirty = new BitArray(outputs, false);

            Gates = new List<Gate>();
            Chips = new List<Chip>();
            WireDict = new Dictionary<Guid, List<Wire>>();
        }
    }

Код, который я использую для сериализации

*
using(StreamWriter file = File.CreateText(filePath))
{
    JsonSerializer serializer = new JsonSerializer
    {
        TypeNameHandling = TypeNameHandling.Auto,
        Formatting = Formatting.Indented
    };            

    serializer.Serialize(file, componentsDict);
}

Код, который я использую для десериализации

using (StreamReader file = File.OpenText(filePath))
{
    JsonSerializer serializer = new JsonSerializer();
    serializer.TypeNameHandling = TypeNameHandling.Auto;
    Dictionary<Guid, ChipWrapper> componentsDict = (Dictionary<Guid, ChipWrapper>)serializer.Deserialize(file, typeof(Dictionary<Guid, ChipWrapper>));
}

Я получаю ошибку

JsonSerializationException: Cannot populate list type System.Collections.BitArray. Path 'a77af562-0e5e-4471-86c5-06857610ae6d.Chip.Input', line 612, position 16.
Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateList (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, System.Object existingValue, System.String id) (at <97722d3abc9f4cf69f9e21e6770081b3>:0)
Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal (Newtonsoft.Json.JsonReader reader, System.Type objectType, 

Etc ...

Словарь содержит класс, из которого получено множество других классов, но только классы с битовыми массивами дают сбой.

Ответы [ 2 ]

1 голос
/ 08 октября 2019

Вы правы, Json.NET не может сериализовать BitArray, особенно потому, что этот класс является нетипизированной коллекцией , датируемой .Net 1.1 :

public sealed class BitArray : ICloneable, System.Collections.ICollection

Поскольку класс реализует только ICollection, а не ICollection<bool>, Json.NET не знает правильный тип, к которому следует десериализовать свои члены, и как добавить их воднажды созданная коллекция.

Самый простой способ обойти эту проблему - создать пользовательский JsonConverter для этого типа. Но сначала нам нужно выбрать способ представления BitArray в JSON. Есть две возможности:

  1. в виде массива bool значений. С этим представлением легко работать, но при сериализации оно будет занимать значительное пространство.

    A JsonConverter, генерирующее JSON в этом формате, будет выглядеть следующим образом:

    public class BitArrayConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(BitArray);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var bools = serializer.Deserialize<bool[]>(reader);
            return bools == null ? null : new BitArray(bools);
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            serializer.Serialize(writer, ((BitArray)value).Cast<bool>());
        }
    }
    

    При соответствующем JSON:

    {
      "ID": "cb7f8e9e-a4a8-4cc5-b96f-c07779f91409",
      "Input": [
        true,
        true,
        false
      ],
      "Output": [
        true,
        true,
        false
      ],
      "Dirty": [
        false,
        false,
        false
      ]
    }
    
  2. Как массив byte [], который Json.NET будет автоматически кодировать как Base64. С этим представлением может быть не так просто работать, но оно будет значительно более компактным.

    Преобразователь для этого формата будет выглядеть так:

    public class BitArrayConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(BitArray);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var bytes = serializer.Deserialize<byte[]>(reader);
            return bytes == null ? null : new BitArray(bytes);
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            serializer.Serialize(writer, ((BitArray)value).BitArrayToByteArray());
        }
    }
    
    public static class BitArrayExtensions
    {
        // Copied from this answer https://stackoverflow.com/a/4619295
        // To /396145/konvertirovat-iz-bitarray-v-byte
        // By https://stackoverflow.com/users/313088/tedd-hansen
        // And made an extension method.
        public static byte[] BitArrayToByteArray(this BitArray bits)
        {
            byte[] ret = new byte[(bits.Length - 1) / 8 + 1];
            bits.CopyTo(ret, 0);
            return ret;
        }
    }
    

    С соответствующим JSON:

    {
      "ID": "2df93cf8-d28f-40d5-9959-46afc1679d5d",
      "Input": "fw==",
      "Output": "fw==",
      "Dirty": "AA=="
    }
    

Теперь существует дополнительная проблема с типом Chip, показанным в вашем вопросе, а именно, что у него нет открытого конструктора (или даже частного конструктора по умолчанию). Таким образом, Json.NET не будет знать, как его построить. Это может быть опечаткой в ​​вашем вопросе, но если нет, вам также понадобится JsonConverter для Chip, в частности, наследование от CustomCreationConverter<T>:

public class ChipConverter : CustomCreationConverter<Chip>
{
    public override bool CanConvert(Type objectType) { return objectType == typeof(Chip); }

    public override Chip Create(Type objectType)
    {
        return (Chip)Activator.CreateInstance(typeof(Chip), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { 0, 0 }, null);
    }
}

Когда у вас есть все необходимые конвертеры, вы можете сериализовать и десериализовать их следующим образом:

var settings = new JsonSerializerSettings
{
    Converters = { new ChipConverter(), new BitArrayConverter() },
};
var chipJson = JsonConvert.SerializeObject(chip, Formatting.Indented, settings);
var chip2 = JsonConvert.DeserializeObject<Chip>(chipJson, settings);

Добавляя конвертер в настройки, нет необходимости вносить какие-либо изменения в ваш Chipмодель данных.

Демонстрационная скрипка здесь .

0 голосов
/ 08 октября 2019

Не знаю, почему вы не можете десериализовать напрямую, но я смог обойти это, создав «фальшивый» логический массив, который действует как интерфейс к реальному BitArray. Единственный раз, когда доступ к массиву bool происходит при сериализации, поэтому он вызывается только несколько раз и позволяет мне продолжать использовать объект BitArray.

        public BitArray Input { get; set; }

        [DataMember]
        private bool[] _Input {
            get {
                bool[] b = new bool[Input.Length];
                Input.CopyTo(b, 0);
                return b;
            }
            set
            {
                Input = new BitArray(value);
            }
        }    
...