Skip to content

encoding/json: add omitzero option #45669

Closed
tailscale/go
#108

Description

@palsivertsen

The omitempty json tag is kind of confusing to use when working with nested structs. The following example illustrates the most basic case using an empty struct for argument's sake.

type Foo struct {
	EmptyStruct struct{} `json:",omitempty"`
}

The "EmptyStruct" field is a struct without any fields and can be empty, that is equal to it's zero value. But when I try to marshal it to json the field is still included in the resulting json object. Reading the encoding/json documentation about the definition of empty it does not mention empty structs:

The "omitempty" option specifies that the field should be omitted from the encoding if the field has an empty value, defined as false, 0, a nil pointer, a nil interface value, and any empty array, slice, map, or string.

It feels weird that adding the omitempty tag to struct fields is allowed if it will never have the desired effect. If structs will never be considered empty shouldn't there at least be a compiler warning when adding this tag to a struct field?

Working with json time.Time

This behavior causes some confusion when working with time.Time in json structures. Go's zero values for primitive types are fairly reasonable and "guessable" from a non-gopher point of view. But the zero value for time.Time, January 1, year 1, 00:00:00.000000000 UTC, is less common. A more(?) common zero value for time is January 01, 1970 00:00:00 UTC. To avoid confusion when working with json outside the world of go it would be nice to have a way to omit zero value dates.

A commonly suggested workaround to this problem is to use pointers, but pointers might be undesirable for a number of reasons. They are for example cumbersome to assign values to:

type Foo struct {
	T *time.Time
}
_ = Foo{
	T: &time.Now(), // Compile error
}
_ = Foo{
	T: &[]time.Time{time.Now()}[0], // Weird workaround
}

The time.Time documentation also recommends to pass the type as value, not pointer since some methods are not concurrency-safe:

Programs using times should typically store and pass them as values, not pointers. That is, time variables and struct fields should be of type time.Time, not *time.Time.

A Time value can be used by multiple goroutines simultaneously except that the methods GobDecode, UnmarshalBinary, UnmarshalJSON and UnmarshalText are not concurrency-safe.

This playground example illustrates three different uses of empty structs where I'd expect the fields to be excluded from the resulting json. Note that the time.Time type has no exposed fields and uses the time.Time.NarshalJSON() function to marshal itself into json.

Solution

Would it make sense to add a new tag, like omitzero, that also excludes structs?

There's a similar proposal in #11939, but that changes the definition of empty in the omitempty documentation.

Activity

  1. mvdan commented on Apr 21, 2021

    @mvdan
    Member

    Pretty similar to #22480. I think this needs to be put on hold along with #11939. We are aware that the API of encoding/json has issues, and we're working on it.

  2. changed the title [-]Omit zero values when encoding json[/-] [+]propsoal: encoding/json: add omitzero option[/+] on Apr 22, 2021
  3. changed the title [-]propsoal: encoding/json: add omitzero option[/-] [+]proposal: encoding/json: add omitzero option[/+] on Apr 22, 2021
  4. added this to the Proposal milestone on Apr 22, 2021
  5. rsc commented on May 12, 2021

    @rsc
    Contributor

    Hold for #22480.

  6. rsc commented on May 12, 2021

    @rsc
    Contributor

    Placed on hold.
    — rsc for the proposal review group

  7. rsc commented on Jun 1, 2022

    @rsc
    Contributor

    Taking off hold after closing #52803 as a duplicate of this one.

    The big question is if we add ,omitzero do we also add a method to let "am I zero" be user-controllable?

    Or perhaps we should allow MarshalJSON to return nil as in #50480.

  8. rsc commented on Jun 1, 2022

    @rsc
    Contributor

    This proposal has been added to the active column of the proposals project
    and will now be reviewed at the weekly proposal review meetings.
    — rsc for the proposal review group

  9. smikulcik commented on Jun 1, 2022

    @smikulcik

    Personally I don't like baking the omission policy into struct tags. Things like omitempty, omitnil, omitzero are all limited by that one policy. I kinda liked my proposal for #52803 since it allowed that extra flexibility.

    the IsEmpty() proposal also adds this flexibility, but it seems odd to further the abstraction of what it means for a property to be "empty". Arn't we just trying to determine if we want to omit a field or not?

  10. icholy commented on Jun 2, 2022

    @icholy
    Contributor

    Or perhaps we should allow MarshalJSON to return nil as in #50480.

    Say we go down that path and I write a type like this:

    type Null[T any] struct {
    	Valid bool
    	Value T
    }
    
    func (n Null[T]) MarshalJSON() ([]byte, error) {
    	if !n.Valid {
    		return nil, nil
    	}
    	return json.Marshal(n.Valid)
    }

    Now this type can only be used as an omitzero field.

    @rsc

  11. joeshaw commented on Jun 2, 2022

    @joeshaw
    Contributor

    @icholy I believe the return-nil option would be instead of omitzero, not in addition to it.

  12. icholy commented on Jun 2, 2022

    @icholy
    Contributor

    @joeshaw having a nil return result in a null in non omitzero or omitempty fields would change the behavior of existing code.

  13. joeshaw commented on Jun 2, 2022

    @joeshaw
    Contributor

    @icholy Returning nil would not result in null in the JSON (that is done by returning []byte("null")), it would cause the field to be omitted entirely, under the proposal from #50480. Returning nil today causes invalid JSON to be produced.

    However, @dsnet points out in #52803 (comment) that nil as a return value might cause existing code which calls MarshalJSON to break because it is not expecting this behavior.

  14. 44 remaining items

  15. added 4 commits that reference this issue on Sep 27, 2024
    aae92fa
    d599945
    33ac9b3
    bfb9b41
  16. added a commit that references this issue on Oct 2, 2024
    e86982c
  17. modified the milestones: Backlog, Go1.24 on Oct 2, 2024
  18. added 2 commits that reference this issue on Dec 16, 2024
    7c8d87d
    e005697
  19. gopherbot commented on Feb 25, 2025

    @gopherbot
    Contributor

    Change https://go.dev/cl/620595 mentions this issue: cmd: use omitzero option for time.Time

  20. locked and limited conversation to collaborators on Feb 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions