11月4日、Michael PrattほかGoチームの主要メンバーら7名が「A new Go API for JSON」と題した記事を公開した。Go 1.27で正式リリースされたencoding/json/v2とencoding/json/jsontextの設計思想と主要APIを詳しく解説した内容だ。
Goのencoding/jsonはGo 1.0以前から存在し、15年以上にわたって世界中のGoプロジェクトで使われてきた。しかし後方互換性の制約(Go 1互換性保証)により、既知の欠陥があっても修正できないという状況が長年続いてきた。今回のv2はその呪縛を断ち切る試みだ。重複キーによるセキュリティ上の懸念、カスタムシリアライザで発生する二重パース、nilスライスがnullにマーシャルされる相互運用性の問題——これらはすべて、今日から使える形で解消される。
なぜencoding/jsonを直接修正できなかったのか
v1の問題は単なるバグではなく、APIの設計そのものに起因するものが多い。Go 1の互換性保証により、既存の動作を変えると無数の既存コードが壊れる。MarshalV2のような別名を同一パッケージに追加する案もあったが、同一パッケージ内に並行した名前空間を作ることになり現実的でない。結論として、encoding/json/v2という独立したパッケージを新設することが選ばれた。
v1の何が問題だったのか
記事では問題を「動作上の欠陥」「APIの制約」「パフォーマンスの限界」の3カテゴリに分けて解説している。
動作上の欠陥
- 不正なUTF-8を無警告で受け入れる:RFC 8259は有効なUTF-8を必須とするが、
v1はサイレントにデータを破損させる可能性がある。 - 重複キーを許容する:JSONオブジェクト内に同名のキーが複数ある場合、実装によって解釈が異なる。これはセキュリティ上の懸念につながる問題であり、JSONパーサーの重複キー処理の差異を突いた攻撃手法の一例としてCVE-2017-12635(CouchDBにおける管理者権限昇格)が知られている。
v2ではデフォルトで重複キーをエラーとして扱う。 - nilスライス・マップが
nullにマーシャルされる:他言語のJSON実装ではnullを配列やオブジェクトとして扱えないものも多く、相互運用性の問題になる。ユーザー調査では、大多数のGoユーザーがデフォルトで空配列・空オブジェクトへのマーシャルを望んでいた。 - 大文字小文字を区別しないアンマーシャル:デフォルトで大文字小文字を無視して構造体フィールドにマッチするが、セキュリティ上の懸念とパフォーマンス低下の原因になる。
APIの制約
io.Readerからのアンマーシャルを正しく行うのが難しい。よく見かけるjson.NewDecoder(r).Decode(v)というコードは末尾の余分なデータを検出しない。また、Encoder/Decoderに設定したオプション(例:DisallowUnknownFields)が、カスタムのUnmarshalJSONメソッド内では効かなくなるという問題もある。
パフォーマンスの限界
MarshalJSONインターフェースは[]byteを返すため必ずアロケーションが発生し、さらにencoding/jsonがその結果を検証・フォーマットし直す必要がある。UnmarshalJSONも同様で、JSON値の終端を見つけるために一度パースし、次いでメソッド内でもう一度パースするという二重パースが起きる。さらにMarshalJSON/UnmarshalJSONが内部でMarshal/Unmarshalを再帰的に呼ぶと、計算量が二次関数的に増大する。ネストが深い構造体(たとえば10階層を超えるような場合)では、この問題が実測パフォーマンスに顕著に現れる。
新パッケージの構造:jsontext と v2
新しい設計の核心は、JSONの処理を構文(syntax)と意味(semantics)に明確に分離したことだ。
encoding/json/jsontext:JSON文法レベルの処理(エンコード・デコード)を担当。Goのリフレクションに依存しない。encoding/json/v2:Go値とJSON値の対応関係(マーシャル・アンマーシャル)を担当。

jsontextの基本APIは以下の通りだ。
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)
従来のRawMessageはjsontext.Valueの型エイリアスになった。新しいMarshalerTo/UnmarshalerFromインターフェースはEncoder/Decoderを直接受け取る設計で、ストリーミング処理が可能になり、前述の二重パース問題を根本から解消する。
v2のAPIと注目点
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
v1との最大の違いの一つは、オプションがすべての関数の第一級引数として渡せる点だ。v1ではEncoder/Decoderにのみオプションを設定でき、Marshal/Unmarshalには渡せなかった。
もう一つの目玉が、呼び出し元によるカスタムシリアライズ関数の注入(caller-specified customization)だ。型定義側のメソッドより呼び出し元の指定が優先されるため、ライブラリ内部の型に対しても外部からシリアライズ挙動を制御できる。たとえば、依存しているサードパーティ型のJSON出力を変えたい場合に、型自体を変更せず対応できる。
func WithMarshalers(*Marshalers) Options
func MarshalFunc[T any](fn func(T) ([]byte, error)) *Marshalers
func MarshalToFunc[T any](fn func(*jsontext.Encoder, T) error) *Marshalers
移行について
「v1の使い方をそのままv2に移行すれば、ほぼ同じように動作する」ことを目標として設計されている。ただしデフォルト動作の一部(nilマップの扱い、重複キーのエラー化など)は変わるため、既存コードのテストは必要だ。v1のRawMessageはjsontext.Valueの型エイリアスになっているため、この部分の移行コストは低い。段階的な移行を想定した設計になっており、既存のv1コードと共存させながら徐々に切り替えていくことも可能だ。Go 1.27は2026年8月にリリースされており、現在の安定版でencoding/json/v2を利用できる。
詳細はA new Go API for JSONを参照していただきたい。