【Go】encoding/json/v2を試してみる
encoding/json/v2の使用方法
This package (encoding/json/v2) is experimental, and not subject to the Go 1 compatibility promise. It only exists when building with the GOEXPERIMENT=jsonv2 environment variable set. Most users should use encoding/json.
json/v2を使用するには上記のようにGOEXPERIMENTを使用します。
go runで試す場合は、つぎのようにコマンドを実行することができます。
GOEXPERIMENT=jsonv2 go run main.go
Golandで試すには、Settings > Build Tags > Experimentsにjsonv2設定します。
omitzeroの挙動を試してみる
各型がomitzeroやomitemptyでどうなるかは、サンプルコードがわかりやすい。
つぎのコードを使用して、マーシャルしたときのomitzeroの挙動を試してみます。
import (
"encoding/json/jsontext"
"encoding/json/v2"
"fmt"
"log"
)
type ZeroString string
func (z *ZeroString) IsZero() bool {
if z == nil {
return true
}
if *z == "undefined" {
return true
}
return false
}
type ZeroStringStruct struct {
Str string `json:"str,omitzero"`
StrPtr *string `json:"strPtr,omitzero"`
StrIsZero ZeroString `json:"strIsZero,omitzero"`
StrPtrIsZero *ZeroString `json:"strPtrIsZero,omitzero"`
}
type Number interface {
int64 | float64
}
type ZeroNumber[T Number] struct {
Value T
}
func (z *ZeroNumber[T]) IsZero() bool {
if z == nil {
return true
}
return false
}
func (z *ZeroNumber[T]) MarshalJSONTo(enc *jsontext.Encoder) error {
return json.MarshalEncode(enc, z.Value)
}
type ZeroNumberStruct struct {
Int int64 `json:"int,omitzero"`
IntPtr *int64 `json:"intPtr,omitzero"`
IntIsZero ZeroNumber[int64] `json:"intIsZero,omitzero"`
IntPtrIsZero *ZeroNumber[int64] `json:"intPtrIsZero,omitzero"`
}
func toPtr[T any](v T) *T {
return &v
}
func main() {
strInputs := []*ZeroStringStruct{
{
Str: "hello world",
StrPtr: toPtr("hello world"),
StrIsZero: "hello world",
StrPtrIsZero: toPtr(ZeroString("hello world")),
},
{
Str: "",
StrPtr: nil,
StrIsZero: "",
StrPtrIsZero: nil,
},
{
Str: "undefined",
StrPtr: toPtr("undefined"),
StrIsZero: "undefined",
StrPtrIsZero: toPtr(ZeroString("undefined")),
},
}
for _, input := range strInputs {
b, err := json.Marshal(input)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
}
numberInputs := []*ZeroNumberStruct{
{
Int: 123,
IntPtr: toPtr(int64(123)),
IntIsZero: ZeroNumber[int64]{123},
IntPtrIsZero: &ZeroNumber[int64]{123},
},
{
Int: 0,
IntPtr: nil,
IntIsZero: ZeroNumber[int64]{0},
IntPtrIsZero: nil,
},
}
for _, input := range numberInputs {
b, err := json.Marshal(input)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
}
}
この設定をした結果は、
strInputs := []*ZeroStringStruct{
{
Str: "hello world",
StrPtr: toPtr("hello world"),
StrIsZero: "hello world",
StrPtrIsZero: toPtr(ZeroString("hello world")),
},
{
Str: "",
StrPtr: nil,
StrIsZero: "",
StrPtrIsZero: nil,
},
{
Str: "undefined",
StrPtr: toPtr("undefined"),
StrIsZero: "undefined",
StrPtrIsZero: toPtr(ZeroString("undefined")),
},
}
以下のようになりました。
{"str":"hello world","strPtr":"hello world","strIsZero":"hello world","strPtrIsZero":"hello world"}
{"strIsZero":""}
{"str":"undefined","strPtr":"undefined"}
IsZero()でnilか"undefined"の場合にtrueを返すようにしているため、上記の結果になります。
type ZeroString string
func (z *ZeroString) IsZero() bool {
if z == nil {
return true
}
if *z == "undefined" {
return true
}
return false
}
2つめの結果の
{"strIsZero":""}
では、 "strIsZero" はブランクではあるが、nilではないため、マーシャルした結果に出力されています。
3つめの結果の
{"str":"undefined","strPtr":"undefined"}
では、 "undefined" が設定されているため、"strIsZero"と"strPtrIsZero"は出力されていません。
つぎにnumberInputsをマーシャルした結果は、
numberInputs := []*ZeroNumberStruct{
{
Int: 123,
IntPtr: toPtr(int64(123)),
IntIsZero: ZeroNumber[int64]{123},
IntPtrIsZero: &ZeroNumber[int64]{123},
},
{
Int: 0,
IntPtr: nil,
IntIsZero: ZeroNumber[int64]{0},
IntPtrIsZero: nil,
},
}
以下になりました。
{"int":123,"intPtr":123,"intIsZero":123,"intPtrIsZero":123}
{"intIsZero":0}
ZeroNumberという構造体を定義していて、これはint64かfloat64のみを設定できるようにしています。
また、ZeroNumberは構造体のため、先に使用したZeroStringと異なる点として、 type MarshalerTo interface のMarshalJSONToを実装しています。
type Number interface {
int64 | float64
}
type ZeroNumber[T Number] struct {
Value T
}
func (z *ZeroNumber[T]) IsZero() bool {
if z == nil {
return true
}
return false
}
func (z *ZeroNumber[T]) MarshalJSONTo(enc *jsontext.Encoder) error {
return json.MarshalEncode(enc, z.Value)
}
type ZeroNumberStruct struct {
Int int64 `json:"int,omitzero"`
IntPtr *int64 `json:"intPtr,omitzero"`
IntIsZero ZeroNumber[int64] `json:"intIsZero,omitzero"`
IntPtrIsZero *ZeroNumber[int64] `json:"intPtrIsZero,omitzero"`
}
{
Int: 0,
IntPtr: nil,
IntIsZero: ZeroNumber[int64]{0},
IntPtrIsZero: nil,
},
の出力が
{"intIsZero":0}
となっているのは、ZeroNumberで実装したIsZeroがnilの場合のみtrueを返す判定となっているので、nilではなく0が設定されているIntIsZeroは省略されなかったという結果になりました。
v1:
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
v2:
type MarshalerTo interface {
MarshalJSONTo(*jsontext.Encoder) error
}
v2のMarshalerToについて、LLMでサンプルコードを作りながら調べたのでメモ。
いったん以下のような理解をした。
v1のMarshalJSONは、jsonをマーシャルするときに、jsonがネストしている等で、複数回MarshalJSONが呼ばれたときに、その分の[]byteを生成する。
一方、MarshalJSONToは、ネストしている場合であっても同じEncoderを使ってエンコードする。
MarshalerTo is implemented by types that can marshal themselves. It is recommended that types implement MarshalerTo instead of Marshaler since this is both more performant and flexible.
func (c *Company) MarshalJSONTo(enc *jsontext.Encoder) error {
enc.WriteToken(jsontext.BeginObject)
json.MarshalEncode(enc, "name")
json.MarshalEncode(enc, c.Name)
json.MarshalEncode(enc, "employees")
enc.WriteToken(jsontext.BeginArray)
for i, emp := range c.Employees {
json.MarshalEncode(enc, emp)
}
enc.WriteToken(jsontext.EndArray)
enc.WriteToken(jsontext.EndObject)
return nil
}
func (e *Employee) MarshalJSONTo(enc *jsontext.Encoder) error {
enc.WriteToken(jsontext.BeginObject)
json.MarshalEncode(enc, "name")
json.MarshalEncode(enc, e.Name)
json.MarshalEncode(enc, "age")
json.MarshalEncode(enc, e.Age)
enc.WriteToken(jsontext.EndObject)
return nil
}