Open9

【Go】encoding/json/v2を試してみる

aiiroaiiro

encoding/json/v2の使用方法

https://pkg.go.dev/encoding/json/v2#pkg-overview

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

https://pleiades.io/help/go/configuring-build-constraints-and-vendoring.html#using_go_experiments
Golandで試すには、Settings > Build Tags > Experimentsにjsonv2設定します。

aiiroaiiro

omitzeroの挙動を試してみる

https://pkg.go.dev/encoding/json/v2#hdr-JSON_Representation_of_Go_structs

各型がomitzeroやomitemptyでどうなるかは、サンプルコードがわかりやすい。
https://pkg.go.dev/encoding/json/v2#example-package-OmitFields

つぎのコードを使用して、マーシャルしたときの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))
	}
}
aiiroaiiro

この設定をした結果は、

	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"}
aiiroaiiro

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"は出力されていません。

aiiroaiiro

つぎに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}
aiiroaiiro

ZeroNumberという構造体を定義していて、これはint64かfloat64のみを設定できるようにしています。
また、ZeroNumberは構造体のため、先に使用したZeroStringと異なる点として、 type MarshalerTo interface のMarshalJSONToを実装しています。

https://pkg.go.dev/encoding/json/v2#pkg-types

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"`
}
aiiroaiiro
{
    Int:          0,
    IntPtr:       nil,
    IntIsZero:    ZeroNumber[int64]{0},
    IntPtrIsZero: nil,
},

の出力が

{"intIsZero":0}

となっているのは、ZeroNumberで実装したIsZeroがnilの場合のみtrueを返す判定となっているので、nilではなく0が設定されているIntIsZeroは省略されなかったという結果になりました。

aiiroaiiro

https://pkg.go.dev/encoding/json/v2#pkg-types

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
}