Go version
go1.25.0 (actually since omitzero was added in go.1.24.0)
Output of go env
in your module/workspace:
AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/maruel/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/maruel/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build3213510385=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/home/maruel/src-oth/golang/src/go.mod'
GOMODCACHE='/home/maruel/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/maruel/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/maruel/src-oth/golang'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/maruel/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/maruel/src-oth/golang/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.25.0'
GOWORK=''
PKG_CONFIG='pkg-config'
What did you do?
encoding/json's omitzero is great. One surprising behavior is that zero length and zero capacity slices are still included. I wasn't expecting that.
https://github.com/golang/go/blob/go1.25.0/src/encoding/json/encode.go#L747 fv.IsZero() calls https://github.com/golang/go/blob/go1.25.0/src/reflect/value.go#L1676 which only checks for nil. It's actually a bummer that reflect.Value.IsZero() only checks for nil for slices and not for zero-length. I fear it would probably be a breaking change to change that but that would be my preferred fix.
I believe there's a workaround that doesn't require changing reflect,
https://github.com/golang/go/blob/go1.25.0/src/encoding/json/encode.go#L1216
we could hack this and add an explicit check for slice and if case t.Kind() == reflect.Slice && !t.Implements(isZeroerType):
then add our custom isZero that return v.Len() == 0
.
That said, there's already a workaround today. It is to create a type and implement IsZero(), as shown below:
https://go.dev/play/p/_Dyirw7MVQo
package main
import (
"encoding/json"
"fmt"
)
type Contents []string
func (c *Contents) IsZero() bool {
return len(*c) == 0
}
type Container1 struct {
Content []string `json:"content,omitzero"`
}
type Container2 struct {
Content Contents `json:"content,omitzero"`
}
func printJSON(d any) {
b, _ := json.Marshal(d)
fmt.Println(string(b))
}
func main() {
println("Container1")
printJSON(Container1{Content: make([]string, 0, 1)})
printJSON(Container1{Content: make([]string, 0)})
printJSON(Container1{Content: nil})
println("Container2")
printJSON(Container2{Content: make([]string, 0, 1)})
printJSON(Container2{Content: make([]string, 0)})
printJSON(Container2{Content: nil})
}
What did you see happen?
Container1
{"content":[]}
{"content":[]}
{}
Container2
{}
{}
{}
What did you expect to see?
Container1
{}
{}
{}
Container2
{}
{}
{}
Comment From: TapirLiu
zero-length slices are not zero slices. It looks the docs is clear. The reflect IsZero
is right here.
Comment From: callthingsoff
Hi, what do you think about also adding omitempty
option:
`json:"content,omitzero,omitempty"`
https://go.dev/play/p/3VX3JLvdwWB
Container1
{}
{}
{}
Container2
{}
{}
{}
Comment From: maruel
Hi, what do you think about also adding
omitempty
option:
`json:"content,omitzero,omitempty"`
Oh I didn't think of that! That's brilliant! I guess that's the best solution all around. I'll update my code accordingly.
Comment From: gabyhelp
Related Issues
- encoding/json: strange isEmptyValue behavior with arrays #31189 (closed)
- encoding/json: omitempty for arrays is confusing #29310
- encoding/json: add omitzero option #45669 (closed)
- proposal: encoding/json: `nilasempty` to encode nil-slices as `[]` #37711
Related Code Changes
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)