Unmarshaling Time values from JSON

Go’s json package automatically marshals and unmarshals time.Time values
[1]. If you’re veering away from the defaults, however, there are some quirks
to be aware of. This short post covers some of the more important ones I found.

RFC 3339 by default

While the time package lets us serialize Times in many standard or
custom layouts, json has a default – RFC 3339.

If you’re marshaling Time into JSON from Go and unmarshaling it back in Go,
this shouldn’t bother you too much. That said, you may still need to produce
valid JSON objects for testing.

Here’s some code with a manually crafted time value that will be accepted by
the json package and unmarshaled correctly into a time.Time field:

type Event struct {
Name string `json:”name”`
Started time.Time `json:”started”`
}

var jsonText = []byte(`
{
“name”: “foobar”,
“started”: “2020-11-30T14:20:28.000+07:00”
}`)

func main() {

var e Event
if err := json.Unmarshal(jsonText, &e); err != nil {
log.Fatal(err)
}

fmt.Printf(“%+vn”, e)
}

A couple of notes on the format of the time string:

It’s fully specified in RFC 3339,
which is fairly short and worth a read. The grammar in section 5.6 is
particularly useful.
All RFC 3339 times must have UTC offsets in order to be unambiguous. When
we state the time is “8 PM” we typically don’t have to specify the time zone.
But 8 PM in California or Jerusalem are very different times, and in some
scenarios specifying the time zone is useful – e.g. for flight departure and
arrival times.
The seconds include an optional fractional component; in the string above
the .000 part can be safely omitted.

The Zs and the Ts

You’ll notice that in the string 2020-11-30T14:20:28.000+07:00, date and time
are separated by a T. While RFC 3339 mentions that this could be replaced
by a space for readability, Go’s time package doesn’t seem to support this
option – it has to be T (neither does it support a lowercase t, even
though that’s also allowed per the RFC).

The case of the Z is trickier. Z stands for “Zulu time”, a military
jargon for UTC+00:00 (previously known as GMT). If you don’t want to specify
an explicit UTC offset, you can use Z instead, like this:

var jsonText = []byte(`
{
“name”: “foobar”,
“started”: “2020-11-30T14:20:28.000Z”
}`)

But you cannot omit it. The parser is fairly strict; if you want Zulu time,
put the Z there explicitly.

A quirk of the Go implementation is that while the layout string for RFC 3339 is given as “2006-01-02T15:04:05Z07:00”,
this string is invalid as a value! That is, this code returns an error:

t, err = time.Parse(time.RFC3339, time.RFC3339)

This is rather unfortunate, but difficult to change at this point. See
https://github.com/golang/go/issues/20555 for more details.

The layout constant is invalid because it uses Z as the UTC offset
separator, instead of + or -. Z is only valid on its own, at the
end of the string.

Unmarshaling times in different layouts

As I said above, the default layout for unmarshaling Time from JSON is
RFC 3339. What to do if you receive the time in a different layout? We’ll have
to define a custom unmarshaler. First, let’s experiment with direct
Time parsing.

The time package defines several predefined layouts, and we can specify custom ones. The
key thing to remember is that the layout we provide has to describe the
canonical time “Mon Jan 2 15:04:05 MST 2006” (where MST stands for Mountain
Standard Time, or UTC-07:00).

Say we want to unmarshal food expiration dates, so we don’t care about times or
time zones at all. We can write:

customLayout := “2006-01-02”
t, err := time.Parse(customLayout, “2020-11-30”)

To unmarshal such times from json we’ll create a custom type with an
UnmarshalJSON method (which will make our type implement the
json.Unmarshaler interface):

type CustomTime struct {
time.Time
}

const expiryDateLayout = “2006-01-02”

func (ct *CustomTime) UnmarshalJSON(b []byte) (err error) {
s := strings.Trim(string(b), “””)
if s == “null” {
ct.Time = time.Time{}
return
}
ct.Time, err = time.Parse(expiryDateLayout, s)
return
}

Now we can use CustomType instead of time.Time:

type MyType struct {
Name string `json:”name”`
Expiring CustomTime `json:”expiring”`
}

var jsonText = []byte(`
{
“name”: “foobar”,
“expiring”: “2020-11-30”
}`)

func main() {
var mt MyType
if err := json.Unmarshal(jsonText, &mt); err != nil {
log.Fatal(err)
}

fmt.Printf(“%+vn”, mt)
}

[1]
To be precise, it’s the time package that does this, in collaboration
with json. The time package defines an UnmarshalJSON method
for Time values.

Flatlogic Admin Templates banner

Leave a Reply

Your email address will not be published. Required fields are marked *