encoding/json/v2で何が変わる? - v1からv2への変化を徹底比較
https://gocon.jp/2025/talks/958036/
https://speakerdeck.com/nagano/v2dehe-gabian-waruka
従来の課題
複数キーがある場合、後にセットされた値が優先される
脆弱性 になりうる: 署名検証・監査・差分適用のシナリオで問題が発生する
e.g. CVE-2017-12635
無効な UTF-8 を用いてもエラーにならない: データ破損のリスク
大文字と小文字を区別しない: RFC 8259 では区別する
map / slice の ゼロ値: nil(null) ではなく [] / {} にしたい
一貫性の欠如: レシーバ が値か ポインタ かで結果が異なる可能性がある
∵ 値レシーバ は ポインタレシーバ で定義されたメソッドを呼び出せない
API が扱いづらい
io.Reader からの正確なアンマーシャルが困難である
json.NewDecoder(r).Decode(v) では入力末尾に含まれる余分なデータを拒否できないため、追加のコードが必要
code:go
r := strings.NewReader({"a":1} true)
dec := json.NewDecoder(r)
var v mapstringint
_ = dec.Decode(&v) // 1 個目のみ受理され、余剰データがあっても警告なし
// チェックするには追加のコードが必要
if err := dec.Decode(&struct{}{}); err != io.EOF {
...
}
オプション設定の柔軟性不足
Encode と Decode のオプションが Marshal / Unmarshal で利用不可
インスタンス単位で設定する必要がある(e.g. DisallowUnknownField())
ユーティリティ関数の性能不足
Compact など、input が []byte、output が *bytes.Buffer なので、すべてメモリに載せること前提
func json.Compact(dst *bytes.Buffer, src []byte) error
そのため、巨大データはそのまま扱えない
また、ファイルの書き込みなどのユースケースに対応する際に io.Writer が使えないのでひと手間必要
実装に起因するパフォーマンス制約
ストリーミング 非対応: 全 JSON を内部バッファリング
強制 メモリアロケーション: MarshalJSON の戻り値が []byte を返す
二度読み・先読み強制: UnmarshalJSON 内部では境界検知と実際のパースで 2 回パース処理が走っている
再帰的な二重処理により、計算量 が指数関数的に増加するため、特にネストした構造体ではパフォーマンス劣化が顕著に
実際、Kubernetes で問題化: https://github.com/kubernetes/kube-openapi/issues/315
上記のような課題を解決するために encoding/json/v2 ならびに encoding/json/jsontext が新しいパッケージとして追加された
v2 のコンセプト
構文機能(jsontext)と意味機能(json/v2)とに分離
jsontext: JSON の文法に基づく処理に特化
トークン ベースのローレベルな操作
JSON トークンを直接扱う Encoder / Decoder
RFC 8259 準拠のバリデーションチェック
位置情報を保持した詳細なエラー報告
json/v2: JSON と Go の値の相互変換を定義
Go ↔︎ JSON の相互変換
拡張可能なオプション機構
Marshaler / Unmarshaller のカスタマイズ可能
reflect ベースの型マッピング
上記のような責務を分離したアーキテクチャがもたらすメリット
柔軟性: 独立利用や低レベルカスタマイズが容易になった
io.Reader / io.Writer に直接書き込めるようになった
オプションも追加
jsontext.WithIndent: 出力のフォーマットにインデントを付与する
jsontext.OmitZeroStructFields: ゼロ値のフィールドのエンコードをスキップする
正確性: RFC 8259 に完全準拠することにより、不正な JSON や重複キー、無効な UTF-8 を検出できるように
また、位置情報やコンテキストを含む詳細なエラーメッセージが表示されるように
効率性; ストリーミング方式によるデータ処理で、大規模データの低メモリ・高速処理が可能
メモリに一度に読み込まず、一部ずつ読み書き・変換可能に
互換性: encoding/json/v2で何が変わる? - v1からv2への変化を徹底比較#68da15980000000000ff48be
v1 との関係性
idea.icon v1 が廃止されることはなく、引き続きサポートされる
v1 は内部的に v2 を基に再実装されており、同じコードベースを共有している
v2 に追加される 後方互換 機能は、v1 でも自動的に利用可能
v2 のバグ修正やパフォーマンス改善が両方のバージョンに適用される
v1 との変更点
厳格なバリデーション
エラー種別の明確化
ストリーミング対応
omitempty の挙動変更
空スライスと空マップは v1 では省略されていたが、v2 では []、{} として出力される
map / slice の nil の非 null 化
空スライスと空マップは v1 では null になっていたが、v2 では []、{} として出力される
新規で追加された機能
オプション設定
すべての API で opts ...json.Options パラメータを利用可能に
新しいタグ機能
omitzero: ゼロ値の場合に省略
omitempty: JSON の空値("", 'null, [], {})の場合に省略
format: time.Time などの日時フォーマットを指定
これめちゃくちゃ便利だーーーーー radish-miyazaki.icon*4
inline: 構造化フィールドをインライン展開(平坦化)
unknown: 未知のフィールドの扱いを制御
エラー処理の向上
カスタマイズ可能な Marshaler / Unmarshaller
v2 への移行手順
1. v1 互換モードでの検証: GOEXPERIMENT=jsonv2 を付けてビルド・テスト実行
2. encoding/json から encoding/json/v2 への置換: インポートパスを置き換える
3. v2 による破壊的変更への対応: ビルドして破壊的変更点を探す
e.g. ゼロ値の扱いや omitempty の挙動変更、io.Reader / io.Writer の利用箇所
公式ベンチマーク
https://github.com/go-json-experiment/jsonbench
参考
https://pkg.go.dev/encoding/json/v2
https://go.dev/doc/go1.25#json_v2
#Go_Conference_2025 #Go