go: encoding/json: tag `json:"-"` doesn't hide an embedded field

What version of Go are you using (go version)?

$ go version
go version go1.13.3 linux/amd64
$ gotip version
go version devel +696c41488a Mon Nov 11 15:37:55 2019 +0000 linux/amd64

Does this issue reproduce with the latest release?

Yes, see gotip version.

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/ainar/.cache/go-build"
GOENV="/home/ainar/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/ainar/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/home/ainar/go/go1.13"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/home/ainar/go/go1.13/pkg/tool/linux_amd64"
GCCGO="/usr/bin/gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/home/ainar/dev/tmp/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build199093238=/tmp/go-build -gno-record-gcc-switches"

What did you do?

type A struct {
	Name string `json:"name"`
}

type B struct {
	A
	Name string `json:"-"`
}

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

What did you expect to see?

Either:

<nil> {}

Or:

some error about tag conflict

What did you see instead?

<nil> {"name":"1234"}

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 11
  • Comments: 29 (26 by maintainers)

Most upvoted comments

the only real way to omit a field

Also, that’s not quite true. Using the rules that already exist today, you can do:

type A struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

// AExclude contains a list of fields to exclude from A.
// It must be embedded alongside A.
type AExclude struct {
	F1 string `json:"name"`
}

type B struct {
	A
	AExclude
}

See https://play.golang.org/p/7wHioEMdfaS

Another (more intuitive) way to omit attributes from json marshalling is:

type A struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

type B struct {
	A                                      // Or *A, both possible
	Name *struct{} `json:"name,omitempty"` // This attribute will be omitted
}

// And marshal something of type B

See https://play.golang.org/p/0OdxDtXcx8-, see also this old blog post.

It’s work for me to omit attributes from json marshalling: type A struct { Name string json:"name" }

type B struct { A Name string json:"name,-,omitempty" }

the only real way to omit a field

Also, that’s not quite true. Using the rules that already exist today, you can do:

type A struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

// AExclude contains a list of fields to exclude from A.
// It must be embedded alongside A.
type AExclude struct {
	F1 string `json:"name"`
}

type B struct {
	A
	AExclude
}

See https://play.golang.org/p/7wHioEMdfaS

I’m have been programming in Go as my main language for over 7 years and you just blew my mind.

Thanks @breml. I agree that this is intuitively what one would understand; it was my first impression too. You also raise a good point about how omitempty does allow shadowing via both struct field name and json field tag name.

For the record, I still think this would be nice to have, for the sake of consistency. Unfortunately, while omitempty is an option, - is not, so you cannot use it while also specifying a field tag name. This is what I tried to explain in https://github.com/golang/go/issues/35501#issuecomment-552890930.

For example, taking the original example once again:

type A struct {
	Name string `json:"name"`
}

type B struct {
	A
	Name string `json:"-"`
}

Following the omitempty logic of shadowing via the json field name, we don’t get any shadowing, since the names are Name and name. They would only be equal if A.Name didn’t have a struct tag, or if we allowed - as an option like `json:"name,-"`, as I showed above.

Aside from turning - into an option, the only other option we have is to always try struct field name shadowing, even if embedded fields have a struct tag field. That is, Name string `json:"-"` would shadow Name string `json:"name"` as well as X string `json:"Name"`.

I assume this is what you meant by your sentence below:

if in the parent struct we have "-", this named field (evaluated either by the struct tag or by the struct field name, in that order) is never shown in the final encoded json.

It’s certainly worth a try, and it could work, but I still wonder if it would break any existing valid uses of encoding/json. I have a CL ready, which we can experiment with. If people find it reasonable, we can attempt a merge early in the 1.15 cycle, and revert if any users bring up regressions.

I looked into this issue myself and I feel like the observation of @ainar-g is valid. The reason for this is the following:

In the documentation of encoding/json the section explaining "-" is directly following the section about ",omitempty" and there it says, that a field is omitted, if it contains the zero value:

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.

As a special case, if the field tag is “-”, the field is always omitted. Note that a field with name “-” can still be generated using the tag “-,”.

Now, if we extend the examples provided above (by @ainar-g and @mvdan) with the ",omitempty" cases, we see, that in this case the anonymous struct is not considered, even if the field in the “parent” struct is empty (zero value). This is the case regardless of the fact, if the final “name” of the field in the encoded json is defined by the struct field name or the struct tag, the behavior is consistent. See: https://play.golang.org/p/VjMa8H7EI9e

Therefore I would argue, that if the “omitting” for ",omitempty" is always decided based on the value in the “parent” struct, the same should be the case for "-", that is, if in the parent struct we have "-", this named field (evaluated either by the struct tag or by the struct field name, in that order) is never shown in the final encoded json.

Yes, whatever we decide to do, I think it would be good to clarify that the visibility rules apply to the JSON names alone.

If the behaviour isn't changed, can it at least be documented better, so that <del>I could bother @dominikh</del> the community could build static analysis tools to detect such idiosyncrasies?

I’ll leave this open for a week if anyone has more thoughts, or a specific way in which this could be changed without breaking existing valid use cases.

The only idea that comes to mind that would be truly backwards-compatible would be to extend the json tag syntax to allow omitting a field name, not just a specific field only. For example, `json:"name,-"` to omit any embedded field with `json:"name"`. "-" already takes the place of a name, so it taking the place of an option doesn’t seem like a terrible idea.

The question then would be if it’s worth adding this feature. How often does one want to omit fields from anonymous structs? Are there any current workarounds? Do any external json libraries support this feature?