Go1.24からjson:",omitzero"でなんでもomit可能に
Go1.24からjson:",omitzero"なんでもomit可能になる
以下が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