When we are building a JSON API, many times we have to deal with dates that we can’t have control of the layout.
Let’s see a common example using a format that Go knows by default.
type Checkin struct {
Timestamp time.Time `json:"timestamp"`
User string `json:"user"`
}
func main() {
j := `{"timestamp":"2016-11-02T08:18:20Z", "user":"John Doe"}`
var c Checkin
// error handling omitted for simplicity (don't do this).
json.Unmarshal([]byte(j), &c)
fmt.Println(c)
}
Here, we are using the RFC3339 format, so Go will do the correct unmarshal and fill the Timestamp field on the struct.
Let’s say, now, that we are consuming this information from a web service that uses a different date format.
func main() {
// using / to divide the date and space for the time
j := `{"timestamp":"2016/11/02 08:18:20", "user":"John Doe"}`
var c Checkin
err := json.Unmarshal([]byte(j), &c)
if err != nil {
fmt.Println(err)
}
fmt.Println(c)
}
When we run this code we will see this error:
parsing time ""2016/11/02 08:18:20"" as ""2006-01-02T15:04:05Z07:00"": cannot parse "/11/02 08:18:20"" as "-" {0001-01-01 00:00:00 +0000 UTC }
This is because Go doesn’t know how to handle this custom date format, so let’s write a custom date type to handle this special case.
The first thing to do is create a custom type for the date, for example, SpecialDate. This new type needs to implement the Unmarshaler interface.
type SpecialDate struct {
time.Time
}
Now, to implement the Unmarshaler interface, we need a function UnmarshalJSON on the SpecialDate type.
func (sd *SpecialDate) UnmarshalJSON(input []byte) error {
strInput := string(input)
strInput = strings.Trim(strInput, `"`)
newTime, err := time.Parse("2006/01/02 15:04:05", strInput)
if err != nil {
return err
}
sd.Time = newTime
return nil
}
First, we get the input that is a []byte and convert it to string, then we remove the excedent double quotes.
strInput := string(input)
strInput = strings.Trim(strInput, `"`)
Second, we do the parse using the layout for our case.
newTime, err := time.Parse("2006/01/02 15:04:05", strInput)
if err != nil {
return err
}
An explanation about the date layout. Normally, date layouts are used as yyyy/mm/dd but Go uses a real date and time to do the layout.
year == 2006
month == 01
day == 02
hour == 15
minute == 04
second == 05
So, our case became 2006/01/02 15:04:05.
To finish our implementation, we need set the newTime to the SpecialDate and return nil because we don’t have errors.
sd.Time = newTime
return nil
With our new type implemented, we can now change the Checkin struct to use the new SpecialDate.
type Checkin struct {
Timestamp SpecialDate `json:"timestamp"`
User string `json:"user"`
}
The rest will be the same, check the full code.
package main
import (
"encoding/json"
"fmt"
"strings"
"time"
)
type Checkin struct {
Timestamp SpecialDate `json:"timestamp"`
User string `json:"user"`
}
type SpecialDate struct {
time.Time
}
func (sd *SpecialDate) UnmarshalJSON(input []byte) error {
strInput := string(input)
strInput = strings.Trim(strInput, `"`)
newTime, err := time.Parse("2006/01/02 15:04:05", strInput)
if err != nil {
return err
}
sd.Time = newTime
return nil
}
func main() {
j := `{"timestamp":"2016/11/02 08:18:20", "user":"John Doe"}`
var c Checkin
err := json.Unmarshal([]byte(j), &c)
if err != nil {
fmt.Println(err)
}
fmt.Println(c)
}
I hope this help you with custom dates when working with JSON.