From be64e92530e52a862671e6e443767032e736ba05 Mon Sep 17 00:00:00 2001 From: Ignacio Van Droogenbroeck Date: Mon, 2 Mar 2026 15:02:49 -0600 Subject: [PATCH] fix(decode): choose TextUnmarshaler over BinaryUnmarshaler for str format (#10) When a type implements both interfaces, the decoder now peeks at the wire format and dispatches to TextUnmarshaler for str codes and BinaryUnmarshaler for bin codes. --- decode_value.go | 22 ++++++++++++++++++++++ types_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/decode_value.go b/decode_value.go index 445532c..47c81fd 100644 --- a/decode_value.go +++ b/decode_value.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "reflect" + + "github.com/vmihailenco/msgpack/v5/msgpcode" ) var ( @@ -71,6 +73,9 @@ func _getDecoder(typ reflect.Type) decoderFunc { return nilAwareDecoder(typ, unmarshalValue) } if typ.Implements(binaryUnmarshalerType) { + if typ.Implements(textUnmarshalerType) { + return nilAwareDecoder(typ, unmarshalBinaryOrTextValue) + } return nilAwareDecoder(typ, unmarshalBinaryValue) } if typ.Implements(textUnmarshalerType) { @@ -87,6 +92,9 @@ func _getDecoder(typ reflect.Type) decoderFunc { return addrDecoder(nilAwareDecoder(typ, unmarshalValue)) } if ptr.Implements(binaryUnmarshalerType) { + if ptr.Implements(textUnmarshalerType) { + return addrDecoder(nilAwareDecoder(typ, unmarshalBinaryOrTextValue)) + } return addrDecoder(nilAwareDecoder(typ, unmarshalBinaryValue)) } if ptr.Implements(textUnmarshalerType) { @@ -238,6 +246,20 @@ func unmarshalValue(d *Decoder, v reflect.Value) error { return unmarshaler.UnmarshalMsgpack(b) } +// unmarshalBinaryOrTextValue peeks at the wire format to choose between +// BinaryUnmarshaler (bin) and TextUnmarshaler (str). Used when a type +// implements both interfaces. +func unmarshalBinaryOrTextValue(d *Decoder, v reflect.Value) error { + c, err := d.PeekCode() + if err != nil { + return err + } + if msgpcode.IsString(c) { + return unmarshalTextValue(d, v) + } + return unmarshalBinaryValue(d, v) +} + func unmarshalBinaryValue(d *Decoder, v reflect.Value) error { data, err := d.DecodeBytes() if err != nil { diff --git a/types_test.go b/types_test.go index 46a051c..33d001a 100644 --- a/types_test.go +++ b/types_test.go @@ -1294,6 +1294,40 @@ func TestPoolReleasesOversizedBuffers(t *testing.T) { require.Equal(t, "small", s) } +// Issue #10: types implementing both TextUnmarshaler and BinaryUnmarshaler +// should use TextUnmarshaler when wire format is str. +type dualUnmarshaler struct { + text string + binary []byte +} + +func (d *dualUnmarshaler) MarshalText() ([]byte, error) { return []byte(d.text), nil } +func (d *dualUnmarshaler) UnmarshalText(b []byte) error { d.text = string(b); return nil } +func (d *dualUnmarshaler) MarshalBinary() ([]byte, error) { return d.binary, nil } +func (d *dualUnmarshaler) UnmarshalBinary(b []byte) error { d.binary = b; return nil } + +func TestTextUnmarshalerWithStrFormat(t *testing.T) { + // Encode a plain string (str format) and decode into a type that + // implements both BinaryUnmarshaler and TextUnmarshaler. + b, err := msgpack.Marshal("hello") + require.NoError(t, err) + + var du dualUnmarshaler + require.NoError(t, msgpack.Unmarshal(b, &du)) + // Should have used TextUnmarshaler because the wire format is str. + require.Equal(t, "hello", du.text) + require.Nil(t, du.binary) + + // Encode as binary (bin format) and decode — should use BinaryUnmarshaler. + b, err = msgpack.Marshal([]byte{0xDE, 0xAD}) + require.NoError(t, err) + + var du2 dualUnmarshaler + require.NoError(t, msgpack.Unmarshal(b, &du2)) + require.Equal(t, []byte{0xDE, 0xAD}, du2.binary) + require.Equal(t, "", du2.text) +} + func mustParseTime(format, s string) time.Time { tm, err := time.Parse(format, s) if err != nil {