encoding/json v1の設計上の問題を刷新する新パッケージ encoding/json/v2 と encoding/json/jsontext が追加され、既存の encoding/json もv2実装に置き換わった
New encoding/json/v2 and encoding/json/jsontext packages
概要
encoding/json/v2 と
encoding/json/jsontext という2つの新しい標準ライブラリパッケージが追加された。
json/v2 は既存の encoding/json(v1)の大改訂であり、Marshal・MarshalWrite・MarshalEncode・Unmarshal・UnmarshalRead・UnmarshalDecode を提供し、いずれも可変長の Options 引数でマーシャル/アンマーシャルの挙動を設定できる。
jsontext はJSONの構文処理だけを担う低レベルパッケージで、Encoder・Decoder が Token・Value 単位でJSONをストリーミング処理する。
v2はv1より厳格でRFC準拠寄りのデフォルト挙動を採る(不正なUTF-8を含む文字列を拒否する、JSONオブジェクト内の重複キーを拒否する、など)。差分の全体は encoding/json パッケージドキュメントの移行ガイドにまとまっている。
既存の encoding/json(v1)自体もv2実装の上に再実装された。マーシャル/アンマーシャルの挙動は保たれるが、エラーメッセージの文言が変わることがある。v1にもv2セマンティクスへ段階移行するための新しい Options が追加されており、v1 APIは今後も引き続きサポートされ、移行は必須ではない。Marshal性能は従来実装とほぼ同等、Unmarshal性能は大幅に高速化されている。
新実装との互換性問題に遭遇した場合は、ビルド時に GOEXPERIMENT=nojsonv2 を指定することで従来のv1実装に戻せる。このオプトアウトは将来のリリースで削除される予定。
導入経緯
本proposal(golang/go#71497)は、golang/go#63397 での長年の議論を経て正式提案となったもので、json/v2 working group による設計検討の成果である。
議論タイムライン
- 2026年4月15日(2026年第16週の週次ダイジェスト): Proposal Review Meeting(@aclements、@adonovan、@cherrymui、@griesemer、@ianlancetaylor、@neild、@rolandshoemaker 出席)で active カラムに追加された。
- 2026年第19週(週次ダイジェスト): 提案者の @dsnet がAPI全体をウォークスルーし、参加者全員の同意を得て @aclements が likely_accept と判定した。
- 2026年第20週(週次ダイジェスト): 追加のコンセンサス変化がなかったため、正式に accepted となった。
議論のハイライト
Options型の設計: 構文層(jsontext)・意味層(json/v2)・マーシャル・アンマーシャルで単一のOptionsインターフェース型を共用する設計は、working groupが多くの代替案(オプション構造体など)を検討した末に採用された。可変長引数として渡し、後から渡したオプションが優先されるマージ挙動が最も扱いやすいと判断された。MarshalerTo/UnmarshalerFromという命名: 議論初期はMarshalerV2/UnmarshalerV2という名前だったが、io.WriterTo/io.ReaderFromの慣習に合わせてストリーミング対応を示す現在の名前に変更された。- v1互換オプションの多さ: v1の挙動の多くが「実質的なバグ」でありながらHyrumの法則により事実上安定したAPIとして定着していたため、
jsonv1.WithLegacySemantics系のv1互換オプションが多数必要になった。提案者自身も「悲しい」と述べているが避けられない現実だとされた。 omitemptyの再定義: v1ではGoの型システム(false・0・nilポインタなど)で「空」を定義していたが、v2ではJSONの型システム(JSON null・空文字列・空オブジェクト・空配列)で再定義された。boolや数値型での従来のomitempty相当の挙動は新設のomitzeroへの移行が推奨される。time.Durationのデフォルト表現の変更: v1ではナノ秒の数値として出力していたが、v2ではデフォルト表現を持たずランタイムエラーとなる。従来の挙動を維持したい場合はjsonv1.FormatDurationAsNanoを指定する。- スコープの絞り込み: ユーザー定義オプションや
[]byte/[N]byteへのformat:stringサポートなど一部の機能は、初回リリースを確実に stdlib へ組み込むことを優先し、意図的にスコープ外として後続の個別proposalに委ねられた。
使用例
Before
package main
import (
"encoding/json"
"fmt"
)
type Response struct {
Items []string `json:"items"`
}
func main() {
r := Response{Items: nil}
data, err := json.Marshal(r)
if err != nil {
panic(err)
}
fmt.Println(string(data)) // {"items":null}
}
After
package main
import (
"fmt"
jsonv2 "encoding/json/v2"
)
type Response struct {
Items []string `json:"items"`
}
func main() {
r := Response{Items: nil}
data, err := jsonv2.Marshal(r)
if err != nil {
panic(err)
}
fmt.Println(string(data)) // {"items":[]}
}
移行時の注意
- v1
encoding/jsonは引き続きサポートされ、既存コードの変更は不要。移行するかどうかは任意である。 - v2のデフォルト挙動はv1と異なる点が複数ある(nilスライス/nilマップが
nullではなく[]/{}になる、不正なUTF-8やオブジェクトの重複キーがエラーになる、フィールド名マッチングが大文字小文字を区別するようになる、など)。詳細はencoding/jsonの移行ガイド を参照。 - v1の挙動を維持したままv2のAPI(ストリーミングなど)だけを使いたい場合は、
jsonv1.DefaultOptionsV1()をjsonv2.Marshal/jsonv2.Unmarshalに渡すことで再現できる。 - 新実装との互換性問題が出た場合は、ビルド時に
GOEXPERIMENT=nojsonv2を指定すると従来のv1実装に戻せる。ただしこのオプトアウトは将来のリリースで削除される予定であるため、恒久的な回避策にはできない。 - 問題を発見した場合はIssueを立てることが推奨されている。
実装解説
GOEXPERIMENT=jsonv2(デフォルト有効)でビルドすると、encoding/json パッケージ内の従来のv1純正実装ファイル(decode.go・encode.go・scanner.go など)は //go:build !goexperiment.jsonv2 ビルドタグにより除外され(decode.go;l=8)、代わりに v2_*.go 群がビルドされる。v1の公開APIである json.Marshal は、実体としては jsonv2.Marshal(v, DefaultOptionsV1()) の薄いラッパーになっている(v2_encode.go;l=184)。DefaultOptionsV1 はv1互換のためのオプション集合(jsontext.AllowDuplicateNames・jsontext.AllowInvalidUTF8・jsonv2.FormatNilSliceAsNull など)を束ねて返す(v2_options.go;l=229)。
また v2_inject.go の init で、v2側のエラー型をv1の MarshalerError 等に変換するフック(internal.TransformMarshalError など)や json.Number 用のフックをv2パッケージへ注入しており、v1のエラー表現・Number 型としての互換性を保っている(v2_inject.go;l=21)。
関連リンク
- go.dev リリースノート (go1.27) go.dev
- Proposal Issue #71497 github.com/golang/go
- pkg.go.dev/encoding/json/v2 pkg.go.dev
- pkg.go.dev/encoding/json pkg.go.dev
- pkg.go.dev/encoding/json/jsontext pkg.go.dev
- 2026 W16 ダイジェスト(active) digest
- 2026 W19 ダイジェスト(active → likely_accept) digest
- 2026 W20 ダイジェスト(likely_accept → accepted) digest