【Go言語】定義型に対するjson.Marshalではomitemptyが効かない件
これは ZOZO Advent Calendar 2022 カレンダーVol.5の6日目の記事です。
まとめ
- 定義型[1]に対するJSONタグでomitemptyを付与しても、json.Marshalでフィールドがエンコードされてしまう
- omitemptyを効かせたい場合は、ポインタ型を使うか埋め込み型でJSONタグを上書きする
omitemptyの仕組み
以下の構造体をjson.Marshal()
する時を例に解説します。
type myStruct struct {
Hoge string `json:"hoge,omitempty"`
}
func main() {
bytes, _ := json.Marshal(myStruct{Hoge: ""})
fmt.Println(string(bytes))
}
json.Marshal()
を呼び出すとjson.structEncoder
のインスタンスが作られますが、この構造体の中にはstructの各フィールドの情報が保持されています。
JSONタグにomitemptyを付与した場合、このfield
構造体のomitempty
フィールドがtrue
になります。
json.Marshal(myStruct{Hoge: ""})
を呼び出した時のfield
インスタンスをdebuggerで見ると、意図通りomitEmpty
フィールドがtrue
になっています。
{
name:"hoge",
nameNonEsc:"\"hoge\":",
nameNonHTML:"\"hoge\":",
tag:true,
omitEmpty:true
}
この後にencoding/json
パッケージでは、omitempty
フィールドがtrue
であり値が空であると判定できるフィールドのエンコードはスキップします。
このスキップ処理の判定で使われているjson.isEmptyValue()
では、対象のvalueの型を見てから空値の判定をしています。
今回json.Marshal()
に渡している引数myStruct{Hoge: ""}
のHoge
フィールドでは、v.Kind() == reflect.String
でv.Len() == 0
を満たすので、空値であると判定されてエンコードはスキップされます。
$ go run main.go
{}
もうお気づきかと思いますがjson.isEmptyValud()
で空値と判定される型は予め定義されたいくつかの型に限られており、それ以外の型ではいかなる場合も空値と判定させることは出来なそうです。
試しにHoge
フィールドをtime.Time
型のフィールドに変えて再実行してみます。
type myStruct struct {
Timestamp time.Time `json:"timestamp,omitempty"`
}
func main() {
var t time.Time
fmt.Println(t.IsZero())
bytes, _ := json.Marshal(myStruct{Timestamp: t})
fmt.Println(string(bytes))
}
$ go run main.go
true
{"timestamp":"0001-01-01T00:00:00Z"}
フィールドの値を(time.Time).IsZero()
がtrue
になるような値にした場合もjsonではtimestamp
プロパティがエンコードされてしまっています。
上記で説明した内容を知っていれば当たり前ですが、time.Time
型はreflect.Struct
でありjson.isEmptyValue
の結果がいかなる場合もfalse
になります。よって、time.Time
型のような定義型はomitemtpy
を効かせることが出来ません。
定義型でomitemptyを使う
定義型ではomitempty
が効かない理由を説明しましたが、どうにかして適用させたい場合があるかと思います。
その場合は以下のような方法でomitempty
の挙動を再現できそうです。
- ポインタ型を使う
- 埋め込み型でJSONタグを上書きする
ポインタ型を使う
json.isEmptyValud
ではreflect.Struct
に対する空値判定はしていませんが、reflect.Pointer
に対する空値判定はしています。
なのでtime.Time
型の代わりに、*time.Time
型を使うことでomitempty
を適用することが出来ます。
type myStruct struct {
Timestamp *time.Time `json:"timestamp,omitempty"`
}
func main() {
bytes, _ := json.Marshal(myStruct{Timestamp: nil})
fmt.Println(string(bytes))
}
$ go run main.go
{}
埋め込み型でJSONタグを上書きする
Goには埋め込み型(Embedding Type)があります。埋め込み型は構造体やインターフェースに他の型を埋め込んで実装の一部を借用する機能です。
埋め込み型を使うと埋め込み先と埋め込み元の型で名前競合が発生する可能性がありますが、常にネストが浅い方の名前を優先することで名前解決を行なっています。
Embedding types introduces the problem of name conflicts but the rules to resolve them are simple. First, a field or method X hides any other item X in a more deeply nested part of the type. If log.Logger contained a field or method called Command, the Command field of Job would dominate it.
JSONタグにもこの名前解決のルールが適用されるため、それを利用してomitempty
が効くような型のフィールドで上書きすることが出来ます。
呼び出し元でハンドリングしてあげる必要がありますが、以下のようにしてomitemtpy
の挙動を再現できます。
type myStruct struct {
Timestamp time.Time `json:"timestamp"`
}
func main() {
var t time.Time
m := myStruct{Timestamp: t}
var bytes []byte
if m.Timestamp.IsZero() {
bytes, _ = json.Marshal(struct {
*myStruct
T json.RawMessage `json:"timestamp,omitempty"`
}{myStruct: &m})
} else {
bytes, _ = json.Marshal(m)
}
fmt.Println(string(bytes))
}
$ go run main.go
{}
myStruct
のTimestamp
型がゼロ値の場合は、同じJSONタグを持ったフィールドがあるstructにmyStruct
を埋め込んであげることで、myStruct
の方のtimestamp
プロパティを無視しています。
-
正確には、
json.isEmptyValue
がいかなる場合もfalseになってしまうような型です ↩︎
Discussion