encoding/json/v2: new API for encoding/json
要約
概要
encoding/json/v2 および encoding/json/jsontext という2つの新しい標準ライブラリパッケージを追加するプロポーザルです。これはGo言語のJSONシリアライズAPIを15年ぶりに抜本的に刷新し、長年の設計上の問題を解決することを目的としています。
ステータス変更
likely_accept → accepted
提案者の @dsnet(Joe Tsai)がプロポーザルレビューグループに対してAPI全体のウォークスルーを実施した。jsontextの設計思想(パフォーマンス優先)、統合オプション型の設計(長期間の試行錯誤の末に現在の形に落ち着いた点)、UnmarshalReadのEOF自動確認、omitemptyの再定義など主要な設計決定について部屋全員が同意し、likely acceptの判定を受けた。その後、追加のコンセンサス変化がなかったため正式にacceptedとなった。プロポーザルはGo 1.26で GOEXPERIMENT=jsonv2 として標準ライブラリに実装される予定(Go 1.27での安定リリースを目標)。
技術的背景
現状の問題点
encoding/json v1は2011年のGoリリース当初から存在するが、長年の運用で多数の設計上の問題が蓄積している。Go 1の後方互換性保証によりこれらを修正することが不可能になっており、v2の新設が必要とされてきた。
主な問題点:
- 不正なUTF-8文字を含む文字列を黙って受け入れる(Unicodeの置換文字に変換)
- JSONオブジェクト内の重複キーを許容する(RFC 7493違反)
- 構造体フィールドのマッチングが大文字小文字を区別しない(意図しない値の書き込みリスク)
nilスライスやnilマップをJSONのnullとしてシリアライズする([]や{}ではなく)MarshalJSON/UnmarshalJSONインターフェースが全データをバッファリングする必要があり、真のストリーミング処理ができないMarshal/Unmarshal関数に振る舞いを変えるオプションを渡せないio.Readerからのアンマーシャル時、EOF確認を怠るよくあるミスを防げない
提案された解決策
2つの新パッケージを追加する:
encoding/json/jsontext: JSONの文法処理だけを担うパッケージ。Goのリフレクションに依存せず、トークン単位・値単位での読み書きをストリーミングで行える低レベルAPI。Encoder、Decoder、Token、Value、Kindの各型で構成される。
encoding/json/v2: 従来のencoding/jsonの第2メジャーバージョン。jsontextの上に実装されており、Goの値とJSONデータの変換(マーシャル/アンマーシャル)を担う。
さらに、既存のencoding/json v1はv2の上に再実装される。これにより、v1ユーザーもv2の実装から恩恵を受けつつ、後方互換性が維持される。
これによって何ができるようになるか
コード例
// Before: v1でのJSONマーシャル(オプション渡し不可)
data, err := json.Marshal(v)
// nullの問題: var s []string = nil → "null" が出力される
// After: v2でのJSONマーシャル(オプション付き)
import jsonv2 "encoding/json/v2"
import "encoding/json/jsontext"
// nil スライスは空配列として出力される(デフォルト)
data, err := jsonv2.Marshal(v)
// → []string(nil) は "[]" として出力される
// HTMLエスケープしたい場合は明示的にオプションを渡す
data, err = jsonv2.Marshal(v, jsontext.EscapeForHTML(true))
// Before: v1では型ごとのカスタムマーシャラーが全バッファを要求
func (t MyType) MarshalJSON() ([]byte, error) {
return []byte(`"custom"`), nil
}
// After: v2では真のストリーミングマーシャラーが使用可能
func (t MyType) MarshalJSONTo(enc *jsontext.Encoder) error {
return enc.WriteToken(jsontext.String("custom"))
}
// Before: v1では呼び出し元が型に対するカスタム動作を上書きできない
// After: v2ではWithMarshalersで任意の型をカスタマイズ可能
import jsonv2 "encoding/json/v2"
marshalers := jsonv2.MarshalToFunc(func(enc *jsontext.Encoder, t time.Time) error {
return enc.WriteToken(jsontext.String(t.Format(time.RFC3339)))
})
data, err := jsonv2.Marshal(v, jsonv2.WithMarshalers(marshalers))
主なユースケース
- RFC準拠が必要なサービス: v2はデフォルトでRFC 8259/7493に準拠し、不正UTF-8や重複キーをエラーにするため、仕様に忠実なJSON処理が必要なAPIサーバー開発に有用。
- 大規模JSONのストリーミング処理:
MarshalerTo/UnmarshalerFromインターフェースとjsontextの低レベルAPIにより、メモリに全体をロードせず大規模JSONを処理できる。ベンチマークでは最大10倍の速度向上も確認されている。 - v1からの段階的移行:
jsonv1.DefaultOptionsV1()をjsonv2.Marshalに渡すことでv1と同一の振る舞いを再現でき、オプションを一つずつ外すことで段階的にv2動作へ移行できる。
議論のハイライト
- オプション設計の試行錯誤: 単一の
Options型を構文層(jsontext)と意味論層(json/v2)で共有する設計は、長期間の代替案検討を経て最終的に採用された。後置の可変長引数...Optionsを受け取る形式で、後から渡したオプションが優先される。 - 後方互換性の維持戦略: v1の振る舞いの多くが「実質的なバグ」でありながら、Hyrumの法則により安定した動作として定着してしまっているため、v1のすべての動作を再現するための
jsonv1.WithLegacySemantics系オプション群が多数必要になった。提案者も「悲しい」と述べているが避けられない現実。 omitemptyの再定義: v1ではGoの型システムで「空」を定義(false、0、nilポインタなど)していたが、v2ではJSONの型システムで再定義(JSON null、空文字列、空オブジェクト、空配列)。Goのboolや数値型でのomitemptyはomitzeroへの移行が推奨される。time.Durationのデフォルト表現の廃止: v1ではナノ秒の数値として出力していたが、v2ではデフォルト表現がなくランタイムエラーとなる。jsonv1.FormatDurationAsNanoオプションで従来動作を維持可能。今後のフォローアップ提案に委ねられる。- スコープの絞り込み: 当初の議論からいくつかの機能(ユーザー定義オプション、
[]byte/[N]byteへのformat:stringサポートなど)は初期リリースから意図的に除外された。「既に大きすぎる提案にこれ以上機能追加はしない」という方針のもと、後続の個別提案として分離された。