francoispqt / gojay

high performance JSON encoder/decoder with stream API for Golang
MIT License
2.11k stars 112 forks source link

Encoding/Decoding time.Time #42

Closed nicklaw5 closed 6 years ago

nicklaw5 commented 6 years ago

It would be nice if there was an example showing the best approach for encoding and decoding time.Time values. I have searched this repository but can't find any reference on how to accomplish this.

Say I have the following object that maps to a SQL database table, how would I go about encoding/decoding the timestamp fields?

type User struct {
    ID        uint64
    Username  string
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt *time.Time
}
nicklaw5 commented 6 years ago

For a little more context, this is how I currently approach encoding the User object when using Go's native marshaller:

type User struct {
    ID        uint64     `json:"id"`
    Username  string     `json:"username"`
    CreatedAt time.Time  `json:"created_at"`
    UpdatedAt time.Time  `json:"updated_at"`
    DeletedAt *time.Time `json:"deleted_at"`
}

func (u *User) MarshalJSON() ([]byte, error) {
    type UserAlias User

    return json.Marshal(&struct {
        *UserAlias
        CreatedAt interface{} `json:"created_at"`
        UpdatedAt interface{} `json:"updated_at"`
        DeletedAt interface{} `json:"deleted_at"`
    }{
        UserAlias: (*UserAlias)(u),
        CreatedAt: nullifyTime(u.CreatedAt),
        UpdatedAt: nullifyTime(u.UpdatedAt),
        DeletedAt: nullifyTime(u.DeletedAt),
    })
}

func nullifyTime(t interface{}) interface{} {
    if reflect.ValueOf(t).Kind() == reflect.Ptr {
        if t.(*time.Time) == nil {
            return nil
        }
        return t.(*time.Time).Format(time.RFC3339)
    }

    if t.(time.Time).IsZero() {
        return nil
    }
    return t.(time.Time).Format(time.RFC3339)
}

Which once encode would result in the following example output:

{"id":2,"username":"nicklaw5","created_at":"2018-07-14T05:46:43+10:00","updated_at":"2018-07-14T05:46:43+10:00","deleted_at":null}
francoispqt commented 6 years ago

For time we do it this way:

// Decoding
func (u *User) UnmarshalJSONObject(dec *Decoder, k string) error {
    switch k {
    "createdAt":
        var str string
        if err := dec.String(&str); err != nil {
            return err
        }
        if t, err := time.Parse(time.RFC3339 , str); err != nil {
             return err
        } else {
            u.CreatedAt = t
        }
    }
    return nil
}
// Encoding
func (u *User) MarshalJSONObject(enc *gojay.Encoder) {
    enc.StringKey("createdAt", u.CreatedAt.Format(time.RFC3339))
}

But we could easily add some methods like:

// Decoding
dec.Time(t, time.RFC3339) // where t is a *time.Time
// Encoding
enc.TimeKey(t, time.RFC3339) // where t is a *time.Time

What do you think?

nicklaw5 commented 6 years ago

I think some methods would be ideal, as time properties are very common attributes in relaying information between services. Perhaps make the formatting optional however.

francoispqt commented 6 years ago

Ok will add those in the next release.

Will also add something like dec.SQLNullString()and enc.SQLNullStringKey("k", *sql.NullString) and all nullable types of the sql package.

Will try to release this in the coming days.

francoispqt commented 6 years ago

Will be added in next 1.2.0 version to be released tomorrow.