Skip to content

Commit cf206ed

Browse files
authored
atomic.Time: add MarshalJSON / UnmarshalJSON (#208)
atomic.Time has no JSON methods, so json.Marshal(t) returns "{}" and the wrapped time is lost on the wire. Other wrapped types in this package (Duration, Bool, Pointer) all have these methods. Adding them to Time fixes #124. Implementation mirrors duration.go (delegate to json.Marshal / json.Unmarshal on the wrapped value). Did this in time_ext.go rather than the generated time.go so I didn't have to touch the gen-atomic template just for one type, but the long-term home is probably re-running gen-atomicwrapper with -json. Added TestTimeJSON covering MarshalJSON, UnmarshalJSON, and a struct round-trip that demonstrates the issue from the report. Signed-off-by: Charlie Tonneslan <cst0520@gmail.com>
1 parent c8c4698 commit cf206ed

3 files changed

Lines changed: 71 additions & 1 deletion

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
### Added
99
- Add `MarshalJSON` and `UnmarshalJSON` method to `atomic.Pointer[T]` type
1010
allowing users to use pointer with json.
11+
- Add `MarshalJSON` and `UnmarshalJSON` methods to `atomic.Time` so it
12+
encodes and decodes the wrapped `time.Time` instead of an empty struct.
1113

1214
### Updated
1315
- Bumped https://github.com/stretchr/testify to v1.12.1.

‎time_ext.go‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020

2121
package atomic
2222

23-
import "time"
23+
import (
24+
"encoding/json"
25+
"time"
26+
)
2427

2528
//go:generate bin/gen-atomicwrapper -name=Time -type=time.Time -wrapped=Value -pack=packTime -unpack=unpackTime -imports time -file=time.go
2629

@@ -34,3 +37,18 @@ func unpackTime(v interface{}) time.Time {
3437
}
3538
return time.Time{}
3639
}
40+
41+
// MarshalJSON encodes the wrapped time.Time into JSON.
42+
func (x *Time) MarshalJSON() ([]byte, error) {
43+
return json.Marshal(x.Load())
44+
}
45+
46+
// UnmarshalJSON decodes a time.Time from JSON.
47+
func (x *Time) UnmarshalJSON(b []byte) error {
48+
var v time.Time
49+
if err := json.Unmarshal(b, &v); err != nil {
50+
return err
51+
}
52+
x.Store(v)
53+
return nil
54+
}

‎time_test.go‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
package atomic
2222

2323
import (
24+
"encoding/json"
2425
"testing"
2526
"time"
2627

@@ -48,6 +49,55 @@ func TestTimeLocation(t *testing.T) {
4849
assert.Equal(t, ny, atom.Load().Location(), "Location information is wrong")
4950
}
5051

52+
func TestTimeJSON(t *testing.T) {
53+
start := time.Date(2021, 6, 17, 9, 10, 0, 0, time.UTC)
54+
atom := NewTime(start)
55+
56+
t.Run("MarshalJSON", func(t *testing.T) {
57+
marshalled, err := atom.MarshalJSON()
58+
require.NoError(t, err)
59+
60+
// Compare against time.Time's own JSON encoding so we stay aligned with
61+
// the standard library regardless of how it chooses to format times.
62+
wanted, err := json.Marshal(start)
63+
require.NoError(t, err)
64+
assert.Equal(t, string(wanted), string(marshalled))
65+
})
66+
67+
t.Run("UnmarshalJSON", func(t *testing.T) {
68+
raw, err := json.Marshal(start)
69+
require.NoError(t, err)
70+
71+
var got Time
72+
require.NoError(t, got.UnmarshalJSON(raw))
73+
assert.Equal(t, start.UTC(), got.Load().UTC())
74+
})
75+
76+
t.Run("RoundTripInsideStruct", func(t *testing.T) {
77+
// This is the case from #124: atomic.Time was being silently encoded
78+
// as `{}` because it had no MarshalJSON.
79+
type Container struct {
80+
Plain time.Time
81+
Atomic *Time
82+
AtomicNil *Time
83+
AtomicZero *Time
84+
}
85+
c := Container{
86+
Plain: start,
87+
Atomic: atom,
88+
AtomicZero: NewTime(time.Time{}),
89+
}
90+
91+
blob, err := json.Marshal(c)
92+
require.NoError(t, err)
93+
94+
var decoded Container
95+
require.NoError(t, json.Unmarshal(blob, &decoded))
96+
assert.Equal(t, c.Plain.UTC(), decoded.Plain.UTC())
97+
assert.Equal(t, c.Atomic.Load().UTC(), decoded.Atomic.Load().UTC())
98+
})
99+
}
100+
51101
func TestLargeTime(t *testing.T) {
52102
// Check "large/small" time that are beyond int64 ns
53103
// representation (< year 1678 or > year 2262) can be

0 commit comments

Comments
 (0)