A while ago, I was integrating with a REST API which returned JSON. The responses looked as follows:
1{
2 "status": "ok",
3 "last_check": 1572428388
4}
The status
field is a string value, the last_check
field is a unix timestamp. A unix timestamp is an integer indicating the number of seconds since 00:00:00 UTC on 1 January 1970.
Initially, I was writing the parsing as follows:
1package main
2
3import (
4 "encoding/json"
5
6 "github.com/pieterclaerhout/go-log"
7)
8
9const jsonResponse = `{
10 "status": "ok",
11 "last_check": 1572428388
12}`
13
14type Response struct {
15 Status string `json:"status"`
16 LastCheck int64 `json:"last_check"`
17}
18
19func main() {
20
21 var r Response
22 if err := json.Unmarshal([]byte(jsonResponse), &r); err != nil {
23 log.Error(err)
24 return
25 }
26
27 log.InfoDump(r, "r")
28
29}
Running this outputs:
1r main.Response{
2 Status: "ok",
3 LastCheck: 1572428388,
4}
My first reaction was, this can be done better. The unix timestamp isn't very Go-like and it would be much easier to have a time.Time
instance instead. Nothing prevents us from doing the conversion manually using time.Unix
:
1package main
2
3import (
4 "encoding/json"
5 "time"
6
7 "github.com/pieterclaerhout/go-log"
8)
9
10const jsonResponse = `{
11 "status": "ok",
12 "last_check": 1572428388
13}`
14
15type Response struct {
16 Status string `json:"status"`
17 LastCheck int64 `json:"last_check"`
18}
19
20func main() {
21
22 var r Response
23 if err := json.Unmarshal([]byte(jsonResponse), &r); err != nil {
24 log.Error(err)
25 return
26 }
27
28 log.InfoDump(r, "r")
29
30 timestamp := time.Unix(r.LastCheck, 0)
31 log.InfoDump(timestamp.String(), "timestamp")
32
33}
Already slightly better, but when you have many of these, it becomes cumbersome. The best solution would be that during the the unmarshal of the JSON, we can convert the timestamps directly into a time.Time
instance. There is (as usual) a neat way of handling this in Go. The trick is to define a custom type and implement MarshalJSON
and UnmarshalJSON
.
To do this, let's define a type called Time
which does just this:
1package main
2
3import (
4 "strconv"
5 "time"
6)
7
8// Time defines a timestamp encoded as epoch seconds in JSON
9type Time time.Time
10
11// MarshalJSON is used to convert the timestamp to JSON
12func (t Time) MarshalJSON() ([]byte, error) {
13 return []byte(strconv.FormatInt(time.Time(t).Unix(), 10)), nil
14}
15
16// UnmarshalJSON is used to convert the timestamp from JSON
17func (t *Time) UnmarshalJSON(s []byte) (err error) {
18 r := string(s)
19 q, err := strconv.ParseInt(r, 10, 64)
20 if err != nil {
21 return err
22 }
23 *(*time.Time)(t) = time.Unix(q, 0)
24 return nil
25}
26
27
28// Unix returns t as a Unix time, the number of seconds elapsed
29// since January 1, 1970 UTC. The result does not depend on the
30// location associated with t.
31func (t Time) Unix() int64 {
32 return time.Time(t).Unix()
33}
34
35// Time returns the JSON time as a time.Time instance in UTC
36func (t Time) Time() time.Time {
37 return time.Time(t).UTC()
38}
39
40// String returns t as a formatted string
41func (t Time) String() string {
42 return t.Time().String()
43}
In the UnmarshalJSON
function, we are receiving the value as a raw byte slice. We are parsing it as a string and then convert it into a time.Time
instance. We are then replacing the t
variable with the time value.
We can also do the opposite by using MarshalJSON
. This is useful if we want to convert our object back to JSON so that this works in two ways.
I also added some convenience functions so that the new type works pretty much like a native time.Time
instance.
We can now update our program to:
1package main
2
3import (
4 "encoding/json"
5 "time"
6
7 "github.com/pieterclaerhout/go-log"
8)
9
10const jsonResponse = `{
11 "status": "ok",
12 "last_check": 1572428388
13}`
14
15type Response struct {
16 Status string `json:"status"`
17 LastCheck Time `json:"last_check"`
18}
19
20func main() {
21
22 var r Response
23 if err := json.Unmarshal([]byte(jsonResponse), &r); err != nil {
24 log.Error(err)
25 return
26 }
27
28 log.InfoDump(r, "r")
29 log.InfoDump(r.LastCheck.String(), "r.LastCheck")
30
31}
This will now output:
1r main.ImprovedResponse{
2 Status: "ok",
3 LastCheck: main.Time{},
4}
5r.LastCheck "2019-10-30 09:39:48 +0000 UTC"
The solution also works in the other direction when you convert the object back to JSON:
1package main
2
3import (
4 "encoding/json"
5 "time"
6
7 "github.com/pieterclaerhout/go-log"
8)
9
10const jsonResponse = `{
11 "status": "ok",
12 "last_check": 1572428388
13}`
14
15type Response struct {
16 Status string `json:"status"`
17 LastCheck Time `json:"last_check"`
18}
19
20func main() {
21
22 var r Response
23 if err := json.Unmarshal([]byte(jsonResponse), &r); err != nil {
24 log.Error(err)
25 return
26 }
27
28 jsonBytes, err := json.MarshalIndent(rImproved, "", " ")
29 if err != nil {
30 log.Error(err)
31 return
32 }
33
34 log.Info(string(jsonBytes))
35
36}
Running this results in:
1{
2 "status": "ok",
3 "last_check": 1572428388
4}
You can find the complete example here.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.