#development #golang #pattern

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.