9月10日、Go公式ブログが「A new experimental Go API for JSON」と題した記事を公開した。この記事では、Go 1.25で実験的に導入されたencoding/json/v2
とencoding/json/jsontext
という新API群の狙いと設計、移行方法について詳しく紹介されている。

以下に、その内容を紹介する。
なぜJSON APIの“v2”が必要なのか
JSONはWebで最も広く使われるデータ形式であり、Goのencoding/json
は長年にわたり多くのプロジェクトを支えてきた。一方で、標準の厳格化(RFC 8259)や実運用で顕在化した要件に対し、既存API(以下v1と呼ぶ)の互換性制約のため根治的な改善が困難になっていた。そこで、挙動の見直し・性能改善・拡張性を両立するため、実験的な新APIとしてencoding/json/jsontext
(構文処理の土台)とencoding/json/v2
(セマンティクス=Go値との対応付け)を導入したという位置づけである。
v1で指摘されてきた問題点
挙動上の欠陥
JSON構文の扱いが不正確
- 無効なUTF-8を受け入れてしまい、静かにデータ破損を招きうる。標準は有効なUTF-8のみを要求する。
- 重複キー(同一オブジェクト内の同名メンバー)を許容し意味が不定。これはセキュリティ上の悪用余地(例:CVE-2017-12635)を生むため、既定で拒否すべきである。
nilスライス/マップの“null”化
相手実装がnull
を配列・オブジェクトに受け付けない場合に不整合を引き起こす。多くのGoユーザーは空配列/空オブジェクトとしての直列化を望んでいる。大文字小文字を無視したフィールド解決
既定のケース非依存マッチは意外性・脆弱性・性能面の懸念につながる。MarshalJSON
(ポインタレシーバ)の不整合呼び出し
既存アプリが依存する挙動のため、互換性を壊さずに修正しづらい。
APIの使い勝手の制約
io.Reader
からの正しいUnmarshalが難しく、json.NewDecoder(r).Decode(v)
が末尾のゴミを見逃しがち。Encoder
/Decoder
に設定できるオプションが、Marshal
/Unmarshal
関数やMarshaler
/Unmarshaler
実装へ伝播しない。Compact
/Indent
/HTMLEscape
がbytes.Buffer
に固定され柔軟性に欠ける。
公開APIが課す性能上の限界
MarshalJSON
は割当てと検証・再整形を強要
返す[]byte
の割当て、正当性検証、インデント整形のやり直しが必要。UnmarshalJSON
は“完全な1値”を要求
末尾境界検出のため二重パースが起こりうる。- 真のストリーミング不足
Encoder
/Decoder
は実質的に値全体をバッファリングし、トークン読み書きの効率も低い。
既存v1を直接直すのが難しい理由
Go 1互換性ポリシーのもとでは、挙動変更が困難で部分的追加に留まる。MarshalV2
のような並行名前空間をv1内部に作るのは混乱を招くため、パッケージとしてのv2分離が選択された。
設計の基盤:encoding/json/jsontext
jsontext
は構文解析のみを扱う下位レイヤで、セマンティクス(marshal/unmarshal)は上位のjson/v2
が担う。これにより、厳格な構文処理と真のストリーミング化を実現する。

