DEV Community

Roman
Roman

Posted on • Updated on • Originally published at romangaranin.net

JSON, time, and golang

Fun thing about JSON - it doesn't have dates or time support, as per RFC.
So whereas you see something like 2021-02-18T21:54:42.123Z in JSON - it's still just a string.

Some languages firmly follow that, like nim stdlib parser.
Some try to parse as if it were ISO 8601 time.
Which is actually nice - it's human readable, sortable, portable, well supported and precise.
Though such support sometimes causes extra fun, like java's jackson lib and JSR 310. If correspondent parsing module is not added for java 8+ datetime classes then marshalling result will be bizzare.

Golang supports ISO 8601, so it will work as you might expect:

dateString := "2021-02-18T21:54:42.123Z"
date, err := time.Parse(time.RFC3339, dateString) //RFC 3339 is a profile for ISO 8601
Enter fullscreen mode Exit fullscreen mode

date will have the time.Time value. (let's skip for a moment the difference between realtime and monotonic time)

Apart from such a detailed 'point of time' there is also 'calendar date', or 'civil time' - representation of a 'day', e.g. 2021-02-18. A common thing to use.

Golang stdlib doesn't have special type for that, one should still rely on time.Time type.
And it actually works:

dateString := "2021-02-18"
date, err := time.Parse("2006-01-02", dateString) //note the date layout YYYY-MM-DD
Enter fullscreen mode Exit fullscreen mode

It prints into 2021-02-18 00:00:00 +0000 UTC.

So if you want to parse a JSON with time field you might come up with that:

type Entity struct {
    Name string    `json:"name"`
    Time time.Time `json:"time"`
}

func main() {
    jsonString := `{"name": "A name", "time": "2021-02-18T21:54:42.123Z"}`
    var entity Entity
    err := json.Unmarshal([]byte(jsonString), &entity)
}
Enter fullscreen mode Exit fullscreen mode

Works nicely.

But if the time field has only 'date' part (2021-02-18), unmarshalling will fail with:
parsing time ""2021-02-18"" as ""2006-01-02T15:04:05Z07:00"": cannot parse """ as "T"

And in this moment you either have to fallback to using string type and parse the date manually.
Or introduce your own time type alias and provide custom (un)marshaller, for example:

type Entity struct {
    Name string    `json:"name"`
    Time CivilTime `json:"time"`
}

type CivilTime time.Time

func (c *CivilTime) UnmarshalJSON(b []byte) error {
    value := strings.Trim(string(b), `"`) //get rid of "
    if value == "" || value == "null" {
        return nil
    }

    t, err := time.Parse("2006-01-02", value) //parse time
    if err != nil {
        return err
    }
    *c = CivilTime(t) //set result using the pointer
    return nil
}

func (c CivilTime) MarshalJSON() ([]byte, error) {
    return []byte(`"` + time.Time(c).Format("2006-01-02") + `"`), nil
}
Enter fullscreen mode Exit fullscreen mode

Implementing (un)marshaller json interfaces is pretty straight forward.
Just a tad cumbersome :)

I hope civil time proposal will be implemented.

Top comments (0)