如何在Go中将空结构体不编组为JSON?

huangapple go评论81阅读模式
英文:

How to not marshal an empty struct into JSON with Go?

问题

我有一个这样的结构体:

type Result struct {
    Data       MyStruct  `json:"data,omitempty"`
    Status     string    `json:"status,omitempty"`
    Reason     string    `json:"reason,omitempty"`
}

但是,即使MyStruct的实例完全为空(即所有值都是默认值),它仍然被序列化为:

"data":{}

我知道encoding/json文档指定了“空”字段的定义:

false、0、任何nil指针或接口值,以及长度为零的任何数组、切片、映射或字符串

但是对于所有字段都为空/默认值的结构体没有考虑。所有字段都标记了omitempty,但这没有任何效果。

如何让JSON包序列化我的空结构体字段?

英文:

I have a struct like this:

type Result struct {
	Data       MyStruct  `json:"data,omitempty"`
	Status     string    `json:"status,omitempty"`
	Reason     string    `json:"reason,omitempty"`
}

But even if the instance of MyStruct is entirely empty (meaning, all values are default), it's being serialized as:

"data":{}

I know that the encoding/json docs specify that "empty" fields are:

> false, 0, any nil pointer or interface value, and any array,
> slice, map, or string of length zero

but with no consideration for a struct with all empty/default values. All of its fields are also tagged with omitempty, but this has no effect.

How can I get the JSON package to not marshal my field that is an empty struct?

答案1

得分: 206

根据文档所说,“任何空指针。”-- 将结构体定义为指针。指针具有明显的“空”值:nil

修复方法 - 使用指针字段定义类型:

type Result struct {
    Data       *MyStruct `json:"data,omitempty"`
    Status     string    `json:"status,omitempty"`
    Reason     string    `json:"reason,omitempty"`
}

然后,像这样创建一个值:

result := Result{}

将被编组为:

{}

解释:注意我们类型定义中的 *MyStruct。JSON 序列化不关心它是指针还是非指针 -- 这是运行时的细节。因此,将结构体字段定义为指针只对编译和运行时有影响。

只需注意,如果将字段类型从 MyStruct 更改为 *MyStruct,则需要使用结构体值的指针来填充它,如下所示:

Data: &MyStruct{ /* values */ }
英文:

As the docs say, "any nil pointer." -- make the struct a pointer. Pointers have obvious "empty" values: nil.

Fix - define the type with a struct pointer field:

type Result struct {
    Data       *MyStruct `json:"data,omitempty"`
    Status     string    `json:"status,omitempty"`
    Reason     string    `json:"reason,omitempty"`
}

Then a value like this:

result := Result{}

Will marshal as:

{}

Explanation: Notice the *MyStruct in our type definition. JSON serialization doesn't care whether it is a pointer or not -- that's a runtime detail. So making struct fields into pointers only has implications for compiling and runtime).

Just note that if you do change the field type from MyStruct to *MyStruct, you will need pointers to struct values to populate it, like so:

Data: &MyStruct{ /* values */ }

答案2

得分: 23

如@chakrit在评论中提到的,你不能通过在MyStruct上实现json.Marshaler来使其工作,而且在每个使用它的结构体上实现自定义的JSON编组函数可能会更加繁琐。这真的取决于你的用例,是否值得额外的工作,或者你是否愿意在你的JSON中使用空结构体。但是这里是我应用于Result的模式:

type Result struct {
    Data       MyStruct
    Status     string   
    Reason     string    
}

func (r Result) MarshalJSON() ([]byte, error) {
    return json.Marshal(struct {
        Data     *MyStruct   `json:"data,omitempty"`
        Status   string      `json:"status,omitempty"`
        Reason   string      `json:"reason,omitempty"`
    }{
        Data:   &r.Data,
        Status: r.Status,
        Reason: r.Reason,
    })        
}

func (r *Result) UnmarshalJSON(b []byte) error {
    decoded := new(struct {
        Data     *MyStruct   `json:"data,omitempty"`
        Status   string      `json:"status,omitempty"`
        Reason   string      `json:"reason,omitempty"`
    })
    err := json.Unmarshal(b, decoded)
    if err == nil {
        r.Data = decoded.Data
        r.Status = decoded.Status
        r.Reason = decoded.Reason
    }
    return err
}

如果你有很多字段的大型结构体,这可能会变得乏味,特别是在以后更改结构体的实现时,但是除非重写整个json包以适应你的需求(不是一个好主意),否则这几乎是我能想到的唯一方法,同时仍然保持非指针的MyStruct

另外,你不必使用内联结构体,你可以创建命名结构体。不过,我使用带有代码完成的LiteIDE,所以我更喜欢内联以避免混乱。

英文:

As @chakrit mentioned in a comment, you can't get this to work by implementing json.Marshaler on MyStruct, and implementing a custom JSON marshalling function on every struct that uses it can be a lot more work. It really depends on your use case as to whether it's worth the extra work or whether you're prepared to live with empty structs in your JSON, but here's the pattern I use applied to Result:

type Result struct {
    Data       MyStruct
    Status     string   
    Reason     string    
}

func (r Result) MarshalJSON() ([]byte, error) {
    return json.Marshal(struct {
        Data     *MyStruct   `json:"data,omitempty"`
        Status   string      `json:"status,omitempty"`
        Reason   string      `json:"reason,omitempty"`
    }{
        Data:   &r.Data,
        Status: r.Status,
        Reason: r.Reason,
    })        
}

func (r *Result) UnmarshalJSON(b []byte) error {
    decoded := new(struct {
        Data     *MyStruct   `json:"data,omitempty"`
        Status   string      `json:"status,omitempty"`
        Reason   string      `json:"reason,omitempty"`
    })
    err := json.Unmarshal(b, decoded)
    if err == nil {
        r.Data = decoded.Data
        r.Status = decoded.Status
        r.Reason = decoded.Reason
    }
    return err
}

