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

Go Proposal Weekly Digest

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

#71497likely_accept

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

ステータス変更: active likely_accept

要約

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

概要

encoding/json/v2 および encoding/json/jsontext という2つの新しいパッケージをGo標準ライブラリに追加するproposalです。既存の encoding/json(v1)が抱える長年の設計上の問題を解消し、より正確・高速・柔軟なJSON処理を実現することを目的としています。Go標準ライブラリ史上最大規模のパッケージ改訂です。

ステータス変更

activelikely_accept
proposalレビューミーティングで提案者の@dsnetがAPI全体をウォークスルー形式で説明し、参加者全員から👍が得られたことにより、@aclementsがlikely acceptと判定しました。主な確認事項は、jsontextパッケージが安全性よりパフォーマンスを重視した低レベルAPIであること、Options型の設計がワーキンググループで徹底的に検討された末の結論であること、UnmarshalReadがEOFまで必ず読み進める点でv1の一般的なミスを防ぐ設計であることなどです。なお、本proposalはGo 1.27での安定版リリースを目標としており、Go 1.25でGOEXPERIMENT=jsonv2として先行公開済みです。

技術的背景

現状の問題点

v1 encoding/json には以下の設計上の問題が蓄積されていました。

  • 無効なUTF-8を黙って置換文字に変換する(セキュリティリスク)
  • JSONオブジェクト内の重複キーを許容する
  • nil のスライス/マップをJSONの null としてマーシャルする(空配列/空オブジェクトが自然)
  • フィールドマッチングが大文字小文字を区別しない(パフォーマンスと正確性の問題)
  • MarshalJSON / UnmarshalJSON がバイト列を返すためアロケーションが必須でストリーミング不可
  • Marshal / Unmarshal にオプションを渡す手段がない
  • io.Readerからデコード後にEOFチェックを忘れるバグが頻発

提案された解決策

JSON処理を「構文(syntactic)」と「意味(semantic)」の2層に明確に分離する2パッケージ構成を採用します。
encoding/json/jsontext — 構文層(Goのreflect非依存)

  • Encoder / Decoder によるトークン・値単位のストリーミング処理
  • Token(個別のJSONトークン)と Value(完全なJSON値のバイト表現)の2つの基本型
  • Pointer(RFC 6901 JSON Pointer)によるエラー位置の正確な報告
    encoding/json/v2 — 意味層(reflectを使用)
  • Marshal / Unmarshal に加え、MarshalWrite / UnmarshalRead(io.Writer/io.Reader対応)、MarshalEncode / UnmarshalDecode(Encoder/Decoder対応)を追加
  • 新インターフェース MarshalerTo / UnmarshalerFrom によるアロケーションフリーのストリーミング実装
  • ジェネリクスを使った呼び出し側カスタマイズ(MarshalToFunc[T] / UnmarshalFromFunc[T]
  • Options 型(可変長引数)で全関数に統一的なオプション注入
    v1のv2実装による透過的な継続 — v1 encoding/json はv2実装の上に構築され、 DefaultOptionsV1() で従来の動作を再現します。v1のAPIと動作は変わりません。

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

1. アロケーションフリーのストリーミング処理で高パフォーマンスを実現
大量のJSONを扱うサービスで MarshalerTo / UnmarshalerFrom を実装することで、中間バイト列のアロケーションを排除できます。ベンチマークではUnmarshalがv1比で最大10倍の高速化が確認されています。
2. 呼び出し側から型ごとの変換ロジックを注入できる
これまで不可能だった「特定の型だけ独自のシリアライズ処理を適用する」ことが、コードを変更せずに実現できます。
3. オプションによる柔軟な動作制御

// Before: v1では動作を変更する手段がなかった
data, err := json.Marshal(v)
// After: v2ではオプションで動作を制御できる
data, err := jsonv2.Marshal(v,
    jsonv2.Deterministic(true),            // マップキーの順序を固定
    jsontext.WithIndent("  "),             // インデント出力
    jsonv2.WithMarshalers(
        jsonv2.MarshalToFunc(func(enc *jsontext.Encoder, t time.Time) error {
            return enc.WriteToken(jsontext.String(t.Format(time.RFC3339)))
        }),
    ),
)

4. 新しいstructタグによる表現力の向上

type User struct {
    Name      string    `json:"name"`
    CreatedAt time.Time `json:"created_at,omitzero,format:RFC3339"` // 新: omitzero, format
    Internal  string    `json:"internal,inline"`                    // 新: inline
    Config    map[string]any `json:",inline"`                       // JSON objectのインライン展開
}

5. 正確なエラー情報

// v2ではエラー発生箇所をJSONポインタで特定できる
// &json.SemanticError{ByteOffset:42, JSONPointer:"/users/3/age", ...}

議論のハイライト

  • Options 型の設計: 単一の Options インターフェース型を構文層・意味層・マーシャル・アンマーシャルで共用する設計は、ワーキンググループが多くの代替案(オプション構造体等)を徹底検討した末に採用されたものです。可変長引数での渡し方と後勝ちのマージ挙動が最も人間工学的と判断されました。
  • jsontext*Encoder / *Decoder の具体型を使用: インターフェースにすることで拡張性が増しますが、全トークン書き込みにバーチャルメソッドコールが発生しパフォーマンスが大幅に低下するため、具体型を採用しました。将来的にカスタム実装を登録できるAPIの追加は検討中です。
  • v1の動作互換性オプション群: v1の動作をv2上で再現するための DefaultOptionsV1() には20個を超えるオプションが含まれています。これはHyrumの法則(意図せず安定した振る舞いが依存される現象)により多くのバグ的挙動がデファクトのAPIとなっていたためです。
  • omitempty の再定義: v1ではGoの型システム(falsy値かどうか)で定義されていましたが、v2ではJSONの型システム(JSONとして空の値かどうか)で再定義しました。既存コードへの影響は bool や数値型の omitempty に限られ、新設の omitzero で同等の動作を実現できます。
  • スコープの絞り込み: ユーザー定義オプション、[]byte/[N]byteformat:string サポートなど有益な機能はあえて初回リリースから除外し、安定したコアAPIの確定を優先しました。

関連リンク