DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

Go json.Marshal 的 omitempty 没有省略零值结构体字段

encoding/json 的 omitempty 不省略值结构体。用 nil 指针表示缺失,或在 Go 1.24 起使用 omitzero,并核对空对象与缺失键的接口差异。

给嵌套字段加了 json:"details,omitempty",字段内没有业务数据,编码后却仍出现 "details":{},或者出现一组空字符串、null。在标准库 encoding/json 的默认编码中,值类型结构体不属于 omitempty 定义的空值。

这里讨论导出字段、普通命名嵌套字段,以及没有自定义 MarshalJSON 的类型。encoding/json/v2 和第三方编码器应按各自规则判断,不能直接套用下面的省略条件。

内层字段省略了,外层结构体仍然存在

例如:

type Details struct {
    Name string `json:"name,omitempty"`
}

type Response struct {
    Details Details `json:"details,omitempty"`
}

Response{} 的 Details 是一个零值结构体。内层 Name 的空字符串符合省略条件,但外层 Details 仍会编码为对象,所以结果包含 "details":{}。

omitempty 的空值包括 false、数值零、nil 指针、nil 接口,以及长度为零的数组、切片、映射和字符串。它不会递归检查普通值结构体内部的字段,以此决定外层字段是否省略。给每个内部字段都加上 omitempty,也不能改变这一点。

“没有详情”用 nil 指针表达

如果接口需要区分“没有提供详情”和“提供了一个空详情对象”,可以把外层字段改为指针,让 nil 表示缺失:

package main

import (
    "encoding/json"
    "fmt"
)

type Details struct {
    Name string `json:"name,omitempty"`
}

type Response struct {
    Details *Details `json:"details,omitempty"`
}

func main() {
    samples := []Response{
        {},
        {Details: &Details{}},
        {Details: &Details{Name: "demo"}},
    }
    for _, sample := range samples {
        data, err := json.Marshal(sample)
        if err != nil {
            panic(err)
        }
        fmt.Println(string(data))
    }
}

预期输出依次为:

{}
{"details":{}}
{"details":{"name":"demo"}}

这是三次编码的输出示意,不是一个包含三项的 JSON 文档。只有第一项的指针为 nil;&Details{} 虽然指向零值结构体,指针本身并不为 nil,所以仍保留 details。

若程序在所有路径上都提前分配了 &Details{},仅把字段声明改成指针并不会自动省略它。应在确实没有详情时保持 nil,有数据时再分配。访问 response.Details.Name 前也要检查指针,避免把编码问题改成 nil 指针访问问题。

Go 1.24 起,可以按零值省略值字段

如果接口不需要区分“缺失”和“显式零值”,并且构建环境至少为 Go 1.24,可以考虑 omitzero:

type Response struct {
    Details Details `json:"details,omitzero"`
}

在前面那个仅有 Name string 的 Details 定义下,Response{} 的详情字段是零值,会被省略;非空名称则保留。这个选项从 Go 1.24 才加入,不能把它作为更旧构建环境的解决办法。

omitzero 与 omitempty 判断的条件不同。若字段类型实现了 IsZero() bool,omitzero 会使用该方法判断;否则按类型零值判断。因此,复杂类型是否能省略,还要看它的实际值和方法,不能只凭最终 JSON 看起来像空对象。

不要为了去掉一处空对象就全局替换标签。指针方案表达“是否存在”,零值省略表达“这个值是否为零”;选择取决于接口希望保留哪种信息。

验证字段是否缺失,而不只看编码有没有报错

修复后分别检查 nil、显式空对象、有内容对象三个分支,并核对客户端是否把缺失键、null 和 {} 当作不同含义。成功编码并不说明输出符合接口约定。

将值字段改为指针时,要同步检查对象构造和字段访问;采用 omitzero 时,要核对实际构建工具链以及相关类型的 IsZero 方法。若需要恢复旧接口格式,回退字段类型或标签并恢复对应调用代码,再比较响应样例。已经输出给下游的数据不会随代码回退自动恢复。