If you have huge structs with many fields this can become tedious, especially changing a struct's implementation later, but short of rewriting the whole json package to suit your needs (not a good idea), this is pretty much the only way I can think of getting this done while still keeping a non-pointer MyStruct in there.

Also, you don't have to use inline structs, you can create named ones. I use LiteIDE with code completion though, so I prefer inline to avoid clutter.

答案3

得分: 9

Data是一个已初始化的结构体,所以它不被认为是空的,因为encoding/json只查看立即值,而不是结构体内部的字段。

不幸的是,从json.Marshaler返回nil目前不起作用:

func (_ MyStruct) MarshalJSON() ([]byte, error) {
    if empty {
        return nil, nil // JSON输入意外结束
    }
    // ...
}

你可以给Result也添加一个编组器,但这并不值得努力。

唯一的选择,正如Matt建议的那样,是将Data设置为指针并将值设置为nil

英文:

Data is an initialized struct, so it isn't considered empty because encoding/json only looks at the immediate value, not the fields inside the struct.

Unfortunately, returning nil from json.Marshaler doesn't currently work:

func (_ MyStruct) MarshalJSON() ([]byte, error) {
	if empty {
		return nil, nil // unexpected end of JSON input
	}
	// ...
}

You could give Result a marshaler as well, but it's not worth the effort.

The only option, as Matt suggests, is to make Data a pointer and set the value to nil.

答案4

得分: 6

这是一个关于Golang的提案,已经活跃了4年多,所以可以安全地假设它不会很快进入标准库。传统的方法是将结构体转换为指向结构体的指针。如果这种方法不可行(或不实际),那么另一种选择是使用支持省略零值结构体的替代json编码器。

我创建了一个Golang json库的镜像(clarketm/json),在应用omitempty标签时,它支持省略零值结构体。该库通过类似于流行的YAML编码器go-yaml的方式(通过递归检查公共结构体字段)来检测零值。

示例:

$ go get -u "github.com/clarketm/json"
import (
	"fmt"
	"github.com/clarketm/json" // 用于替代 `encoding/json` 的库
)

type Result struct {
	Data   MyStruct `json:"data,omitempty"`
	Status string   `json:"status,omitempty"`
	Reason string   `json:"reason,omitempty"`
}

j, _ := json.Marshal(&Result{
    Status: "204",
    Reason: "No Content",
})

fmt.Println(string(j))
// 注意:结果的json中省略了 `data` 字段。
{
  "status": "204",
  "reason": "No Content"
}
英文:

There is an outstanding Golang proposal for this feature which has been active for over 4 years, so at this point, it is safe to assume that it will not make it into the standard library anytime soon. As @Matt pointed out, the traditional approach is to convert the structs to pointers-to-structs. If this approach is infeasible (or impractical), then an alternative is to use an alternate json encoder which does support omitting zero value structs.

I created a mirror of the Golang json library (clarketm/json) with added support for omitting zero value structs when the omitempty tag is applied. This library detects zeroness in a similar manner to the popular YAML encoder go-yaml by recursively checking the public struct fields.

e.g.

$ go get -u "github.com/clarketm/json"
import (
	"fmt"
	"github.com/clarketm/json" // drop-in replacement for `encoding/json`
)

type Result struct {
	Data   MyStruct `json:"data,omitempty"`
	Status string   `json:"status,omitempty"`
	Reason string   `json:"reason,omitempty"`
}

j, _ := json.Marshal(&Result{
    Status: "204",
    Reason: "No Content",
})

fmt.Println(string(j))
// Note: `data` is omitted from the resultant json.
{
  "status": "204"
  "reason": "No Content"
}

答案5

得分: 0

omitempty不仅仅是一个解决方案,如果你想要省略它,那么你需要将代码替换为:

type Result struct {
    Data       *MyStruct  `json:"data,omitempty"`
    Status     string    `json:"status,omitempty"`
    Reason     string    `json:"reason,omitempty"`
}

现在,omitempty将正常工作。谢谢...

另外,假设你想要保持类似于oneOf的条件,意味着只允许填充一个字段到结构体中,那么代码将如下所示:

type Result struct {
    Data1      *MyStruct1    `json:"Data1,omitempty"`
    Data2      *Mystruct2    `json:"Data2,omitempty"`
    Data3      *Mystruct3    `json:"Data3,omitempty"`
}

上述代码只允许你填充一个字段。例如,如果你填充了Data1,那么就不能填充Data2和Data3,否则会报错。

英文:

omitempty is not only the solution if you want to omit it then you have replace code from

type Result struct {
	Data       MyStruct  `json:"data,omitempty"`
	Status     string    `json:"status,omitempty"`
	Reason     string    `json:"reason,omitempty"`
}

To this,

type Result struct {
    Data       *MyStruct  `json:"data,omitempty"`
    Status     string    `json:"status,omitempty"`
    Reason     string    `json:"reason,omitempty"`
}

Now omitempty will work properly
Thanks...

Additionally, suppose if you want to keep condition like oneOf means only one field should be allowed to fill in stuct then the code would looks like,

type Result struct {
    Data1      *MyStruct1    `json:"Data1,omitempty"`
    Data2      *Mystruct2    `json:"Data2,omitempty"`
    Data3      *Mystruct3    `json:"Data3,omitempty"`
}

The above code only allow you to fill only 1 field.
E.g if you are filling Data1 then you can't fill Data2 & Data3, it will throw error for that

huangapple
  • 本文由 发表于 2013年8月7日 02:56:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/18088294.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定