Joe Bergevin
Joe Bergevin

Reputation: 3268

JSON omitempty With time.Time Field

Trying to json Marshal a struct that contains 2 time fields. But I only want the field to come through if it has a time value. So I'm using json:",omitempty" but it's not working.

What can I set the Date value to so json.Marshal will treat it like an empty (zero) value and not include it in the json string?

Playground: http://play.golang.org/p/QJwh7yBJlo

Actual Outcome:

{"Timestamp":"2015-09-18T00:00:00Z","Date":"0001-01-01T00:00:00Z"}

Desired Outcome:

{"Timestamp":"2015-09-18T00:00:00Z"}

Code:

package main

import (
    "encoding/json"
    "fmt"
    "time"
)

type MyStruct struct {
    Timestamp time.Time `json:",omitempty"`
    Date      time.Time `json:",omitempty"`
    Field     string    `json:",omitempty"`
}

func main() {
    ms := MyStruct{
        Timestamp: time.Date(2015, 9, 18, 0, 0, 0, 0, time.UTC),
        Field:     "",
    }

    bb, err := json.Marshal(ms)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(bb))
}

Upvotes: 81

Views: 41585

Answers (4)

blackgreen
blackgreen

Reputation: 44627

Go 1.24

You can now use the standard json tag omitzero. From Go 1.24 release notes:

If the field type has an IsZero() bool method, that will be used to determine whether the value is zero.

time.Time does have an IsZero() method, therefore it will nicely work without using pointers or other workarounds.

Your original code with a small modification:

package main

import (
    "encoding/json"
    "fmt"
    "time"
)

type MyStruct struct {
    Date  time.Time `json:",omitzero"`
    Field string    `json:",omitempty"`
}

func main() {
    ms := MyStruct{
        // Date not set
        Field: "foo",
    }

    bb, err := json.Marshal(ms)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(bb))
}

Now prints:

{"Field":"foo"}

Go playground: https://go.dev/play/p/Qc_XB5IWR7h?v=gotip

Upvotes: 0

Jiang YD
Jiang YD

Reputation: 3311

You may define you self Time type for custom marshal format, and use it everywhere instead time.Time

https://play.golang.org/p/C8nIR1uZAok

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "time"
)

type MyTime struct {
    *time.Time
}

func (t MyTime) MarshalJSON() ([]byte, error) {
    return []byte(t.Format("\"" + time.RFC3339 + "\"")), nil
}

// UnmarshalJSON implements the json.Unmarshaler interface.
// The time is expected to be a quoted string in RFC 3339 format.
func (t *MyTime) UnmarshalJSON(data []byte) (err error) {

    // by convention, unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op.
    if bytes.Equal(data, []byte("null")) {
        return nil
    }

    // Fractional seconds are handled implicitly by Parse.
    tt, err := time.Parse("\""+time.RFC3339+"\"", string(data))
    *t = MyTime{&tt}
    return
}

func main() {
    t := time.Now()
    d, err := json.Marshal(MyTime{&t})
    fmt.Println(string(d), err)
    var mt MyTime
    json.Unmarshal(d, &mt)
    fmt.Println(mt)
}

Upvotes: 8

chim
chim

Reputation: 8573

As a follow up to icza's answer here is a custom marshaller that omits an empty date field but keeps the rest of the fields unchanged.

func (ms *MyStruct) MarshalJSON() ([]byte, error) {
    type Alias MyStruct
    if ms.Timestamp.IsZero() {
        return json.Marshal(&struct {
            Timestamp int64 `json:",omitempty"`
            *Alias
        }{
            Timestamp: 0,
            Alias:     (*Alias)(ms),
        })
    } else {
        return json.Marshal(&struct {
            *Alias
        }{
            Alias: (*Alias)(ms),
        })
    }
}

This borrows from http://choly.ca/post/go-json-marshalling/

The OPs case has two time fields which would make it much more complicated. (you'd have to check for neither, either and both being empty!)

There may be better ways to achieve this, so comments are welcome.

Upvotes: 6

icza
icza

Reputation: 417472

The omitempty tag option does not work with time.Time as it is a struct. There is a "zero" value for structs, but that is a struct value where all fields have their zero values. This is a "valid" value, so it is not treated as "empty".

But by simply changing it to a pointer: *time.Time, it will work (nil pointers are treated as "empty" for json marshaling/unmarshaling). So no need to write custom Marshaler in this case:

type MyStruct struct {
    Timestamp *time.Time `json:",omitempty"`
    Date      *time.Time `json:",omitempty"`
    Field     string     `json:",omitempty"`
}

Using it:

ts := time.Date(2015, 9, 18, 0, 0, 0, 0, time.UTC)
ms := MyStruct{
    Timestamp: &ts,
    Field:     "",
}

Output (as desired):

{"Timestamp":"2015-09-18T00:00:00Z"}

Try it on the Go Playground.

If you can't or don't want to change it to a pointer, you can still achieve what you want by implementing a custom Marshaler and Unmarshaler. If you do so, you can use the Time.IsZero() method to decide if a time.Time value is the zero value.

Upvotes: 154

Related Questions