encoding/json/v2: new API for encoding/json
要約
概要
encoding/json/v2 および encoding/json/jsontext という2つの新パッケージを Go 標準ライブラリに追加するproposalです。これは Go の JSON 処理を根本から再設計する、標準ライブラリ史上最大規模のパッケージ改訂であり、長年の議論(#63397)を経て正式提案に昇格しました。
ステータス変更
(新規) → active
2026年4月15日のProposal Review Meeting(@aclements、@adonovan、@cherrymui、@griesemer、@ianlancetaylor、@neild、@rolandshoemaker 出席)において、activeカラムに追加されました。ミーティング議事録では "added to minutes" と記録されており、週次レビュー会議での継続的なレビュー対象となります。提案はすでに github.com/go-json-experiment/json として動作する実装が存在しており、GopherConでの発表("The Future of JSON in Go")も行われていることから、正式なProposalとしての審査段階に入ったと判断されたと考えられます。
技術的背景
現状の問題点
v1 encoding/json には多くの設計上の問題が蓄積されており、後方互換性の制約から修正が困難な状態です。主な問題点は以下のとおりです。
- フィールド名の大文字小文字を区別しないマッチング(セキュリティ・パフォーマンス上の問題)
- nilスライス・nilマップがJSONのnullとしてエンコードされる(直感に反する挙動)
- 重複したJSONオブジェクトのキーが許容される(RFC違反)
- 無効なUTF-8がエラーなく変換される
omitemptyの定義がGoの型システムに基づくため、直感的でない(例:falseや0が省略される)- カスタムシリアライズがオプション(呼び出し元指定の挙動変更)をサポートしない
- 構文処理と意味処理が分離されておらず、低レベルなJSON処理が困難
// v1 の問題例: nilスライスがJSONのnullになる
type Response struct {
Items []string `json:"items"`
}
r := Response{Items: nil}
b, _ := json.Marshal(r) // {"items":null} ← 意図しないnull
// v1 の問題例: ケースインセンシティブマッチ
type User struct {
Name string `json:"name"`
}
var u User
json.Unmarshal([]byte(`{"NAME":"Alice"}`), &u) // u.Name = "Alice" ← v2では拒否
提案された解決策
2つのパッケージを新設し、既存のv1を v2 の上に再実装します。
encoding/json/jsontext — 構文レイヤー専用の低レベルパッケージ。JSONの文法処理のみを担い、Goのreflectに依存しません。Encoder/Decoder(ストリーミング処理)、Token(単一JSONトークン、アロケーションフリー)、Value(生のJSON値、v1のRawMessage相当)が主要型です。
encoding/json/v2 — v1の第2世代として設計されたセマンティックパッケージ。jsontextを基盤に実装されます。
encoding/json(v1) — v2 の上に再実装され、後方互換性を保ちつつ v2 の新機能も利用可能になります。
これによって何ができるようになるか
1. 呼び出し元によるカスタムシリアライズ
型に依存せず、呼び出し元が特定の型の JSON 表現を上書きできます。
// After: 呼び出し元がerrorをJSON化する方法を指定できる
m := json.WithMarshalers(json.MarshalFunc(func(e error) ([]byte, error) {
return []byte(`"` + e.Error() + `"`), nil
}))
b, _ := jsonv2.Marshal(someValue, m)
2. 段階的なv1→v2移行
v1とv2のオプションを組み合わせ、挙動を細かく制御できます。
// Before: v1の挙動のまま
jsonv1.Marshal(v)
// After: v2ベース + 特定のv1互換オプションのみ有効化
jsonv2.Marshal(v, jsonv1.DefaultOptionsV1())
// 一部だけv2に移行する例(重複キーのみ禁止)
jsonv2.Marshal(v, jsonv1.DefaultOptionsV1(), jsontext.AllowDuplicateNames(false))
3. 新しい構造体タグオプション
type Event struct {
// omitzero: IsZero()メソッドかGoのゼロ値で省略
Timestamp time.Time `json:",omitzero"`
// nocase: アンマーシャル時に大文字小文字・ダッシュ・アンダースコアを無視してマッチ
Name string `json:",nocase"`
// format: 型ごとのフォーマット指定
Duration time.Duration `json:",format:RFC3339"`
// unknown: 未知のJSONフィールドを保持
Extra jsontext.Value `json:",unknown"`
// inline: Goの埋め込みに相当するJSONオブジェクトの展開
Metadata map[string]any `json:",inline"`
}
4. ストリーミングカスタムシリアライズ
// After: MarshalerTo インターフェース(ストリーミング・オプション対応)
type MyType struct{ /* ... */ }
func (m MyType) MarshalJSONTo(enc *jsontext.Encoder, opts json.Options) error {
return enc.WriteToken(jsontext.String(m.String()))
}
5. 低レベルなJSON処理
// After: jsontext を使ってJSONを構文レベルで処理(reflection不要)
enc := jsontext.NewEncoder(w)
enc.WriteToken(jsontext.ObjectStart)
enc.WriteToken(jsontext.String("key"))
enc.WriteToken(jsontext.String("value"))
enc.WriteToken(jsontext.ObjectEnd)
議論のハイライト
- インターフェース命名の変更: 議論中は
MarshalerV2/UnmarshalerV2という名前だったが、最終的にMarshalerTo/UnmarshalerFromに変更。io.WriterTo/io.ReaderFromの慣習に合わせ、ストリーミングサポートを示す命名となった。 - v1互換オプションの多さへの懸念:
WithLegacySemantics系オプションが多数生まれたことへの批判があった。v1の「バグとも言える挙動」がHyrum's Lawによって事実上安定したAPIとなっており、個別オプションを設けざるを得なかったと@dsnetが説明。 Kind型の定数を設けない設計:'n'(null)のように1バイトのリテラルが可読であること、http.MethodGetが導入されながら"GET"リテラルが75%程度使われ続けている問題を踏まえ、定数は設けないと判断。- パフォーマンス改善: v2はv1より多くの場面でメモリアロケーションが少なく、文字列インターン(intern)キャッシュの活用などで優位性がある。ただしJSONオブジェクトの重複キーチェックは常時実施するため、その分のオーバーヘッドが存在する。
- 機能の範囲絞り込み:
[]byte/[N]byteのformat:stringサポートなど、いくつかの機能はスコープ外とし、初期リリース後の別Proposalとして分離する方針が取られた。新機能の追加よりも既存提案を確実にstdlibに組み込むことを優先している(@neild)。 time.Durationのデフォルト表現変更: v1では10億単位のナノ秒数(JSON数値)だったが、v2では"1h2m3.456s"のような文字列表現がデフォルトとなる。jsonv1.FormatTimeWithLegacySemanticsオプションで旧挙動に戻すことが可能。