1

I am trying to serialize and de-serialize objects to Json files to a Dictionary using Json.Net. The serialization works great and I can see all the data in the file. But when I try and de-serialize it fails on populating a System.Collections.BitArray. Are BitArrays not properly supported?

The Json file appears to have the correct values and in the correct form. I've also stepped through the code and it builds the object correctly only failing to set the value of the BitArray. It has been working correctly so far for all objcets, only failing once I introduced an object with a BitArray.

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

The Code I'm using to serialize

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

    serializer.Serialize(file, componentsDict);
}

The Code I'm using to de-serialize

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

I get the error

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

The Dictionary holds a class which a lot of other classes are derived from but only the classes with bit arrays are failing.

Quantum-Pie
  • 196
  • 7
  • How do you create your `Chip` object given that the only constructor is protected? Is there a public parameterless constructor? – dbc Oct 08 '19 at 02:53
  • Chip is a parent/base class that a bunch of other classes inherit from and all those inherited classes have public constructors. Since I never want to create an instance of the base 'Chip' class on it's own i made it's constructor protected. – Quantum-Pie Oct 09 '19 at 15:44

2 Answers2

4

You are correct, Json.NET cannot serialize BitArray, specifically because this class is an untyped collection dating from .Net 1.1:

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

Since the class only implements ICollection and not ICollection<bool>, Json.NET does not know the correct type to which to deserialize its members, nor how to add them to the collection once created.

The easiest way to work around this problem is to create a custom JsonConverter for this type. But first, we will need to chose how to represent the BitArray in JSON. There are two possibilities:

  1. As an array of bool values. This representation is simple to work with but will consume substantial space when serialized.

    A JsonConverter that generates JSON in this format would look like:

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

    With corresponding JSON:

    {
      "ID": "4fa76f3a-66fc-4832-8c94-280725486270",
      "Input": [
        true,
        true,
        false
      ],
      "Output": [
        true,
        true,
        false
      ],
      "Dirty": [
        false,
        false,
        false
      ]
    }
    
  2. As a string of 0 and 1 characters, e.g. "110". This will be more compact that the array above yet should still be fairly easy to work with:

    public class BitArrayConverter : JsonConverter<BitArray>
    {
        public override BitArray ReadJson(JsonReader reader, Type objectType, BitArray existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            if (reader.TokenType == JsonToken.Null)
                return null;
            else if (reader.TokenType != JsonToken.String)
                throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
            var s = (string)reader.Value;
            var bitArray = new BitArray(s.Length);
            for (int i = 0; i < s.Length; i++)
                bitArray[i] = s[i] == '0' ? false : s[i] == '1' ? true : throw new JsonSerializationException(string.Format("Unknown bit value {0}", s[i]));
            return bitArray;
        }
    
        public override void WriteJson(JsonWriter writer, BitArray value, JsonSerializer serializer)
        {
            writer.WriteValue(value.Cast<bool>().Aggregate(new StringBuilder(value.Length), (sb, b) => sb.Append(b ? "1" : "0")).ToString());
        }
    }
    

    With corresponding JSON:

    {
      "ID": "4fa76f3a-66fc-4832-8c94-280725486270",
      "Input": "110",
      "Output": "110",
      "Dirty": "000"
    }
    
  3. As a DTO containing a byte [] array along with the number of bits. This representation may be less easy to work with but will most compact for large arrays as Json.NET will automatically encode the byte array in Base64.

    A converter for this format would look like:

    public class BitArrayConverter : JsonConverter<BitArray>
    {
        class BitArrayDTO
        {
            public byte[] Bytes { get; set; }
            public int Length { get; set; }
        }
    
        public override BitArray ReadJson(JsonReader reader, Type objectType, BitArray existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            var dto = serializer.Deserialize<BitArrayDTO>(reader);
            if (dto == null)
                return null;
            var bitArray = new BitArray(dto.Bytes);
            bitArray.Length = dto.Length;
            return bitArray;
        }
    
        public override void WriteJson(JsonWriter writer, BitArray value, JsonSerializer serializer)
        {
            var dto = new BitArrayDTO
            {
                Bytes = value.BitArrayToByteArray(),
                Length = value.Length
            };
           serializer.Serialize(writer, dto);
        }
    }
    
    public static class BitArrayExtensions
    {
        // Copied from this answer https://stackoverflow.com/a/4619295
        // To https://stackoverflow.com/questions/560123/convert-from-bitarray-to-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;
        }
    }
    

    With corresponding JSON:

    {
      "ID": "4fa76f3a-66fc-4832-8c94-280725486270",
      "Input": {
        "Bytes": "Aw==",
        "Length": 3
      },
      "Output": {
        "Bytes": "Aw==",
        "Length": 3
      },
      "Dirty": {
        "Bytes": "AA==",
        "Length": 3
      }
    }
    

Now, there is an additional problem with the Chip type shown in your question, namely that it does not have a public constructor (or even a private default constructor). As such, Json.NET will not know how to construct it. This may be a typo in your question, but if not, you will also need a JsonConverter for Chip, specifically one inheriting from 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);
    }
}

Once you have all the necessary converters, you can serialize and deserialize as follows:

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

By adding the converter in settings, it is not necessary to make any changes to your Chip data model.

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • The BitArray to Byte looks promising, especially since that it will reduce the size of my json files, if at the cost of readability. The Chip class doesn't need a public constructor because my Dictionary of Chips is filled entirely with classes derived from Chip. If there is a plain instance of Chip in the dictionary I want it to fail right now cause it means something went wrong somewhere. – Quantum-Pie Oct 09 '19 at 15:48
  • @Quantum-Pie - if `Chip` can never exist as an independent object, you can declare it as `abstract` to clearly indicate it can't be instantiated standalone. Also, if the question is answered, you might [mark it as such](https://meta.stackexchange.com/q/147531). – dbc Oct 09 '19 at 18:36
  • Right, thank you. I was just trying to get it working when I made the class and had completely forgotten about abstract classes. – Quantum-Pie Oct 09 '19 at 20:01
  • 1
    @dbc, I tried out your solution and it works great. However, one observation I found is that when I deserialize, my `BitArray` will always have more elements. Is that due to the fact that there is 8 bits in a byte; and whenever I serialize and deserialize, it will introduced the addition bits which increases the BitArray elements? – SimonSays Jul 30 '20 at 06:33
  • @SimonSays - looks like you may be right, in my original test fiddle I the number of bools was always divisible by 8. See https://dotnetfiddle.net/PMrScd. Will update. – dbc Aug 03 '20 at 21:30
1

Not sure why you can't deserialize directly but I was able to get around it by creating a 'fake' boolean array that acts as an interface to the actual BitArray. The only time the bool array is accessed is on serializing so it is only called a few times and allows me to keep using the BitArray object.

        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);
            }
        }    
Quantum-Pie
  • 196
  • 7