Skip to content

Custom serialization

yfakariya edited this page Nov 15, 2015 · 5 revisions

msgpack-cli's built-in serialization mechanism can support most cases, but there are edge cases which it does not support. For example:

  • The type defines its own binary representation.
  • The requirement for serialization/deserialization speed is critical (note that default implementation generates straightforward code -- it has good performance).
  • You do not refer *.Serialization assembly in Silverlight/WindowsPhone project to reduce application size. In this case, you must use Manually packing/unpacking because the built-in serialization mechanism is contained in those assembly.

This article discusses the first and second items above. See Manual packings for third item.

There are two options to implement custom serialization -- IPackable/IUnpackable and Custom Serializer.

IPackable

The easiest way to implement custom serialization is implementing IPackable and IUnpackable on your class. In this case, you will interact with packers/unpackers via MessagePackObject.

Note that the serialization mechanism prefers to use registered (custom) serializers even if the type implements IPackable and IUnpackable because the runtime generate dedicated serializer for the type, which delegates actual serialization process to IPackable/IUnpackable methods.

The type must have default public constructor even if it implements IPackable and IUnpackable.

Custom Serializer

If you want to serialize existing types, want to share the serialization logic in a number of MessagePackSerialier, or the type does not have default public constructor, you can implement your own custom MessagePackSerializer. In this case, you must register its instance to the used serialization context. To use custom serializer, you must do following:

  1. Create dedicated SerializationContext instance.
  2. Implement custom serializer. The serializer must derived from MessagePackSerializer<T>, and implements its abstract methods.
    • PackToCore method. It is similar to IPackable.PackTo method. Implement packing logic from the instance to the stream via Packer object.
    • UnpackFromCore method. It is similar to IUnpackable.UnpackFrom method. Implement unpacking logic which pulls data from the stream via Unpacker object and deserialize to newly created instance.
    • Optional UnpackToCore method. If T is collection and it has public default constructor, this method implements item feeding style deserialization. Specifically, if the hosted type has T type member which is read only and initialized in its constructor, the serializer will use UnpackTo to deserialize elements for the read only collection member.
  3. Create custom serializer using dedicated serialization context.
  4. Register serializer instance to SerialzationRepository via SerializationContext.Serializers property. As of 0.6.0, you can register on demand via the event handler for SerializationContext.ResolveSerializer event.

If you use MessagePackSerializer.Create to get a built-in serializer, you must pass a dedicated serialization context so that the generated serializers use registered custom serializers. Also, you must pass dedicated serialization context to use custom serializers in MessagePack RPC APIs.

Example

Here is a sample custom DateTimeOffset serializer implementation:

[C#]

public class CustomDateTimeOffsetSerializer : MessagePackSerializer<DateTimeOffset>
{
    public CustomDateTimeOffsetSerializer()
        : this( SerializationContext.Default ) { }

    public CustomDateTimeOffsetSerializer( SerializationContext context )
    {
        // If the target objects has complex (non-primitive) objects,
        // you can get serializers which can handle complex type fields.
        // And then, you can cache them to instance fields of this custom serializer.
    }

    protected override void PackToCore( Packer packer, DateTimeOffset value )
    {
        // First, pack array length (or map length).
        // It should be a number of members of the object to be serialized.
        packer.PackArrayHeader( 2 );

        // If you choice array encoding, just pack fields in order.
        packer.Pack( value.Ticks );
        packer.Pack( ( short )value.Offset.TotalMinutes );
    }

    protected override DateTimeOffset UnpackFromCore( Unpacker unpacker )
    {
        // unpacker should be located in the underlying stream.
        // It should not be the head of the stream.
        Contract.Assume( unpacker.IsArrayHeader || unpacker.IsMapHeader );

        // TIPS: You might not be able to determine that the incoming stream contains
        //       array encoded msgpack stream or map encoded msgpack stream.
        //       So, you should implement both unpacking logic.
        if ( unpacker.IsArrayHeader )
        {
            // Note exception handling is omitted...
            // Fetch next field.
            if ( !unpacker.Read() )
            {
                throw new SerializationException();
            }

            // Data is MessagePackObject, so you can cast it to primitive value.
            var ticks = ( long )unpacker.Data.Value;

            // As of 0.3, you can use ReadXxx(out T) method.
            short offset;
            if ( !unpacker.ReadInt16( out offset ) )
            {
                throw new SerializationException();
            }

            return new DateTimeOffset( ticks, TimeSpan.FromMinutes( offset ) );
        }
        else // Map encoded
        {
            // You should not assume that keys are ordered as you expect.
            string key;
            long ticks = 0;
            short offset = 0;
            bool ticksFound = false;
            bool offsetFound = false;
            while ( unpacker.ReadString( out key ) )
            {
                // Assume key is property name...
                switch ( key )
                {
                    case "Ticks":
                    {
                        if ( !unpacker.ReadInt64( out ticks ) )
                        {
                            throw new SerializationException();
                        }

                        ticksFound = true;
                        break;
                    }
                    case "Offset":
                    {
                        if ( !unpacker.ReadInt16( out offset ) )
                        {
                            throw new SerializationException();
                        }

                        offsetFound = true;
                        break;
                    }
                    default:
                    {
                        // Unknown key.
                        throw new SerializationException();
                    }
                }
            }

            if ( !ticksFound || !offsetFound )
            {
                // Incompleted stream
                throw new SerializationException();
            }

            return new DateTimeOffset( ticks, TimeSpan.FromMinutes( offset ) );
        }
    }
}