How to format timestamp in outgoing JSON
Asked Answered
S

5

80

I've been playing with Go recently and it's awesome. The thing I can't seem to figure out (after looking through documentation and blog posts) is how to get the time.Time type to format into whatever format I'd like when it's encoded by json.NewEncoder.Encode

Here's a minimal Code example:

package main

type Document struct {
    Name        string
    Content     string
    Stamp       time.Time
    Author      string
}

func sendResponse(data interface{}, w http.ResponseWriter, r * http.Request){
     _, err := json.Marshal(data)
    j := json.NewEncoder(w)
    if err == nil {
        encodedErr := j.Encode(data)
        if encodedErr != nil{
            //code snipped
        }
    }else{
       //code snipped
    }
}

func main() {
    http.HandleFunc("/document", control.HandleDocuments)
    http.ListenAndServe("localhost:4000", nil)
}

func HandleDocuments(w http.ResponseWriter,r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Access-Control-Allow-Origin", "*")

    switch r.Method {
        case "GET": 
            //logic snipped
            testDoc := model.Document{"Meeting Notes", "These are some notes", time.Now(), "Bacon"}    
            sendResponse(testDoc, w,r)
            }
        case "POST":
        case "PUT":
        case "DELETE":
        default:
            //snipped
    }
}

Ideally, I'd like to send a request and get the Stamp field back as something like May 15, 2014 and not 2014-05-16T08:28:06.801064-04:00

But I'm not really sure how, I know I can add json:stamp to the Document type declaration to get the field to be encoded with the name stamp instead of Stamp, but I don't know what those types of things are called, so I'm not even sure what to google for to find out if there is some type of formatting option in that as well.

Does anyone have a link to the an example or good documentation page on the subject of those type mark ups (or whatever they're called) or on how I can tell the JSON encoder to handle time.Time fields?

Just for reference, I have looked at these pages: here and here and of course, at the official docs

Skatole answered 16/5, 2014 at 12:53 Comment(0)
P
122

What you can do is, wrap time.Time as your own custom type, and make it implement the Marshaler interface:

type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

So what you'd do is something like:

type JSONTime time.Time

func (t JSONTime)MarshalJSON() ([]byte, error) {
    //do your serializing here
    stamp := fmt.Sprintf("\"%s\"", time.Time(t).Format("Mon Jan _2"))
    return []byte(stamp), nil
}

and make document:

type Document struct {
    Name        string
    Content     string
    Stamp       JSONTime
    Author      string
}

and have your intialization look like:

 testDoc := model.Document{"Meeting Notes", "These are some notes", JSONTime(time.Now()), "Bacon"}    

And that's about it. If you want unmarshaling, there is the Unmarshaler interface too.

Prank answered 16/5, 2014 at 13:6 Comment(2)
This though does not allow to use all the other functions in the time package, like After and Before.Charcuterie
@DejoriDavid you just gotta cast it back to time.Time to do that: time.Time(myJsonTime).After(...Mither
T
79

Perhaps another way will be interesting for someone. I wanted to avoid using alias type for Time.

type Document struct {
    Name    string
    Content string
    Stamp   time.Time
    Author  string
}

func (d *Document) MarshalJSON() ([]byte, error) {
    type Alias Document
    return json.Marshal(&struct {
        *Alias
        Stamp string `json:"stamp"`
    }{
        Alias: (*Alias)(d),
        Stamp: d.Stamp.Format("Mon Jan _2"),
    })
}

Source: http://choly.ca/post/go-json-marshalling/

Thermophone answered 2/3, 2016 at 10:37 Comment(3)
I found this to be the cleaner and more contained option. However, I opted to use Document rather than *Document as the receiver because this custom MarshalJSON function was not being called on individual document objects (in a detail api view), but was when the api responded with a slice of documents (really pointers to those documents). Something to look out for. (this article was helpful for me: hackernoon.com/…)Below
This is great, but how could I Unmarshal this again?Handfast
@Handfast by implementing JSON Unmarshaler interface, i.e. give it a UnmarshalJSON method.Harlandharle
P
50

I would NOT use:

type JSONTime time.Time

I would use it only for primitives (string, int, ...). In case of time.Time which is a struct, I would need to cast it every time I want to use any time.Time method.

I would do this instead (embedding):

type JSONTime struct {
    time.Time
}

func (t JSONTime)MarshalJSON() ([]byte, error) {
    //do your serializing here
    stamp := fmt.Sprintf("\"%s\"", t.Format("Mon Jan _2"))
    return []byte(stamp), nil
}

No need to cast t to time. The only difference is that new instance is NOT created by JSONTime(time.Now()) but by JSONTime{time.Now()}

Peachey answered 16/1, 2017 at 14:6 Comment(1)
Great recommendation. By the way, if you are wondering how you can convert it back to a time.Time, you simply need t.Time, where t is a JSONTime: golangbyexample.com/anonymous-fields-struct-golangMuddlehead
I
9

But I'm not really sure how, I know I can add json:stamp to the Document type declaration to get the field to be encoded with the name stamp instead of Stamp, but I don't know what those types of things are called, so I'm not even sure what to google for to find out if there is some type of formatting option in that as well.

You mean tags. But these won't help you with your formatting problem.

The string representation you get for your time is returned by MarshalJSON implemented by Time.

You can go ahead and implement your own MarshalJSON method by copying the relevant bits from the Time implementation by either embedding time.Time or wrapping it. Wrapping example (Click to play):

type ShortDateFormattedTime time.Time

func (s ShortDateFormattedTime) MarshalJSON() ([]byte, error) {
    t := time.Time(s)
    if y := t.Year(); y < 0 || y >= 10000 {
        return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]")
    }

    return []byte(t.Format(`"Jan 02, 2006"`)), nil
}
Instruction answered 16/5, 2014 at 13:27 Comment(1)
upvote for the great links. the other answer was just a bit faster. Your naming convention for the date is better though.Skatole
H
0

Based on the above answers, here is a bigger example that includes unmarshalling.

package main

import (
    "errors"
    "fmt"
    "time"
)

type (
    JSONTimeMs struct {
        time.Time
    }

    JSONTimeUs struct {
        time.Time
    }
)

func (ms JSONTimeMs) MarshalJSON() ([]byte, error) {
    str := fmt.Sprintf(`"%s"`, ms.Format("2006-01-02T15:04:05.000Z"))
    return []byte(str), nil
}

func (ms *JSONTimeMs) UnmarshalJSON(text []byte) error {
    if len(text) < 26 {
        return errors.New("malformed millisecond timestamp")
    }
    str := string(text[1:25])
    t, err := time.Parse("2006-01-02T15:04:05.000Z", str)
    if err != nil {
        return err
    }
    ms.Time = t
    return nil
}

func (us JSONTimeUs) MarshalJSON() ([]byte, error) {
    str := fmt.Sprintf(`"%s"`, us.Format("2006-01-02T15:04:05.000000Z"))
    return []byte(str), nil
}

func (us *JSONTimeUs) UnmarshalJSON(text []byte) error {
    if len(text) < 29 {
        return errors.New("malformed microsecond timestamp")
    }
    str := string(text[1:28])
    t, err := time.Parse("2006-01-02T15:04:05.000000Z", str)
    if err != nil {
        return err
    }
    us.Time = t
    return nil
}

Also after unmarshalling, you probably want to work with the inner Time, e.g., myjsontime.Time.

Hades answered 25/1 at 15:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.