Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to deserialize BitArray using JsonSerializer?

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.

like image 886
Quantum-Pie Avatar asked Apr 13 '26 18:04

Quantum-Pie


2 Answers

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 + 7) >> 3]; // Fix for 0-length BitArrays suggested by https://stackoverflow.com/users/113701/doug-domeny
            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.

like image 98
dbc Avatar answered Apr 16 '26 07:04

dbc


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);
            }
        }    
like image 32
Quantum-Pie Avatar answered Apr 16 '26 07:04

Quantum-Pie



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!