How to parse non standard time format from json

22,133

Solution 1

That's a case when you need to implement custom marshal and unmarshal functions.

UnmarshalJSON(b []byte) error { ... }

MarshalJSON() ([]byte, error) { ... }

By following the example in the Golang documentation of json package you get something like:

// First create a type alias
type JsonBirthDate time.Time

// Add that to your struct
type Person struct {
    Name string `json:"name"`
    BirthDate JsonBirthDate `json:"birth_date"`
}

// Implement Marshaler and Unmarshaler interface
func (j *JsonBirthDate) UnmarshalJSON(b []byte) error {
    s := strings.Trim(string(b), "\"")
    t, err := time.Parse("2006-01-02", s)
    if err != nil {
        return err
    }
    *j = JsonBirthDate(t)
    return nil
}
    
func (j JsonBirthDate) MarshalJSON() ([]byte, error) {
    return json.Marshal(time.Time(j))
}

// Maybe a Format function for printing your date
func (j JsonBirthDate) Format(s string) string {
    t := time.Time(j)
    return t.Format(s)
}

Solution 2

If there are lots of struct and you just implement custom marshal und unmarshal functions, that's a lot of work to do so. You can use another lib instead,such as a json-iterator extension jsontime:

import "github.com/liamylian/jsontime"

var json = jsontime.ConfigWithCustomTimeFormat

type Book struct {
    Id        int           `json:"id"`
    UpdatedAt *time.Time    `json:"updated_at" time_format:"sql_date" time_utc:"true"`
    CreatedAt time.Time     `json:"created_at" time_format:"sql_datetime" time_location:"UTC"`
}

Solution 3

I wrote a package for handling yyyy-MM-dd and yyyy-MM-ddThh:mm:ss dates at https://github.com/a-h/date

It uses the type alias approach in the answer above, then implements the MarshalJSON and UnmarshalJSON functions with a few alterations.

// MarshalJSON outputs JSON.
func (d YYYYMMDD) MarshalJSON() ([]byte, error) {
    return []byte("\"" + time.Time(d).Format(formatStringYYYYMMDD) + "\""), nil
}

// UnmarshalJSON handles incoming JSON.
func (d *YYYYMMDD) UnmarshalJSON(b []byte) (err error) {
    if err = checkJSONYYYYMMDD(string(b)); err != nil {
        return
    }
    t, err := time.ParseInLocation(parseJSONYYYYMMDD, string(b), time.UTC)
    if err != nil {
        return
    }
    *d = YYYYMMDD(t)
    return
}

It's important to parse in the correct timezone. My code assumes UTC, but you may wish to use the computer's timezone for some reason.

I also found that solutions which involved using the time.Parse function leaked Go's internal mechanisms as an error message which clients didn't find helpful, for example: cannot parse "sdfdf-01-01" as "2006". That's only useful if you know that the server is written in Go, and that 2006 is the example date format, so I put in more readable error messages.

I also implemented the Stringer interface so that it gets pretty printed in log or debug messages.

Share:
22,133

Related videos on Youtube

zola
Author by

zola

Updated on July 09, 2022

Comments

  • zola
    zola almost 2 years

    lets say i have the following json

    {
        name: "John",
        birth_date: "1996-10-07"
    }
    

    and i want to decode it into the following structure

    type Person struct {
        Name string `json:"name"`
        BirthDate time.Time `json:"birth_date"`
    }
    

    like this

    person := Person{}
    
    decoder := json.NewDecoder(req.Body);
    
    if err := decoder.Decode(&person); err != nil {
        log.Println(err)
    }
    

    which gives me the error parsing time ""1996-10-07"" as ""2006-01-02T15:04:05Z07:00"": cannot parse """ as "T"

    if i were to parse it manually i would do it like this

    t, err := time.Parse("2006-01-02", "1996-10-07")
    

    but when the time value is from a json string how do i get the decoder to parse it in the above format?

  • Jonathan
    Jonathan almost 7 years
    Right, and for the UnmarshalJSON func, OP could add multiple time.Parse attempts based on how many different formats need to be supported. I believe the format for time.RFC3339 is the default parser and more formats can be found in the docs
  • Kiril
    Kiril almost 7 years
    Sure, when you have the custom un/marshal functions, you should try to cover every case that's possible.
  • Ainar-G
    Ainar-G almost 7 years
    Minor nit: it should be JSONBirthDate, not JsonBirthDate, as per the code style.
  • WaltPurvis
    WaltPurvis almost 7 years
    What is JB in the *j = JB(t) line?
  • Kiril
    Kiril almost 7 years
    Since you have a type alias, you need to cast it. Read this, and the corresponding links to the documentation: stackoverflow.com/questions/19577423/…
  • danielcooperxyz
    danielcooperxyz over 5 years
    The MarshalJSON() method causes a stack overflow panic, as it indirectly calls itself.
  • Leo Alekseyev
    Leo Alekseyev about 3 years
    @danielcooperxyz is right; you need a cast here: return j.(time.Time).MarshalJSON()