msgpack icon indicating copy to clipboard operation
msgpack copied to clipboard

[Feature Request] Support unmarshal into orderedMap

Open MejiroRina opened this issue 3 months ago • 2 comments

Description

In Go, the built-in map type does not guarantee iteration order. When unmarshaling data structures (such as those encoded via MessagePack), the key iteration order of a map is non-deterministic. This is often acceptable for many use cases, but in some scenarios, preserving the original key order is essential.

My use case involves processing MessagePack data where the ordering of keys carries semantic meaning (e.g., Convert msgpack data into JSON data). For these cases, I need to unmarshal directly into an orderedMap type (such as github.com/iancoleman/orderedmap), rather than a standard Go map[string]interface{}.

At the moment, msgpack decoding always uses the Go map type for map objects, resulting in loss of ordering. It would be very helpful if the library could support unmarshaling into a user-defined ordered map type, either by:

  • Detecting a known orderedMap type and populating it accordingly, or
  • Providing a customizable decode hook / interface that allows users to define how map entries should be stored when decoded.

Code Example

var m orderedmap.OrderedMap
err := msgpack.Unmarshal(data, &m)
if err != nil {
    return err
}

This feature would allow applications that depend on key order to use this library more reliably.

Thank you for your work on this project!

MejiroRina avatar Nov 06 '25 19:11 MejiroRina

orderedMap implements custom JSON encoding and decoding (MarshalJSON / UnmarshalJSON) from the encoding/json package, so it can be converted to and from JSON without any additional handling.

In my package, you can achieve similar behavior by implementing an encoder and decoder using the ext package and registering them with msgpack.

I’ve also created an example implementation, so please take a look and use it as a reference. You can also test this example on Playgound: https://go.dev/play/p/mGid5Hjddly

const orderedMapExtCode = 100

type internalOrderedMap struct {
	Keys []string `msgpack:"k"`
	Vals []any    `msgpack:"v"`
}

func (om internalOrderedMap) ToOrderedMap() orderedmap.OrderedMap {
	orderedMap := orderedmap.New()
	for i, key := range om.Keys {
		orderedMap.Set(key, om.Vals[i])
	}
	return *orderedMap
}

func newInternalMap(om orderedmap.OrderedMap) internalOrderedMap {
	v := internalOrderedMap{
		Keys: om.Keys(),
		Vals: make([]any, 0, len(om.Values())),
	}
	for _, vv := range om.Values() {
		v.Vals = append(v.Vals, vv)
	}
	return v
}

// Decoder

type OrderedMapDecoder struct {
	ext.DecoderCommon
}

var _ ext.Decoder = (*OrderedMapDecoder)(nil)

func (d *OrderedMapDecoder) Code() int8 {
	return orderedMapExtCode
}

func (d *OrderedMapDecoder) IsType(offset int, data *[]byte) bool {
	code, offset := d.ReadSize1(offset, data)
	if code == def.Ext8 {
		_, offset = d.ReadSize1(offset, data)
		t, _ := d.ReadSize1(offset, data)
		return int8(t) == d.Code()
	}
	return false
}

func (d *OrderedMapDecoder) AsValue(offset int, k reflect.Kind, data *[]byte) (any, int, error) {
	code, offset := d.ReadSize1(offset, data)

	switch code {
	case def.Ext8:
		size, offset := d.ReadSize1(offset, data)
		_, offset = d.ReadSize1(offset, data)
		extData, offset := d.ReadSizeN(offset, int(size), data)

		var iom internalOrderedMap
		err := msgpack.Unmarshal(extData, &iom)
		if err != nil {
			return nil, 0, fmt.Errorf("failed to unmarshal ordered map data: %w", err)
		}

		return iom.ToOrderedMap(), offset, nil
	}
	return nil, 0, fmt.Errorf("should not reach this line!! code %x decoding %v", d.Code(), k)
}

// StreamDecoder

type OrderedMapStreamDecoder struct{}

var _ ext.StreamDecoder = (*OrderedMapStreamDecoder)(nil)

func (d *OrderedMapStreamDecoder) Code() int8 {
	return orderedMapExtCode
}

func (d *OrderedMapStreamDecoder) IsType(code byte, innerType int8, _ int) bool {
	return code == def.Ext8 && innerType == d.Code()
}

func (d *OrderedMapStreamDecoder) ToValue(code byte, data []byte, k reflect.Kind) (any, error) {
	if code == def.Ext8 {
		var iom internalOrderedMap
		err := msgpack.Unmarshal(data, &iom)
		if err != nil {
			return nil, fmt.Errorf("failed to unmarshal ordered map data: %w", err)
		}

		return iom.ToOrderedMap(), nil
	}
	return nil, fmt.Errorf("should not reach this line!! code %x decoding %v", d.Code(), k)
}

// Encoder

type OrderedMapEncoder struct {
	ext.EncoderCommon
}

var _ ext.Encoder = (*OrderedMapEncoder)(nil)

func (e *OrderedMapEncoder) Code() int8 {
	return orderedMapExtCode
}

func (e *OrderedMapEncoder) Type() reflect.Type {
	return reflect.TypeOf(orderedmap.OrderedMap{})
}

func (e *OrderedMapEncoder) CalcByteSize(value reflect.Value) (int, error) {
	om := value.Interface().(orderedmap.OrderedMap)
	v := newInternalMap(om)

	data, err := msgpack.Marshal(v)
	if err != nil {
		return 0, err
	}

	return def.Byte1 + def.Byte1 + def.Byte1 + len(data), nil
}

func (e *OrderedMapEncoder) WriteToBytes(value reflect.Value, offset int, bytes *[]byte) int {
	om := value.Interface().(orderedmap.OrderedMap)
	v := newInternalMap(om)
	data, _ := msgpack.Marshal(v)

	offset = e.SetByte1Int(def.Ext8, offset, bytes)
	offset = e.SetByte1Int(len(data), offset, bytes)
	offset = e.SetByte1Int(int(e.Code()), offset, bytes)
	offset = e.SetBytes(data, offset, bytes)
	return offset
}

// StreamEncoder

type OrderedMapStreamEncoder struct{}

var _ ext.StreamEncoder = (*OrderedMapStreamEncoder)(nil)

func (e *OrderedMapStreamEncoder) Code() int8 {
	return orderedMapExtCode
}

func (e *OrderedMapStreamEncoder) Type() reflect.Type {
	return reflect.TypeOf(orderedmap.OrderedMap{})
}

func (e *OrderedMapStreamEncoder) Write(w ext.StreamWriter, value reflect.Value) error {
	om := value.Interface().(orderedmap.OrderedMap)
	v := newInternalMap(om)

	data, err := msgpack.Marshal(v)
	if err != nil {
		return err
	}

	if err := w.WriteByte1Int(def.Ext8); err != nil {
		return err
	}
	if err := w.WriteByte1Int(len(data)); err != nil {
		return err
	}
	if err := w.WriteByte1Int(int(e.Code())); err != nil {
		return err
	}
	if err := w.WriteBytes(data); err != nil {
		return err
	}

	return nil
}

shamaton avatar Nov 19 '25 23:11 shamaton

Thanks for your patient responding! I will try it later as i am busy with other projects.

MejiroRina avatar Nov 20 '25 00:11 MejiroRina