Go1.24からjson:",omitzero"でなんでもomit可能に
json:",omitzero"
なんでもomit可能になる
Go1.24から以下がGo1.24のDraft Release Notesです。現時点(2025-01-18)ではDraftですがリリースされるとURLはそのままでDraftの文言が消されます。
上記の通り、encoding/json
に変更が入ります。
When marshaling, a struct field with the new omitzero option in the struct field tag will be omitted if its value is zero. If the field type has an IsZero() bool method, that will be used to determine whether the value is zero. Otherwise, the value is zero if it is the zero value for its type.
筆者はこれまで下記の記事のとおりにstructなどの値がomitempty
によってomitされないことにあらがおうとごちゃごちゃしてきたわけですが、
これからはstdの範疇でもっと簡単にこれらが実現できるわけです。
この記事は上記の記事群で求めていたテーマの最終着地点です。
環境
# go install golang.org/dl/go1.24rc2@latest
go: downloading golang.org/dl v0.0.0-20250116195134-55ca457114df
# go1.24rc2 download
Downloaded 0.0% ( 3121 / 77743067 bytes) ...
Downloaded 6.0% ( 4685792 / 77743067 bytes) ...
Downloaded 15.3% (11878320 / 77743067 bytes) ...
Downloaded 24.5% (19070832 / 77743067 bytes) ...
Downloaded 33.8% (26262784 / 77743067 bytes) ...
Downloaded 42.9% (33340624 / 77743067 bytes) ...
Downloaded 51.1% (39713952 / 77743067 bytes) ...
Downloaded 60.3% (46907040 / 77743067 bytes) ...
Downloaded 69.6% (54098992 / 77743067 bytes) ...
Downloaded 78.9% (61308464 / 77743067 bytes) ...
Downloaded 88.1% (68500416 / 77743067 bytes) ...
Downloaded 96.3% (74890128 / 77743067 bytes) ...
Downloaded 100.0% (77743067 / 77743067 bytes)
Unpacking /root/sdk/go1.24rc2/go1.24rc2.linux-amd64.tar.gz ...
Success. You may now run 'go1.24rc2'
# go1.24rc2 version
go version go1.24rc2 linux/amd64
前提知識
-
encoding/json
の使い方を知っていること -
JSON
とは何かを知っていること。- とりあえずRFC8259を読めばOK
json:",omitempty"
はstructをomitしない
前提: Go
のstd libraryの範疇ではencoding/json
パッケージを用いてstructなどのdata typeとJSONとの相互変換を行います。
JSON
は、時と場合によってObject
のfieldを省略することがあります。
encoding/json
ではstruct tagでjson:",omitempty"
を指定することで、empty value
であるときfieldの省略(=omit)を行う挙動があります。
empty value
の判定は以下で行われます。
見てのとおり、
- array, map, slice, string: len == 0
- bool, intとそのvariant(int32のような), uintとそのvariant(uint16のような), float32/float64, interface, pointer: zero value
であるときにempty
であるとみなされます。
この条件にstructがないことから、structがemptyとみなされることがないことがわかります。
(chan T
もこの中で無視されていますが、そもそもchanを含むstructのmarshalはサポートされていないためエラーになります。)
課題: time.Timeのような型がomitできない
例えばtime.Time
のようなstructをunderlying typeとする型がomitできません。
time.Time
はstructであり、すべてのfieldがunexportであるので型そのものが値みたいなものですが、今まではomitできませんでした。
omitzero
issue
ということで何年も前からstructも含めてzero valueならomitする機能ほしいよねっていうissueは上がっていました。
ついに上記が採用された形でcloseされました!
大雑把に以下の3つが検討されてomitzero
に終着した形になります。
-
omitempty
の挙動を変える -
MarshalJSON
実装でnil
を返させる or 特殊なエラーを返させる omitnil
omitzero
が現実的になったのはおそらくreflect.Value.IsZero
が最適化されて高速化されたからでしょうね。
実際、isEmptyValue
の実装のなかでIsZero
が呼ばれるようになったのはGo1.22からでGo1.21まででは型ごとに細かいチェックを行っていました。
Go1.22
でCL411478が適用されたことでreflect.Value.IsZero
が高速化されたことでこうなったらようです。
実装
上記のように、json:",omitzero"
がつけられていると、
- 型が
interface { IsZero() bool }
を実装している場合、これがtrueを返すとき - もしくはreflect.Value.IsZeroがtrueを返すとき
のいずれかの時fieldがomitされます。
fieldの型がnon pointerでIsZero
のmethod receiverがpointer typeのときの考慮が特筆すべき点ですね。
挙動
例えば以下のように型を定義します。
type foo struct {
Bar bar `json:",omitzero"`
}
type bar struct {
F1 string
F2 int
}
zero valueのときomitされるのがわかります。
var f foo
bin, err := json.MarshalIndent(f, "", " ")
if err != nil {
panic(err)
}
fmt.Printf("%s\n", bin)
// {}
f.Bar.F1 = "foo"
bin, err = json.MarshalIndent(f, "", " ")
if err != nil {
panic(err)
}
fmt.Printf("%s\n", bin)
// {
// "Bar": {
// "F1": "foo",
// "F2": 0
// }
// }
前述通り、interface { IsZero() bool }
が実装されるとき、こちらが優先して使われます。
time.Time
のIsZero
はwall clockもしくはmonotonic timerがzeroであるときtrueを返します。
time.Time
はfieldに*time.Locationを含むため、zero valueではないがIsZero
がtrueを返すことがあります。
type times struct {
Empty time.Time `json:",omitempty"`
Zero time.Time `json:",omitzero"`
}
以下のように、zero valueでないtime.Time
もomitされます。
t := times{
Empty: time.Time{}.In(time.Local),
Zero: time.Time{}.In(time.Local),
}
bin, err := json.MarshalIndent(t, "", " ")
if err != nil {
panic(err)
}
fmt.Printf("is zero: %t\n", reflect.ValueOf(t.Zero).IsZero())
fmt.Printf("%s\n", bin)
// is zero: false
// {
// "Empty": "0001-01-01T00:00:00Z"
// }
v2とのcompatibility
discussion: encoding/json/v2で述べられているexperimental実装でもほぼ同じ実装になっています。
このコメントからencoding/json
へのomitzero
の追加は、立ち位置的には仮想的なv2からのバックポートということになります。
JSONのT | null | undefinedはOption[Option[T]]で表現できる
Go1.23
以前ではJSONのT | null | undefinedを単なるstruct fieldで表現するには[]Option[T]
を用いる必要がありました。
これは前述のとおり,omitempty
で任意の型を収められるcontainer typeをomitさせようと思うとslice([]T
), map(map[K]V
)を用いる必要があったためです。
下記の記事であれこれ述べました。
この方法には値がuncomparableになってしまうという明確な問題がありました。
Go1.24
以降ではomitzero
が実装されるためOption[Option[T]]
で同じ目的を達成できます。
この場合、Option
の実装をcomparableにしておけばT
がcomparableである限りOption[Option[T]]
もcomparableとなります。
型の定義
以下のようにstructをunderlyingとしたOption[T]
を定義し、
これを2段重ねてOption[Option[T]]
とすることでT | null | undefinedを表現し分けられるようになります。
そしてこの型にIsZero
を実装します。
中身はboolean flagを確認するだけです。
,omitzero
がこの型のfieldをomitできるのでこれでよくなりました!
sample
以下のsnippetをgo1.24rc2 run github.com/ngicks/und/example@v1.0.0-alpha8
で実行すると、コメントされたような結果がprintされます。
und.Und
がOption[Option[T]]
、sliceund.Und
が[]Option[T]
をベースとする型です。
package main
import (
"encoding/json"
"fmt"
"github.com/ngicks/und"
"github.com/ngicks/und/elastic"
"github.com/ngicks/und/option"
"github.com/ngicks/und/sliceund"
sliceelastic "github.com/ngicks/und/sliceund/elastic"
)
type sample1 struct {
Foo string
Bar und.Und[nested1] `json:",omitzero"`
Baz elastic.Elastic[nested1] `json:",omitzero"`
Qux sliceund.Und[nested1] `json:",omitzero"`
Quux sliceelastic.Elastic[nested1] `json:",omitzero"`
}
type nested1 struct {
Bar und.Und[string] `json:",omitzero"`
Baz elastic.Elastic[int] `json:",omitzero"`
Qux sliceund.Und[float64] `json:",omitzero"`
Quux sliceelastic.Elastic[bool] `json:",omitzero"`
}
type sample2 struct {
Foo string
Bar und.Und[nested2] `json:",omitempty"`
Baz elastic.Elastic[nested2] `json:",omitempty"`
Qux sliceund.Und[nested2] `json:",omitempty"`
Quux sliceelastic.Elastic[nested2] `json:",omitempty"`
}
type nested2 struct {
Bar und.Und[string] `json:",omitempty"`
Baz elastic.Elastic[int] `json:",omitempty"`
Qux sliceund.Und[float64] `json:",omitempty"`
Quux sliceelastic.Elastic[bool] `json:",omitempty"`
}
func main() {
s1 := sample1{
Foo: "foo",
Bar: und.Defined(nested1{Bar: und.Defined("foo")}),
Baz: elastic.FromValue(nested1{Baz: elastic.FromOptions(option.Some(5), option.None[int](), option.Some(67))}),
Qux: sliceund.Defined(nested1{Qux: sliceund.Defined(float64(1.223))}),
Quux: sliceelastic.FromValue(nested1{Quux: sliceelastic.FromOptions(option.None[bool](), option.Some(true), option.Some(false))}),
}
var (
bin []byte
err error
)
bin, err = json.MarshalIndent(s1, "", " ")
if err != nil {
panic(err)
}
fmt.Printf("marshaled by with omitzero =\n%s\n", bin)
// see? undefined (=zero value) fields are omitted with json:",omitzero" option.
// ,omitzero is introduced in Go 1.24. For earlier version Go, see example of sample2 below.
/*
marshaled by with omitzero =
{
"Foo": "foo",
"Bar": {
"Bar": "foo"
},
"Baz": [
{
"Baz": [
5,
null,
67
]
}
],
"Qux": {
"Qux": 1.223
},
"Quux": [
{
"Quux": [
null,
true,
false
]
}
]
}
*/
s2 := sample2{
Foo: "foo",
Bar: und.Defined(nested2{Bar: und.Defined("foo")}),
Baz: elastic.FromValue(nested2{Baz: elastic.FromOptions(option.Some(5), option.None[int](), option.Some(67))}),
Qux: sliceund.Defined(nested2{Qux: sliceund.Defined(float64(1.223))}),
Quux: sliceelastic.FromValue(nested2{Quux: sliceelastic.FromOptions(option.None[bool](), option.Some(true), option.Some(false))}),
}
bin, err = json.MarshalIndent(s2, "", " ")
if err != nil {
panic(err)
}
fmt.Printf("marshaled with omitempty =\n%s\n", bin)
// You see. Types defined under ./sliceund/ can be omitted by encoding/json@go1.23 or earlier.
/*
marshaled with omitempty =
{
"Foo": "foo",
"Bar": {
"Bar": "foo",
"Baz": null
},
"Baz": [
{
"Bar": null,
"Baz": [
5,
null,
67
]
}
],
"Qux": {
"Bar": null,
"Baz": null,
"Qux": 1.223
},
"Quux": [
{
"Bar": null,
"Baz": null,
"Quux": [
null,
true,
false
]
}
]
}
*/
}
sliceやarrayに含まれるundefinedであるund.Und[T]
がnull
を出力するのはECMAのJSON.stringify
と挙動が一致しているためちょうどいい感じになっています。
おわりに
もうomitempty
つかわなくていいかも。
Go
も歴史が深くなってきて、昔はこうだったけど今はこうすべき見たいなtipsがいくつか出てきました。
例えばfor k, v := range
でiterator variableをshadowingしたほうがよかったのはGo1.22で修正されたのでやらなくてよくなりました。
omitzero
もそう言ったものの一つです。omitempty
に比べて仕様が明快なのでこっちを使っていくほうが初心者には優しいと思います。(実際筆者はemptyの判定の条件を初めて見たとき混乱しました。)
そういうののを集めたtips集を作ってメンテしていったほうがいいかもしれませんね。多分文法とstd libraryの範疇に話をとどめればそんなに大きなものにはなりませんし。
Discussion