November 3, 2016

json: unmarshal custom date formats

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)
}

Run on Playground.

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)
}

Run on Playground.

I hope this help you with custom dates when working with JSON.

© José Ustra 2023

Powered by Hugo & Kiss.