We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
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.
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.