メインコンテンツへスキップ

Go Proposal Weekly Digest

Go言語のproposal更新を毎週お届け

新機能挙動変更

encoding/json v1の設計上の問題を刷新する新パッケージ encoding/json/v2 と encoding/json/jsontext が追加され、既存の encoding/json もv2実装に置き換わった

New encoding/json/v2 and encoding/json/jsontext packages

この項目の注釈は AI により生成されており、誤りを含む場合があります。
使用例のコンパイル検証: 検証済み

概要

encoding/json/v2
encoding/json/jsontext という2つの新しい標準ライブラリパッケージが追加された。
json/v2 は既存の encoding/json(v1)の大改訂であり、MarshalMarshalWriteMarshalEncodeUnmarshalUnmarshalReadUnmarshalDecode を提供し、いずれも可変長の Options 引数でマーシャル/アンマーシャルの挙動を設定できる。
jsontext はJSONの構文処理だけを担う低レベルパッケージで、EncoderDecoderTokenValue 単位で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の型システム(false0・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.goencode.goscanner.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.AllowDuplicateNamesjsontext.AllowInvalidUTF8jsonv2.FormatNilSliceAsNull など)を束ねて返す(v2_options.go;l=229)。
また v2_inject.goinit で、v2側のエラー型をv1の MarshalerError 等に変換するフック(internal.TransformMarshalError など)や json.Number 用のフックをv2パッケージへ注入しており、v1のエラー表現・Number 型としての互換性を保っている(v2_inject.go;l=21)。

関連リンク