Skip to content
Klaus Post edited this page Nov 1, 2024 · 4 revisions

Versioning

Msgpack behaves much like JSON, so adding and removing fields are generally safe.

The general exception is tuples. This offers compact representation, but at the expense of easy versioning.

If you use tuples, you will need to implement versions manually, similar to manually encoded primitives (non-structs).

Adding & Removing Fields

Adding fields usually can be done without worrying about backwards compatibility, since unknown fields are skipped by the generated decoders.

Similar removing fields will cause the newer versions to simply ignore the field.

Renaming Fields

Similar to JSON Renaming fields can be a challenge, but it should be handled similarly.

For simple examples this can be done, by deserializing into a compatible struct while using the new for serializing.

For example, say we would like to change field X to Y of this struct...

type A struct {
   X string
}

After our change to A, we wrap it and when reading we unmarshal into WrapA:

type A struct {
   Y string
}

type WrapA struct {
   A `msg:",flatten"`
   X *string
}

After decoding, we will need to convert X from the wrapper into A.Y. This keeps the "A" struct nice and clean.

This does break down if A is used in other structs.
In such cases the X *string will need to be moved to A, and users of the struct will need to deal with the fact that the value can be in either.

Alternatively writing a custom msgp.Unmarshaler and msgp.Decodable - for example by copying and modifying the generated output may be easier.

Changing types

Similar to JSON there is no easy way to change types without renaming the field.

Note that some types, like numbers are forward compatible for reading. So changing uint8 -> uint16 always fully compatible with existing content. The only requirement is that the size can only safely go up.

In cases where it is unavoidable to rename the field, the best recommendation is to copy the Decoder/Unmarshal implementation and manually handle the change, and bypass the generator for this type.

For example, if the field C has changed from a timestamp to a string, it could look something like this:

		switch msgp.UnsafeString(field) {
		case "C":
			switch msgp.NextType(bts) {
			case msgp.StrType:
				z.C, bts, err = msgp.ReadStringBytes(bts)
				if err != nil {
					err = msgp.WrapError(err, "C")
					return
				}
			case msgp.TimeType:
				var t time.Time
				t, bts, err = msgp.ReadTimeBytes(bts)
				if err != nil {
					err = msgp.WrapError(err, "C")
					return
				}
				z.C = t.Format(time.RFC3339)
			default:
				err = errors.New("did someone change the type again??")
				return
			}
...

If the same type change is happening in multiple places, alternative is implement the same behavior for a separate type that can then be used for these fields, and implement the interfaces needed (msgp.Unmarshaler, etc).

So changing types without changing the name is generally tedious.