Serialization problem in a custom collection with a private list

474 views Asked by At

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)?

1

There are 1 answers

1
dbc On BEST ANSWER

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.