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

Go Proposal Weekly Digest

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

#71497active

encoding/json/v2: new API for encoding/json

新規提案

要約

AIによる要約であり、誤りを含む場合があります。

概要

encoding/json/v2 および encoding/json/jsontext という2つの新パッケージを Go 標準ライブラリに追加するproposalです。これは Go の JSON 処理を根本から再設計する、標準ライブラリ史上最大規模のパッケージ改訂であり、長年の議論(#63397)を経て正式提案に昇格しました。

ステータス変更

(新規)active
2026年4月15日のProposal Review Meeting(@aclements、@adonovan、@cherrymui、@griesemer、@ianlancetaylor、@neild、@rolandshoemaker 出席)において、activeカラムに追加されました。ミーティング議事録では "added to minutes" と記録されており、週次レビュー会議での継続的なレビュー対象となります。提案はすでに github.com/go-json-experiment/json として動作する実装が存在しており、GopherConでの発表("The Future of JSON in Go")も行われていることから、正式なProposalとしての審査段階に入ったと判断されたと考えられます。

技術的背景

現状の問題点

v1 encoding/json には多くの設計上の問題が蓄積されており、後方互換性の制約から修正が困難な状態です。主な問題点は以下のとおりです。

  • フィールド名の大文字小文字を区別しないマッチング(セキュリティ・パフォーマンス上の問題)
  • nilスライス・nilマップがJSONのnullとしてエンコードされる(直感に反する挙動)
  • 重複したJSONオブジェクトのキーが許容される(RFC違反)
  • 無効なUTF-8がエラーなく変換される
  • omitempty の定義がGoの型システムに基づくため、直感的でない(例: false0 が省略される)
  • カスタムシリアライズがオプション(呼び出し元指定の挙動変更)をサポートしない
  • 構文処理と意味処理が分離されておらず、低レベルなJSON処理が困難
// v1 の問題例: nilスライスがJSONのnullになる
type Response struct {
    Items []string `json:"items"`
}
r := Response{Items: nil}
b, _ := json.Marshal(r) // {"items":null} ← 意図しないnull
// v1 の問題例: ケースインセンシティブマッチ
type User struct {
    Name string `json:"name"`
}
var u User
json.Unmarshal([]byte(`{"NAME":"Alice"}`), &u) // u.Name = "Alice" ← v2では拒否

提案された解決策

2つのパッケージを新設し、既存のv1を v2 の上に再実装します。
encoding/json/jsontext — 構文レイヤー専用の低レベルパッケージ。JSONの文法処理のみを担い、Goのreflectに依存しません。Encoder/Decoder(ストリーミング処理)、Token(単一JSONトークン、アロケーションフリー)、Value(生のJSON値、v1のRawMessage相当)が主要型です。
encoding/json/v2 — v1の第2世代として設計されたセマンティックパッケージ。jsontextを基盤に実装されます。
encoding/json(v1) — v2 の上に再実装され、後方互換性を保ちつつ v2 の新機能も利用可能になります。

これによって何ができるようになるか

1. 呼び出し元によるカスタムシリアライズ

型に依存せず、呼び出し元が特定の型の JSON 表現を上書きできます。

// After: 呼び出し元がerrorをJSON化する方法を指定できる
m := json.WithMarshalers(json.MarshalFunc(func(e error) ([]byte, error) {
    return []byte(`"` + e.Error() + `"`), nil
}))
b, _ := jsonv2.Marshal(someValue, m)

2. 段階的なv1→v2移行

v1とv2のオプションを組み合わせ、挙動を細かく制御できます。

// Before: v1の挙動のまま
jsonv1.Marshal(v)
// After: v2ベース + 特定のv1互換オプションのみ有効化
jsonv2.Marshal(v, jsonv1.DefaultOptionsV1())
// 一部だけv2に移行する例(重複キーのみ禁止)
jsonv2.Marshal(v, jsonv1.DefaultOptionsV1(), jsontext.AllowDuplicateNames(false))

3. 新しい構造体タグオプション

type Event struct {
    // omitzero: IsZero()メソッドかGoのゼロ値で省略
    Timestamp time.Time `json:",omitzero"`
    // nocase: アンマーシャル時に大文字小文字・ダッシュ・アンダースコアを無視してマッチ
    Name string `json:",nocase"`
    // format: 型ごとのフォーマット指定
    Duration time.Duration `json:",format:RFC3339"`
    // unknown: 未知のJSONフィールドを保持
    Extra jsontext.Value `json:",unknown"`
    // inline: Goの埋め込みに相当するJSONオブジェクトの展開
    Metadata map[string]any `json:",inline"`
}

4. ストリーミングカスタムシリアライズ

// After: MarshalerTo インターフェース(ストリーミング・オプション対応)
type MyType struct{ /* ... */ }
func (m MyType) MarshalJSONTo(enc *jsontext.Encoder, opts json.Options) error {
    return enc.WriteToken(jsontext.String(m.String()))
}

5. 低レベルなJSON処理

// After: jsontext を使ってJSONを構文レベルで処理(reflection不要)
enc := jsontext.NewEncoder(w)
enc.WriteToken(jsontext.ObjectStart)
enc.WriteToken(jsontext.String("key"))
enc.WriteToken(jsontext.String("value"))
enc.WriteToken(jsontext.ObjectEnd)

議論のハイライト

  • インターフェース命名の変更: 議論中は MarshalerV2/UnmarshalerV2 という名前だったが、最終的に MarshalerTo/UnmarshalerFrom に変更。io.WriterTo/io.ReaderFrom の慣習に合わせ、ストリーミングサポートを示す命名となった。
  • v1互換オプションの多さへの懸念: WithLegacySemantics 系オプションが多数生まれたことへの批判があった。v1の「バグとも言える挙動」がHyrum's Lawによって事実上安定したAPIとなっており、個別オプションを設けざるを得なかったと@dsnetが説明。
  • Kind 型の定数を設けない設計: 'n'(null)のように1バイトのリテラルが可読であること、http.MethodGet が導入されながら "GET" リテラルが75%程度使われ続けている問題を踏まえ、定数は設けないと判断。
  • パフォーマンス改善: v2はv1より多くの場面でメモリアロケーションが少なく、文字列インターン(intern)キャッシュの活用などで優位性がある。ただしJSONオブジェクトの重複キーチェックは常時実施するため、その分のオーバーヘッドが存在する。
  • 機能の範囲絞り込み: []byte/[N]byteformat:string サポートなど、いくつかの機能はスコープ外とし、初期リリース後の別Proposalとして分離する方針が取られた。新機能の追加よりも既存提案を確実にstdlibに組み込むことを優先している(@neild)。
  • time.Duration のデフォルト表現変更: v1では10億単位のナノ秒数(JSON数値)だったが、v2では "1h2m3.456s" のような文字列表現がデフォルトとなる。jsonv1.FormatTimeWithLegacySemantics オプションで旧挙動に戻すことが可能。

関連リンク