Skip to content

Commit

Permalink
Merge pull request #2216 from pconstantinou/master
Browse files Browse the repository at this point in the history
Timestamp incorrectly adds 'Z' when serializing from JSON to indicate GMT, fixes bug #2215
  • Loading branch information
jackc authored Jan 18, 2025
2 parents 2c1b1c3 + 3c640a4 commit 9cce059
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 11 deletions.
25 changes: 17 additions & 8 deletions pgtype/timestamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
)

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

type TimestampScanner interface {
ScanTimestamp(v Timestamp) error
Expand Down Expand Up @@ -76,7 +77,7 @@ func (ts Timestamp) MarshalJSON() ([]byte, error) {

switch ts.InfinityModifier {
case Finite:
s = ts.Time.Format(time.RFC3339Nano)
s = ts.Time.Format(jsonISO8601)
case Infinity:
s = "infinity"
case NegativeInfinity:
Expand Down Expand Up @@ -104,15 +105,23 @@ func (ts *Timestamp) UnmarshalJSON(b []byte) error {
case "-infinity":
*ts = Timestamp{Valid: true, InfinityModifier: -Infinity}
default:
// PostgreSQL uses ISO 8601 wihout timezone for to_json function and casting from a string to timestampt
tim, err := time.Parse(time.RFC3339Nano, *s+"Z")
if err != nil {
return err
// Parse time with or without timezonr
tss := *s
// PostgreSQL uses ISO 8601 without timezone for to_json function and casting from a string to timestampt
tim, err := time.Parse(time.RFC3339Nano, tss)
if err == nil {
*ts = Timestamp{Time: tim, Valid: true}
return nil
}

*ts = Timestamp{Time: tim, Valid: true}
tim, err = time.ParseInLocation(jsonISO8601, tss, time.UTC)
if err == nil {
*ts = Timestamp{Time: tim, Valid: true}
return nil
}
ts.Valid = false
return fmt.Errorf("cannot unmarshal %s to timestamp with layout %s or %s (%w)",
*s, time.RFC3339Nano, jsonISO8601, err)
}

return nil
}

Expand Down
39 changes: 36 additions & 3 deletions pgtype/timestamp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package pgtype_test

import (
"context"
"encoding/json"
"testing"
"time"

pgx "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxtest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -100,13 +102,24 @@ func TestTimestampCodecDecodeTextInvalid(t *testing.T) {
}

func TestTimestampMarshalJSON(t *testing.T) {

tsStruct := struct {
TS pgtype.Timestamp `json:"ts"`
}{}

tm := time.Date(2012, 3, 29, 10, 5, 45, 0, time.UTC)
tsString := "\"" + tm.Format("2006-01-02T15:04:05") + "\"" // `"2012-03-29T10:05:45"`
var pgt pgtype.Timestamp
_ = pgt.Scan(tm)

successfulTests := []struct {
source pgtype.Timestamp
result string
}{
{source: pgtype.Timestamp{}, result: "null"},
{source: pgtype.Timestamp{Time: time.Date(2012, 3, 29, 10, 5, 45, 0, time.UTC), Valid: true}, result: "\"2012-03-29T10:05:45Z\""},
{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\""},
{source: pgtype.Timestamp{Time: tm, Valid: true}, result: tsString},
{source: pgt, result: tsString},
{source: pgtype.Timestamp{Time: tm.Add(time.Second * 555 / 1000), Valid: true}, result: `"2012-03-29T10:05:45.555"`},
{source: pgtype.Timestamp{InfinityModifier: pgtype.Infinity, Valid: true}, result: "\"infinity\""},
{source: pgtype.Timestamp{InfinityModifier: pgtype.NegativeInfinity, Valid: true}, result: "\"-infinity\""},
}
Expand All @@ -116,12 +129,32 @@ func TestTimestampMarshalJSON(t *testing.T) {
t.Errorf("%d: %v", i, err)
}

if string(r) != tt.result {
if !assert.Equal(t, tt.result, string(r)) {
t.Errorf("%d: expected %v to convert to %v, but it was %v", i, tt.source, tt.result, string(r))
}
tsStruct.TS = tt.source
b, err := json.Marshal(tsStruct)
assert.NoErrorf(t, err, "failed to marshal %v %s", tt.source, err)
t2 := tsStruct
t2.TS = pgtype.Timestamp{} // Clear out the value so that we can compare after unmarshalling
err = json.Unmarshal(b, &t2)
assert.NoErrorf(t, err, "failed to unmarshal %v with %s", tt.source, err)
assert.True(t, tsStruct.TS.Time.Unix() == t2.TS.Time.Unix())
}
}

func TestTimestampUnmarshalJSONErrors(t *testing.T) {
tsStruct := struct {
TS pgtype.Timestamp `json:"ts"`
}{}
goodJson1 := []byte(`{"ts":"2012-03-29T10:05:45"}`)
assert.NoError(t, json.Unmarshal(goodJson1, &tsStruct))
goodJson2 := []byte(`{"ts":"2012-03-29T10:05:45Z"}`)
assert.NoError(t, json.Unmarshal(goodJson2, &tsStruct))
badJson := []byte(`{"ts":"2012-03-29"}`)
assert.Error(t, json.Unmarshal(badJson, &tsStruct))
}

func TestTimestampUnmarshalJSON(t *testing.T) {
successfulTests := []struct {
source string
Expand Down

0 comments on commit 9cce059

Please sign in to comment.