diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e987a2..f4cc3ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add `MarshalJSON` and `UnmarshalJSON` method to `atomic.Pointer[T]` type allowing users to use pointer with json. +- Add `MarshalJSON` and `UnmarshalJSON` methods to `atomic.Time` so it +encodes and decodes the wrapped `time.Time` instead of an empty struct. ## [1.11.0] - 2023-05-02 ### Fixed diff --git a/time_ext.go b/time_ext.go index 1e3dc97..830fc97 100644 --- a/time_ext.go +++ b/time_ext.go @@ -20,7 +20,10 @@ package atomic -import "time" +import ( + "encoding/json" + "time" +) //go:generate bin/gen-atomicwrapper -name=Time -type=time.Time -wrapped=Value -pack=packTime -unpack=unpackTime -imports time -file=time.go @@ -34,3 +37,18 @@ func unpackTime(v interface{}) time.Time { } return time.Time{} } + +// MarshalJSON encodes the wrapped time.Time into JSON. +func (x *Time) MarshalJSON() ([]byte, error) { + return json.Marshal(x.Load()) +} + +// UnmarshalJSON decodes a time.Time from JSON. +func (x *Time) UnmarshalJSON(b []byte) error { + var v time.Time + if err := json.Unmarshal(b, &v); err != nil { + return err + } + x.Store(v) + return nil +} diff --git a/time_test.go b/time_test.go index 83ac022..87fd131 100644 --- a/time_test.go +++ b/time_test.go @@ -21,6 +21,7 @@ package atomic import ( + "encoding/json" "testing" "time" @@ -48,6 +49,55 @@ func TestTimeLocation(t *testing.T) { assert.Equal(t, ny, atom.Load().Location(), "Location information is wrong") } +func TestTimeJSON(t *testing.T) { + start := time.Date(2021, 6, 17, 9, 10, 0, 0, time.UTC) + atom := NewTime(start) + + t.Run("MarshalJSON", func(t *testing.T) { + marshalled, err := atom.MarshalJSON() + require.NoError(t, err) + + // Compare against time.Time's own JSON encoding so we stay aligned with + // the standard library regardless of how it chooses to format times. + wanted, err := json.Marshal(start) + require.NoError(t, err) + assert.Equal(t, string(wanted), string(marshalled)) + }) + + t.Run("UnmarshalJSON", func(t *testing.T) { + raw, err := json.Marshal(start) + require.NoError(t, err) + + var got Time + require.NoError(t, got.UnmarshalJSON(raw)) + assert.Equal(t, start.UTC(), got.Load().UTC()) + }) + + t.Run("RoundTripInsideStruct", func(t *testing.T) { + // This is the case from #124: atomic.Time was being silently encoded + // as `{}` because it had no MarshalJSON. + type Container struct { + Plain time.Time + Atomic *Time + AtomicNil *Time + AtomicZero *Time + } + c := Container{ + Plain: start, + Atomic: atom, + AtomicZero: NewTime(time.Time{}), + } + + blob, err := json.Marshal(c) + require.NoError(t, err) + + var decoded Container + require.NoError(t, json.Unmarshal(blob, &decoded)) + assert.Equal(t, c.Plain.UTC(), decoded.Plain.UTC()) + assert.Equal(t, c.Atomic.Load().UTC(), decoded.Atomic.Load().UTC()) + }) +} + func TestLargeTime(t *testing.T) { // Check "large/small" time that are beyond int64 ns // representation (< year 1678 or > year 2262) can be