Override the layout used by json.Marshal to format time.Time
Asked Answered
G

4

11

In Golang, is there a way to make the generic encoding/json Marshal to use a different layout when Marshaling the time.Time fields?

Basically I have this struct:

s := {"starttime":time.Now(), "name":"ali"}

and I want to encoding to json using encdoding/json's Marshal function, but I want to use my custom layout, I imagine somewhere time.Format(layout) is being called, I want to control that layout,

Gourd answered 9/12, 2013 at 16:16 Comment(0)
L
17

As inspired by zeebo's answer and hashed out in the comments to that answer:

http://play.golang.org/p/pUCBUgrjZC

package main

import "fmt"
import "time"
import "encoding/json"

type jsonTime struct {
    time.Time
    f string
}

func (j jsonTime) format() string {
    return j.Time.Format(j.f)
}

func (j jsonTime) MarshalText() ([]byte, error) {
    return []byte(j.format()), nil
}

func (j jsonTime) MarshalJSON() ([]byte, error) {
    return []byte(`"` + j.format() + `"`), nil
}

func main() {
    jt := jsonTime{time.Now(), time.Kitchen}
    if jt.Before(time.Now().AddDate(0, 0, 1)) { // 1
        x := map[string]interface{}{
            "foo": jt,
            "bar": "baz",
        }
        data, err := json.Marshal(x)
        if err != nil {
            panic(err)
        }
        fmt.Printf("%s", data)
    }
}

This solution embeds the time.Time into the jsonTime struct. Embedding promotes all of time.Time's methods to the jsonTime struct, allowing their use without explicit type conversion (see // 1).

Embedding a time.Time has the downside of also promoting the MarshalJSON method, which the encoding/json marshaling code prioritizes higher than the MarshalText method for backwards compatibility reasons (MarshalText was added in Go 1.2, MarshalJSON predates that). As a result the default time.Time format is used instead of a custom format provided by MarshalText.

To overcome this problem we override MarshalJSON for the jsonTime struct.

Lachus answered 11/12, 2013 at 5:2 Comment(1)
how would you make this respect "omitempty" on the time field?Libertinage
M
7

Maybe something like this will work for you?

package main

import "fmt"
import "time"
import "encoding/json"

type jsonTime struct {
t time.Time
f string
}

func (j jsonTime) MarshalText() ([]byte, error) {
return []byte(j.t.Format(j.f)), nil
}

func main() {
x := map[string]interface{}{
    "foo": jsonTime{t: time.Now(), f: time.Kitchen},
    "bar": "baz",
}
data, err := json.Marshal(x)
if err != nil {
    panic(err)
}
fmt.Printf("%s", data)
}

also available here: http://play.golang.org/p/D1kq5KrXQZ

Just make a custom type that implements MarshalText the way you want it to show up.

Mackay answered 9/12, 2013 at 16:53 Comment(5)
Thanks, That is definitely a step forward, but I wish I didn't have to convert all time.Times to mytime and back everytime I want to use a time.After or time.Before ...Gourd
If you want to call time.Time methods on the custom type, then embed a time.Time instead of giving it a name. e.g. play.golang.org/p/Vudw0hhnweLachus
Unless I am much mistaken, if I embed the time.Time then I the MarshalText won't work for json encoding, which was the reason for all of this to begin with. It needs to be named.Gourd
Yes, embedding the time.Time does prevent MarshalText from working. Embedding promotes the MarshalJSON method of time.Time to the composite type. This can be overcome by overriding the MarshalJSON method as my example did. If you still want MarshalText for use by other encoders and you don't want to duplicate the formatting logic, then something like this might work: play.golang.org/p/liL0kAXp41Lachus
@chrisH you are right, why don't you post your solution as a answer.Gourd
I
4

First, I highly recommend against using a time format other than the default RFC3339. It's a good time format, and can be parsed by any number of languages, so unless you are needing a different format because somebody else's API requires it, it's probably best to use the default.

But, I've had to solve this problem in consuming other people's APIs, so here is one solution that shifts the bulk of the work to the Marshal/Unmarshal step, and leaves you with an ideal structure: http://play.golang.org/p/DKaTbV2Zvl

Intercept answered 10/12, 2013 at 16:17 Comment(1)
How is RFC3339 better than any other? (e.g RFC822Z)Moonseed
K
0

With generics I found a very hacky way to do this without initializing manually the time fields

type BaseDate[T TimeFormat] struct {
    time.Time
}

func (b *BaseDate[T]) Layout() string {
    format := *new(T)
    return format.String()
}

func (b *BaseDate[T]) Format() string {
    return b.Time.Format(b.Layout())
}

func (b *BaseDate[T]) MarshalJSON() ([]byte, error) {
    //do your serializing here
    stamp := fmt.Sprintf("\"%s\"", b.Layout())
    return []byte(stamp), nil
}

func (b *BaseDate[T]) UnmarshalJSON(data []byte) error {
    // Ignore null, like in the main JSON package.
    str := strings.Trim(string(data), "\"")
    if str == "null" || str == "" {
        return nil
    }
    tt, err := time.Parse(b.Layout(), str)
    b.Time = tt
    return err
}

func Now[T TimeFormat]() BaseDate[T] {
    return BaseDate[T]{Time: time.Now()}
}

And Time format is defined as follow

type TimeFormat interface {
    Datetime | Datehour | Date | Hour
    String() string
}

type Datetime string
type Datehour string
type Date string
type Hour string

const (
    dateTime Datetime = "2006-01-02T15:04:05"
    dateHour Datehour = "2006-01-02T15:04"
    date     Date     = "2006-01-02"
    hour Hour = "15:04"
)

func (d Datetime) String() string {
    return string(DateTime)
}

func (d Datehour) String() string {
    return string(DateHour)
}
func (d Date) String() string {
    return string(date)
}
func (d Hour) String() string {
    return string(hour)
}

I think the function String is unnecessary but add clarity. This works because I can define the zero value for the type Datetime, Datehour, Date and Hour.

Katanga answered 10/3, 2023 at 11:58 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Persevere

© 2022 - 2024 — McMap. All rights reserved.