Skip to content

Commit 9cce059

Browse files
authored
Merge pull request #2216 from pconstantinou/master
Timestamp incorrectly adds 'Z' when serializing from JSON to indicate GMT, fixes bug #2215
2 parents 2c1b1c3 + 3c640a4 commit 9cce059

File tree

2 files changed

+53
-11
lines changed

2 files changed

+53
-11
lines changed

pgtype/timestamp.go

+17-8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
)
1313

1414
const pgTimestampFormat = "2006-01-02 15:04:05.999999999"
15+
const jsonISO8601 = "2006-01-02T15:04:05.999999999"
1516

1617
type TimestampScanner interface {
1718
ScanTimestamp(v Timestamp) error
@@ -76,7 +77,7 @@ func (ts Timestamp) MarshalJSON() ([]byte, error) {
7677

7778
switch ts.InfinityModifier {
7879
case Finite:
79-
s = ts.Time.Format(time.RFC3339Nano)
80+
s = ts.Time.Format(jsonISO8601)
8081
case Infinity:
8182
s = "infinity"
8283
case NegativeInfinity:
@@ -104,15 +105,23 @@ func (ts *Timestamp) UnmarshalJSON(b []byte) error {
104105
case "-infinity":
105106
*ts = Timestamp{Valid: true, InfinityModifier: -Infinity}
106107
default:
107-
// PostgreSQL uses ISO 8601 wihout timezone for to_json function and casting from a string to timestampt
108-
tim, err := time.Parse(time.RFC3339Nano, *s+"Z")
109-
if err != nil {
110-
return err
108+
// Parse time with or without timezonr
109+
tss := *s
110+
// PostgreSQL uses ISO 8601 without timezone for to_json function and casting from a string to timestampt
111+
tim, err := time.Parse(time.RFC3339Nano, tss)
112+
if err == nil {
113+
*ts = Timestamp{Time: tim, Valid: true}
114+
return nil
111115
}
112-
113-
*ts = Timestamp{Time: tim, Valid: true}
116+
tim, err = time.ParseInLocation(jsonISO8601, tss, time.UTC)
117+
if err == nil {
118+
*ts = Timestamp{Time: tim, Valid: true}
119+
return nil
120+
}
121+
ts.Valid = false
122+
return fmt.Errorf("cannot unmarshal %s to timestamp with layout %s or %s (%w)",
123+
*s, time.RFC3339Nano, jsonISO8601, err)
114124
}
115-
116125
return nil
117126
}
118127

pgtype/timestamp_test.go

+36-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package pgtype_test
22

33
import (
44
"context"
5+
"encoding/json"
56
"testing"
67
"time"
78

89
pgx "github.com/jackc/pgx/v5"
910
"github.com/jackc/pgx/v5/pgtype"
1011
"github.com/jackc/pgx/v5/pgxtest"
12+
"github.com/stretchr/testify/assert"
1113
"github.com/stretchr/testify/require"
1214
)
1315

@@ -100,13 +102,24 @@ func TestTimestampCodecDecodeTextInvalid(t *testing.T) {
100102
}
101103

102104
func TestTimestampMarshalJSON(t *testing.T) {
105+
106+
tsStruct := struct {
107+
TS pgtype.Timestamp `json:"ts"`
108+
}{}
109+
110+
tm := time.Date(2012, 3, 29, 10, 5, 45, 0, time.UTC)
111+
tsString := "\"" + tm.Format("2006-01-02T15:04:05") + "\"" // `"2012-03-29T10:05:45"`
112+
var pgt pgtype.Timestamp
113+
_ = pgt.Scan(tm)
114+
103115
successfulTests := []struct {
104116
source pgtype.Timestamp
105117
result string
106118
}{
107119
{source: pgtype.Timestamp{}, result: "null"},
108-
{source: pgtype.Timestamp{Time: time.Date(2012, 3, 29, 10, 5, 45, 0, time.UTC), Valid: true}, result: "\"2012-03-29T10:05:45Z\""},
109-
{source: pgtype.Timestamp{Time: time.Date(2012, 3, 29, 10, 5, 45, 555*1000*1000, time.UTC), Valid: true}, result: "\"2012-03-29T10:05:45.555Z\""},
120+
{source: pgtype.Timestamp{Time: tm, Valid: true}, result: tsString},
121+
{source: pgt, result: tsString},
122+
{source: pgtype.Timestamp{Time: tm.Add(time.Second * 555 / 1000), Valid: true}, result: `"2012-03-29T10:05:45.555"`},
110123
{source: pgtype.Timestamp{InfinityModifier: pgtype.Infinity, Valid: true}, result: "\"infinity\""},
111124
{source: pgtype.Timestamp{InfinityModifier: pgtype.NegativeInfinity, Valid: true}, result: "\"-infinity\""},
112125
}
@@ -116,12 +129,32 @@ func TestTimestampMarshalJSON(t *testing.T) {
116129
t.Errorf("%d: %v", i, err)
117130
}
118131

119-
if string(r) != tt.result {
132+
if !assert.Equal(t, tt.result, string(r)) {
120133
t.Errorf("%d: expected %v to convert to %v, but it was %v", i, tt.source, tt.result, string(r))
121134
}
135+
tsStruct.TS = tt.source
136+
b, err := json.Marshal(tsStruct)
137+
assert.NoErrorf(t, err, "failed to marshal %v %s", tt.source, err)
138+
t2 := tsStruct
139+
t2.TS = pgtype.Timestamp{} // Clear out the value so that we can compare after unmarshalling
140+
err = json.Unmarshal(b, &t2)
141+
assert.NoErrorf(t, err, "failed to unmarshal %v with %s", tt.source, err)
142+
assert.True(t, tsStruct.TS.Time.Unix() == t2.TS.Time.Unix())
122143
}
123144
}
124145

146+
func TestTimestampUnmarshalJSONErrors(t *testing.T) {
147+
tsStruct := struct {
148+
TS pgtype.Timestamp `json:"ts"`
149+
}{}
150+
goodJson1 := []byte(`{"ts":"2012-03-29T10:05:45"}`)
151+
assert.NoError(t, json.Unmarshal(goodJson1, &tsStruct))
152+
goodJson2 := []byte(`{"ts":"2012-03-29T10:05:45Z"}`)
153+
assert.NoError(t, json.Unmarshal(goodJson2, &tsStruct))
154+
badJson := []byte(`{"ts":"2012-03-29"}`)
155+
assert.Error(t, json.Unmarshal(badJson, &tsStruct))
156+
}
157+
125158
func TestTimestampUnmarshalJSON(t *testing.T) {
126159
successfulTests := []struct {
127160
source string

0 commit comments

Comments
 (0)