Handling Unix Timestamps in JSON

October 30, 2019
golang | pattern | json

A while ago, I was integrating with a REST API which returned JSON. The responses looked as follows:

{
    "status": "ok",
    "last_check": 1572428388
}

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:

package main

import (
    "encoding/json"

    "github.com/pieterclaerhout/go-log"
)

const jsonResponse = `{
    "status": "ok",
    "last_check": 1572428388
}`

type Response struct {
    Status    string `json:"status"`
    LastCheck int64  `json:"last_check"`
}

func main() {

    var r Response
    if err := json.Unmarshal([]byte(jsonResponse), &r); err != nil {
        log.Error(err)
        return
    }

    log.InfoDump(r, "r")

}

Running this outputs:

r main.Response{
  Status: "ok",
  LastCheck: 1572428388,
}

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:

package main

import (
    "encoding/json"
    "time"

    "github.com/pieterclaerhout/go-log"
)

const jsonResponse = `{
    "status": "ok",
    "last_check": 1572428388
}`

type Response struct {
    Status    string `json:"status"`
    LastCheck int64  `json:"last_check"`
}

func main() {

    var r Response
    if err := json.Unmarshal([]byte(jsonResponse), &r); err != nil {
        log.Error(err)
        return
    }

    log.InfoDump(r, "r")

    timestamp := time.Unix(r.LastCheck, 0)
    log.InfoDump(timestamp.String(), "timestamp")

}

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:

package main

import (
    "strconv"
    "time"
)

// Time defines a timestamp encoded as epoch seconds in JSON
type Time time.Time

// MarshalJSON is used to convert the timestamp to JSON
func (t Time) MarshalJSON() ([]byte, error) {
    return []byte(strconv.FormatInt(time.Time(t).Unix(), 10)), nil
}

// UnmarshalJSON is used to convert the timestamp from JSON
func (t *Time) UnmarshalJSON(s []byte) (err error) {
    r := string(s)
    q, err := strconv.ParseInt(r, 10, 64)
    if err != nil {
        return err
    }
    *(*time.Time)(t) = time.Unix(q, 0)
    return nil
}


// Unix returns t as a Unix time, the number of seconds elapsed
// since January 1, 1970 UTC. The result does not depend on the
// location associated with t.
func (t Time) Unix() int64 {
    return time.Time(t).Unix()
}

// Time returns the JSON time as a time.Time instance in UTC
func (t Time) Time() time.Time {
    return time.Time(t).UTC()
}

// String returns t as a formatted string
func (t Time) String() string {
    return t.Time().String()
}

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:

package main

import (
    "encoding/json"
    "time"

    "github.com/pieterclaerhout/go-log"
)

const jsonResponse = `{
    "status": "ok",
    "last_check": 1572428388
}`

type Response struct {
    Status    string `json:"status"`
    LastCheck Time  `json:"last_check"`
}

func main() {

    var r Response
    if err := json.Unmarshal([]byte(jsonResponse), &r); err != nil {
        log.Error(err)
        return
    }

    log.InfoDump(r, "r")
    log.InfoDump(r.LastCheck.String(), "r.LastCheck")

}

This will now output:

r main.ImprovedResponse{
  Status: "ok",
  LastCheck: main.Time{},
}
r.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:

package main

import (
    "encoding/json"
    "time"

    "github.com/pieterclaerhout/go-log"
)

const jsonResponse = `{
    "status": "ok",
    "last_check": 1572428388
}`

type Response struct {
    Status    string `json:"status"`
    LastCheck Time  `json:"last_check"`
}

func main() {

    var r Response
    if err := json.Unmarshal([]byte(jsonResponse), &r); err != nil {
        log.Error(err)
        return
    }

    jsonBytes, err := json.MarshalIndent(rImproved, "", "  ")
    if err != nil {
        log.Error(err)
        return
    }

    log.Info(string(jsonBytes))

}

Running this results in:

{
  "status": "ok",
  "last_check": 1572428388
}

You can find the complete example here.