Search code examples
c#serializationjson.netjsonconvert

Serialization problem in a custom collection with a private list


Given two classes as follows:

class ListCollection : List<int>
{
}

[Serializable]
class PrivateListCollection: ISerializable
{
    private List<int> list = new List<int>();

    public void Add(int value)
    {
        list.Add(value);
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("items", list);
    }
}

I get after adding 1,2,3 to each of them and serializing using JsonConvert.SerializeObject this:

ListCollection: [1,2,3]
PrivateListCollection: {"items":[1,2,3]}

The question is whether it's possible to get the serialization result of PrivateListCollection as [1,2,3] (without the "items" or whatever in front of it)?


Solution

  • An POCO cannot serialize itself as a JSON array by implementing ISerializable because a JSON array is an ordered sequence of values, while SerializationInfo represents a collection of name/value pairs -- which matches exactly the definition of a JSON object rather than an array. This is confirmed by the Json.NET docs:

    ISerializable

    Types that implement ISerializable and are marked with SerializableAttribute are serialized as JSON objects. When serializing, only the values returned from ISerializable.GetObjectData are used; members on the type are ignored. When deserializing, the constructor with a SerializationInfo and StreamingContext is called, passing the JSON object's values.

    To serialize your PrivateListCollection as a JSON array, you have a couple of options.

    Firstly, you could create a custom JsonConverer:

    class PrivateListCollectionConverter : JsonConverter<PrivateListCollection>
    {
        const string itemsName = "items";
        
        public override PrivateListCollection ReadJson(JsonReader reader, Type objectType, PrivateListCollection existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            var list = serializer.Deserialize<List<int>>(reader);
            if (list == null)
                return null;
            existingValue = existingValue ?? new PrivateListCollection();
            foreach (var item in list)
                existingValue.Add(item);
            return existingValue;
        }
    
        public override void WriteJson(JsonWriter writer, PrivateListCollection value, JsonSerializer serializer)
        {
            // There doesn't seem to be any way to extract the items from the PrivateListCollection other than to call GetObjectData(), so let's do that.
            // Adding PrivateListCollectionConverter as a nested type inside PrivateListCollection would be another option to make the items accessible.
            ISerializable serializable = value;
            var info = new SerializationInfo(value.GetType(), new FormatterConverter());
            serializable.GetObjectData(info, serializer.Context);
            var list = info.GetValue(itemsName, typeof(IEnumerable<int>));
            serializer.Serialize(writer, list);
        }
    }
    

    And then either add it to PrivateListCollection like so:

    [Serializable]
    [JsonConverter(typeof(PrivateListCollectionConverter))]
    class PrivateListCollection: ISerializable
    {
        // Remainder unchanged
    

    Or add it to JsonSerializerSettings like so:

    var settings = new JsonSerializerSettings
    {
        Converters = { new PrivateListCollectionConverter() },
    };
    

    Demo fiddle #1 here.

    Secondly, you could make PrivateListCollection implement IEnumerable<int> and add a parameterized constructor taking a single IEnumerable<int> parameter like so:

    [Serializable]
    class PrivateListCollection: ISerializable, IEnumerable<int>
    {
        public PrivateListCollection() { }
        public PrivateListCollection(IEnumerable<int> items) => list.AddRange(items);
    
        private List<int> list = new List<int>();
    
        public void Add(int value)
        {
            list.Add(value);
        }
    
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("items", list);
        }
                
        public IEnumerator<int> GetEnumerator() => list.GetEnumerator();
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
    

    Having done so, Json.NET will see PrivateListCollection as a constructible read-only collection which can be round-tripped as a JSON array. However, based on the fact that you have named your type PrivateListCollection, you may not want to implement enumeration.

    Demo fiddle #2 here.