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実装に戻せる。このオプトアウトは将来のリリースで削除される予定。
GOEXPERIMENT期間中に、encoding/json/v2 のAPIにはいくつかの見直しが加えられた。
format タグオプション(golang/go#79071)、
unknown タグオプションと DiscardUnknownMembers(golang/go#77271)、
SkipFunc センチネルエラー(golang/go#74324)が削除された。
また inline タグオプションは embed に改名され(golang/go#79985)、string タグオプションの挙動(golang/go#79065)と
MatchCaseInsensitiveNames オプション(CL 792780)まわりの挙動も更新された。
encoding/json/jsontext では、数値系の Token アクセサがエラーも返すように変更された(golang/go#77666)。
導入経緯
本proposal(golang/go#71497)は、golang/go#63397 での長年の議論を経て正式なproposalとなったものである。
議論タイムライン
- 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が多くの代替案(オプション構造体など)を検討した末に採用された。可変長引数として渡し、後から渡したオプションが優先されるマージ挙動が最も扱いやすいと判断された。- v1互換オプションの多さ: v1の挙動の多くが「実質的なバグ」でありながらHyrumの法則により事実上安定したAPIとして定着していたため、
jsonv1.WithLegacySemantics系のv1互換オプションが多数必要になった。 omitemptyの再定義: v1ではGoの型システム(false・0・nilポインタなど)で「空」を定義していたが、v2ではJSONの型システム(JSON null・空文字列・空オブジェクト・空配列)で再定義された。boolや数値型での従来のomitempty相当の挙動は新設のomitzeroへの移行が推奨される。formatタグオプションの削除(golang/go#79071): Go 1.28で型付き構造体タグが導入される見込みとなったため、より自然にそちらで表現できるformatタグはworking groupの判断で削除された。実装自体は消えておらず、github.com/go-json-experiment/json側の実験的なフックからは引き続き利用可能だが、標準のencoding/json/v2パッケージとしては非サポートという位置付けになった。unknownタグオプションとDiscardUnknownMembersの削除(golang/go#77271): 類似機能を持つinlineタグとの違いが微妙すぎるとして、対抗提案(golang/go#76444)ではなくこちらの削除案がworking groupで採用された。アンマーシャル時に未知フィールドを拒否するRejectUnknownMembersは、v1のDecoder.DisallowUnknownFields相当の互換性維持のため残された。inlineタグのembedへの改名(golang/go#79985): Kubernetesエコシステムを中心に、v1で意味を持たない,inlineタグが既に約65万件規模で使われていることが判明し、Go 1.27でv1にもinlineタグの意味が付くと動作が変わってしまうリスクが指摘された。Goモジュールプロキシ全体の調査でembedという名前の既存使用は0件だったため、リスク回避のためembedに改名された。stringタグオプションの再帰動作廃止(golang/go#79065): プロトタイプでは,stringタグがスライスなどの複合型に再帰的に適用されていたが、他のタグオプションはいずれも非再帰的であることや将来の型付き構造体タグとの整合性を理由に、複合型への,stringはエラーとする方針に変更された。- 数値系
Tokenアクセサへのエラー追加(golang/go#77666): 従来案のToken.Float()は1e500のような範囲外の数値を黙ってmath.MaxFloat64に丸めたり"Infinity"を+Infとして受け入れたりしており、カスタムアンマーシャラーの実装者が気付かずに誤った値を使ってしまう懸念があった。最終的にstrconv.ParseFloatと同様の意味論に合わせ、範囲外の値はエラーとして報告するよう変更された。
使用例
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=jsonv2を先行して試していた場合は、unknownタグオプション・DiscardUnknownMembers・SkipFuncセンチネルエラー・複合型への再帰的な,stringタグ・旧名のinlineタグ(embedに改名)に依存したコードが正式版では動作しなくなる点に注意が必要。特にinlineタグは黙って無視されるだけでエラーにはならないため、意図した通りに埋め込まれているか確認すること。- 新実装との互換性問題が出た場合は、ビルド時に
GOEXPERIMENT=nojsonv2を指定すると従来のv1実装に戻せる。ただしこのオプトアウトは将来のリリースで削除される予定であるため、恒久的な回避策にはできない。 - 問題を発見した場合はIssueを立てることが推奨されている。
実装解説
encoding/json/jsontext の数値アクセサは、v1の慣習だった単一戻り値の float64/int64/uint64 から、エラーも返す (float64, error)/(int64, error)/(uint64, error) に変更されている(token.go;l=386 の Token.Float、token.go;l=452 の Token.Int、token.go;l=535 の Token.Uint)。
format タグオプションは encoding/json/v2 の構造体タグパーサ自体では引き続き構文解析されるが(fields.go;l=529)、time.Time など個別にフォーマット処理を持つ型以外でこのタグを指定すると、フィールド解析時点で Go struct field %s has unsupported \format` tag optionというSemanticError` が生成される(fields.go;l=258)。タグ自体をエラーとして拒否するのではなく、型ごとの対応状況に応じて後段でエラーにする設計になっている。
MatchCaseInsensitiveNames はグローバルなオプションだが、フィールドごとに case:strict/case:ignore タグを指定することでこのオプションの適用を上書きできる(options.go;l=204、fields.go;l=370)。
関連リンク
- go.dev リリースノート (go1.27) go.dev
- Proposal Issue #71497 github.com/golang/go
- Proposal Issue #79071 github.com/golang/go
- Proposal Issue #77271 github.com/golang/go
- Proposal Issue #74324 github.com/golang/go
- Proposal Issue #79985 github.com/golang/go
- Proposal Issue #79065 github.com/golang/go
- Proposal Issue #77666 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