Klark
Klark

Reputation: 8290

ProtoBuf-net invalid behaviour when serializing object containing list

I have hard time trying to explain behavior in following example:

[ProtoContract]
public class Class1Proto
{
    [ProtoMember(1)]
    public int data1 = 1;
    [ProtoMember(2)]
    public string data2 = "MYRANDOMSTRING";
}

[ProtoContract]
public class ProtoChunk
{
    [ProtoMember(1)]
    public List<Class1Proto> arr = new List<Class1Proto>();

    public const int PageSize = 4096;
}

Usage:

    byte[] page = new byte[ProtoChunk.PageSize];

    ProtoChunk originalData = new ProtoChunk();
    for (int i = 0; i < 100; i++)
    {
        Class1Proto p = new Class1Proto();
        p.data1 = i * 2;
        p.data2 = (i * 2).ToString();
        originalData.arr.Add(p);
    }

    using (var memStream = new MemoryStream(page, writable:true))
    {
        Serializer.SerializeWithLengthPrefix(memStream, originalData, PrefixStyle.Fixed32);
    }

    using (var memStream = new MemoryStream(page, writable:false))
    {
        ProtoChunk deserializedData = Serializer.DeserializeWithLengthPrefix<ProtoChunk>(memStream, PrefixStyle.Fixed32);
    }

My expectation would be that originalData and deserializedData should be identical. And they mostly are except that deserializedData.arr[0].data1 == 1 while originalData.arr[0].data1 == 0. All the other objects are identical, even including originalData.arr[0].data2 and deserializedData.arr[0].data2 (string field).

Upvotes: 0

Views: 100

Answers (1)

Marc Gravell
Marc Gravell

Reputation: 1064184

protobuf-net assumes "implicit zero defaults" - i.e. unless specific otherwise, members have a default value of zero, meaning: zero is not transmitted. This isn't purely arbitrary - this is actually the "proto3" specification (well... more or less; in "proto3", zero is the only permitted default).

Your code - particularly the property initializer - acts as though it has a default value of 1, so: when the zero isn't transmitted, the 1 still gets applied by your constructor, and that becomes the value (deserialization in protobuf is a "merge" operation - pre-existing values are retained, again accordance with the specification).

Options:

  • tell protobuf-net about your default - add [DefaultValue(1)] to the property
  • tell protobuf-net not to run the constructor (and property initializer) - add SkipConstructor = true to the [ProtoContract]
  • tell protobuf-net not to assume this behaviour: RuntimeTypeModel.Default.ImplicitZeroDefault = false;
  • add your own conditional serialization callbacks (I can give an example if you really want)

I'd use the first option, personally.

Upvotes: 3

Related Questions