基本API(jsontext)
package jsontext
type Encoder struct { ... }
func NewEncoder(io.Writer, ...Options) *Encoder
func (*Encoder) WriteValue(Value) error
func (*Encoder) WriteToken(Token) error
type Decoder struct { ... }
func NewDecoder(io.Reader, ...Options) *Decoder
func (*Decoder) ReadValue() (Value, error)
func (*Decoder) ReadToken() (Token, error)
type Kind byte
type Value []byte
func (Value) Kind() Kind
type Token struct { ... }
func (Token) Kind() Kind
ポイントは以下の通りだ。
- 構文専任:リフレクション非依存で、値(
Value
)やトークン(Token
)をストリーミング処理。 - 真のストリーミング:
Encoder
/Decoder
がトークン単位で効率的に読み書きでき、上位の二重パースや不要な割当てを排除。
さらに、型側のフックとしてストリーミング適合の新インターフェース(後述)を導入する布石となる。
新API:encoding/json/v2
jsontext
の上に、従来の使い勝手を保ちながら挙動・性能・拡張性を改善したv2
がある。
基本API(セマンティクスレイヤ)
package json
func Marshal(in any, opts ...Options) (out []byte, err error)
func MarshalWrite(out io.Writer, in any, opts ...Options) error
func MarshalEncode(out *jsontext.Encoder, in any, opts ...Options) error
func Unmarshal(in []byte, out any, opts ...Options) error
func UnmarshalRead(in io.Reader, out any, opts ...Options) error
func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) error
Marshal
/Unmarshal
はv1に近い形を保ちつつ、可変オプションを受け取る。MarshalWrite
/UnmarshalRead
はio.Writer/Reader
に直接書き読み。MarshalEncode
/UnmarshalDecode
はjsontext
のEncoder
/Decoder
に直結し、実装の本体となる。
型による表現のカスタマイズ
v1と同様に、特定のインターフェースを満たすことで、型が独自の JSON 表現を定義できる。
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type MarshalerTo interface {
MarshalJSONTo(*jsontext.Encoder) error
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
type UnmarshalerFrom interface {
UnmarshalJSONFrom(*jsontext.Decoder) error
}
- 既存の
Marshaler
/Unmarshaler
に加え、**MarshalerTo
/UnmarshalerFrom
**を追加。
ストリーミング・オプション伝播・割当て削減が可能になる。
呼び出し側によるJSON表現の上書き
v2では、呼び出し元が任意の型のカスタム JSON 表現を指定することもできる。この場合、呼び出し元が指定した関数は、型定義のメソッドまたは型のデフォルトの表現よりも優先される。
func WithMarshalers(*Marshalers) Options
type Marshalers struct { ... }
func MarshalFunc[T any](fn func(T) ([]byte, error)) *Marshalers
func MarshalToFunc[T any](fn func(*jsontext.Encoder, T) error) *Marshalers
func WithUnmarshalers(*Unmarshalers) Options
type Unmarshalers struct { ... }
func UnmarshalFunc[T any](fn func([]byte, T) error) *Unmarshalers
func UnmarshalFromFunc[T any](fn func(*jsontext.Decoder, T) error) *Unmarshalers
MarshalFunc
とMarshalToFunc
は、 Marshal
にWithMarshalers
とともに渡すことで、特定の型のマーシャリングをオーバーライドできるカスタムマーシャラーを構築する。同様に、UnmarshalFunc
とUnmarshalFromFunc
は、 UnMarshal
にWithUnmarshalers
とともに渡すことで、カスタマイズされたアンマーシャリング動作をサポートする。
v1からの主な差分
v2は、以下の点でv1と動作が異なる。
- 無効なUTF-8はエラーにする。
- 重複キーはエラーにする。
- nilスライス/マップは空配列/空オブジェクトとしてmarshalする。
- フィールド名の照合は大文字小文字を区別する。
omitempty
の再定義:エンコード結果が“空”(null
、""
、[]
、{}
)になるなら省略。time.Duration
の既定表現なし:エラーとし、オプションで方針を選べる。
※多くの差分はタグやオプションでv1互換に戻すことも可能で、段階的移行を支える設計である。
またv2は、v1に比べてパフォーマンス向上も見られる。
Marshal
の性能は概ねv1と同等(ケースにより前後)。Unmarshal
は最大10倍程度の高速化がベンチマークで示されている。- 既存の
Marshaler
/Unmarshaler
実装を*To
/*From
版へ移行すると、より大きな改善が見込める。
またv1も内部的にv2を利用するようになり、以下のような利点を享受できる。
- 段階的移行を許容(v1/v2挙動をオプションで混在可能)。
- 機能継承(
inline
/format
タグ、MarshalJSONTo
/UnmarshalJSONFrom
など)。 - 保守負荷の低減(1つの実装で両者を賄う)。
パッケージ全体としてのv1の廃止は想定せず、移行は推奨であって強制ではないという姿勢である。
現在、実際にJSON API v2を使ってみるには、利用にはGo 1.25で実験フラグを有効化する必要がある(encoding/json/jsontext
とencoding/json/v2
は既定では非公開)
GOEXPERIMENT=jsonv2 go test ./...
jsonv2
を有効にすると、v1の内部実装もv2ベースに差し替わる。コード変更なしにテストを回し、差分(主にエラーメッセージの文言など)を確認してフィードバックするのが推奨手順である。コミュニティからのフィードバック次第では、次期バージョンGo 1.26でのv2導入もありうるとされている。
詳細はA new experimental Go API for JSONを参照していただきたい。