Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
[![codecov](https://codecov.io/gh/shamaton/msgpack/branch/master/graph/badge.svg?token=9PD2JUK5V3)](https://codecov.io/gh/shamaton/msgpack)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fshamaton%2Fmsgpack.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fshamaton%2Fmsgpack?ref=badge_shield)

## 📣 Announcement: `time.Time` decoding defaults to **UTC** in v3
Starting with **v3.0.0**, when decoding MessagePack **Timestamp** into Go’s `time.Time`,
the default `Location` will be **UTC** (previously `Local`). The instant is unchanged.
To keep the old behavior, use `SetDecodedTimeAsLocal()`.

## Features
* Supported types : primitive / array / slice / struct / map / interface{} and time.Time
* Renaming fields via `msgpack:"field_name"`
Expand Down Expand Up @@ -60,6 +65,51 @@ func handle(w http.ResponseWriter, r *http.Request) {
}
```

## 📣 Announcement: `time.Time` decoding defaults to **UTC** in v3

**TL;DR:** Starting with **v3.0.0**, when decoding MessagePack **Timestamp** into Go’s `time.Time`, the default `Location` will be **UTC** (previously `Local`). The **instant** is unchanged—only the display/location changes. This avoids host-dependent differences and aligns with common distributed systems practice.

### What is changing?

* **Before (v2.x):** Decoded `time.Time` defaults to `Local`.
* **After (v3.0.0):** Decoded `time.Time` defaults to **UTC**.

MessagePack’s Timestamp encodes an **instant** (epoch seconds + nanoseconds) and does **not** carry timezone info. Your data’s point in time is the same; only `time.Time.Location()` differs.

### Why?

* Eliminate environment-dependent behavior (e.g., different hosts showing different local zones).
* Make “UTC by default” the safe, predictable baseline for logs, APIs, and distributed apps.

### Who is affected?

* Apps that **display local time** without explicitly converting from UTC.
* If your code already normalizes to UTC or explicitly sets a location, you’re likely unaffected.

### Keep the old behavior (Local)

If you want the v2 behavior on v3:

```go
msgpack.SetDecodedTimeAsLocal()
```

Or convert after the fact:

```go
var t time.Time
_ = msgpack.Unmarshal(data, &t)
t = t.In(time.Local)
```

### Preview the new behavior on v2 (optional)

You can opt into UTC today on v2.x:

```go
msgpack.SetDecodedTimeAsUTC()
```

## Benchmark
This result made from [shamaton/msgpack_bench](https://github.com/shamaton/msgpack_bench)

Expand Down
11 changes: 11 additions & 0 deletions msgpack.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/shamaton/msgpack/v2/internal/encoding"
streamdecoding "github.com/shamaton/msgpack/v2/internal/stream/decoding"
streamencoding "github.com/shamaton/msgpack/v2/internal/stream/encoding"
"github.com/shamaton/msgpack/v2/time"
)

// StructAsArray is encoding option.
Expand Down Expand Up @@ -82,3 +83,13 @@ func RemoveExtStreamCoder(e ext.StreamEncoder, d ext.StreamDecoder) error {
func SetComplexTypeCode(code int8) {
def.SetComplexTypeCode(code)
}

// SetDecodedTimeAsUTC sets decoded time.Time values to UTC timezone.
func SetDecodedTimeAsUTC() {
time.SetDecodedAsLocal(false)
}

// SetDecodedTimeAsLocal sets decoded time.Time values to local timezone.
func SetDecodedTimeAsLocal() {
time.SetDecodedAsLocal(true)
}
41 changes: 41 additions & 0 deletions msgpack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/shamaton/msgpack/v2"
"github.com/shamaton/msgpack/v2/def"
"github.com/shamaton/msgpack/v2/ext"
tu "github.com/shamaton/msgpack/v2/internal/common/testutil"
extTime "github.com/shamaton/msgpack/v2/time"
)

Expand Down Expand Up @@ -1370,6 +1371,46 @@ func TestTime(t *testing.T) {
ErrorContains(t, err, "should not reach this line")
})
})

loc := time.Local
utc8 := time.FixedZone("UTC-8", -8*60*60)

time.Local = utc8
defer func() { time.Local = loc }()

t.Run("UTC", func(t *testing.T) {
msgpack.SetDecodedTimeAsUTC()
args := []encdecArg[time.Time]{
{
n: "case",
v: time.Unix(now.Unix(), 0),
vc: func(r time.Time) error {
tu.Equal(t, now.Unix(), r.Unix())
tu.Equal(t, r.Location(), time.UTC)
return nil
},
skipEq: true, // skip equal check because of location difference
},
}
encdec(t, args...)
})

t.Run("Local", func(t *testing.T) {
msgpack.SetDecodedTimeAsLocal()
args := []encdecArg[time.Time]{
{
n: "case",
v: time.Unix(now.Unix(), 0),
vc: func(r time.Time) error {
tu.Equal(t, r.Location(), time.Local)
tu.Equal(t, r.Location(), utc8)
return nil
},
},
}
encdec(t, args...)
})

}

func TestMap(t *testing.T) {
Expand Down
22 changes: 17 additions & 5 deletions time/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,25 @@ func (td *timeDecoder) AsValue(offset int, k reflect.Kind, d *[]byte) (interface
case def.Fixext4:
_, offset = td.ReadSize1(offset, d)
bs, offset := td.ReadSize4(offset, d)
return time.Unix(int64(binary.BigEndian.Uint32(bs)), 0), offset, nil
v := time.Unix(int64(binary.BigEndian.Uint32(bs)), 0)
if decodeAsLocal {
return v, offset, nil
}
return v.UTC(), offset, nil

case def.Fixext8:
_, offset = td.ReadSize1(offset, d)
bs, offset := td.ReadSize8(offset, d)
data64 := binary.BigEndian.Uint64(bs)
nano := int64(data64 >> 34)
if nano > 999999999 {
return zero, 0, fmt.Errorf("In timestamp 64 formats, nanoseconds must not be larger than 999999999 : %d", nano)
return zero, 0, fmt.Errorf("in timestamp 64 formats, nanoseconds must not be larger than 999999999 : %d", nano)
}
v := time.Unix(int64(data64&0x00000003ffffffff), nano)
if decodeAsLocal {
return v, offset, nil
}
return time.Unix(int64(data64&0x00000003ffffffff), nano), offset, nil
return v.UTC(), offset, nil

case def.Ext8:
_, offset = td.ReadSize1(offset, d)
Expand All @@ -67,10 +75,14 @@ func (td *timeDecoder) AsValue(offset int, k reflect.Kind, d *[]byte) (interface
secbs, offset := td.ReadSize8(offset, d)
nano := binary.BigEndian.Uint32(nanobs)
if nano > 999999999 {
return zero, 0, fmt.Errorf("In timestamp 96 formats, nanoseconds must not be larger than 999999999 : %d", nano)
return zero, 0, fmt.Errorf("in timestamp 96 formats, nanoseconds must not be larger than 999999999 : %d", nano)
}
sec := binary.BigEndian.Uint64(secbs)
return time.Unix(int64(sec), int64(nano)), offset, nil
v := time.Unix(int64(sec), int64(nano))
if decodeAsLocal {
return v, offset, nil
}
return v.UTC(), offset, nil
}

return zero, 0, fmt.Errorf("should not reach this line!! code %x decoding %v", code, k)
Expand Down
18 changes: 15 additions & 3 deletions time/decode_stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,35 @@ func (td *timeStreamDecoder) IsType(code byte, innerType int8, dataLength int) b
func (td *timeStreamDecoder) ToValue(code byte, data []byte, k reflect.Kind) (interface{}, error) {
switch code {
case def.Fixext4:
return time.Unix(int64(binary.BigEndian.Uint32(data)), 0), nil
v := time.Unix(int64(binary.BigEndian.Uint32(data)), 0)
if decodeAsLocal {
return v, nil
}
return v.UTC(), nil

case def.Fixext8:
data64 := binary.BigEndian.Uint64(data)
nano := int64(data64 >> 34)
if nano > 999999999 {
return zero, fmt.Errorf("in timestamp 64 formats, nanoseconds must not be larger than 999999999 : %d", nano)
}
return time.Unix(int64(data64&0x00000003ffffffff), nano), nil
v := time.Unix(int64(data64&0x00000003ffffffff), nano)
if decodeAsLocal {
return v, nil
}
return v.UTC(), nil

case def.Ext8:
nano := binary.BigEndian.Uint32(data[:4])
if nano > 999999999 {
return zero, fmt.Errorf("in timestamp 96 formats, nanoseconds must not be larger than 999999999 : %d", nano)
}
sec := binary.BigEndian.Uint64(data[4:12])
return time.Unix(int64(sec), int64(nano)), nil
v := time.Unix(int64(sec), int64(nano))
if decodeAsLocal {
return v, nil
}
return v.UTC(), nil
}

return zero, fmt.Errorf("should not reach this line!! code %x decoding %v", code, k)
Expand Down
Loading
Loading