Slide 1

Slide 1 text

encoding/json/v2で何が変わるか ~~ v1からv2への変化を徹底⽐較 ~~ 2025/ 09/ 27 Ubie株式会社 永野峻輔 (@glassmonkey) Go Conference 2025

Slide 2

Slide 2 text

お品書き 従来のencoding/json (v1) の課題 encoding/json/v2 (v2)の設計思想‧コンセプト v1とv2の変更点 v2への移⾏検証 公式ベンチマーク まとめ

Slide 3

Slide 3 text

3 自己紹介 3 永野峻輔 (@glassmonekey) 所属 2024/02 ~ Ubie株式会社 Technology Platform パーソナライズ基盤 Tech Lead 趣味 個人開発, ゲーム, 読書 https://php-play.dev

Slide 4

Slide 4 text

4 Mission 4 テクノロジーで人々を適切な医療に案内する

Slide 5

Slide 5 text

5 Ubie実施の患者サーベイ (2022年2月実施、N=4,462、対象:直近3ヶ月に症状発症経験のある人) 生活者の行動 医療従事者の行動 発症・自覚 受診検討 受診 診断 直近3ヶ月に何かしらの発症があった人を 100とした時 100 66 48 38 - 62% マッチング不全 発症・自覚から適切な診断に至るのはごくわずかです。

Slide 6

Slide 6 text

日本の高い医療技術×テクノロジーから生まれた問診エンジン( AI)とプラットフォーム 6 創業者の思いから 生まれ独自のプラット フォームにより磨かれた 問診エンジン - 50名以上の医師監修のもと、 国内外5万本の医学論文を元に 作られた問診エンジン - 1800以上の医療機関からの フィードバックにより 問診エンジンの精度を向上 問診エンジンをコア技術に据えたプラットフォーム 問診エンジン 月間1200万人以上の生活者が利用 メガファーマの約8割が活用 15,000件以上の医療機関と連携 生活者向け 医療機関向け 製薬企業向け

Slide 7

Slide 7 text

7 2020年のサービス提供開始以来、多くの方の適切な医療へのアクセスを支援しています 月間利用者数 1200万人 提携医療機関数 1万5000以上 累計利用回数 1億 8000 万回以上 対応する症状 3500以上 ユビーを利用した後 実際に受診した人数(推計) 1838万人 対応する病名 1100以上 ユビーを利用したうち 「受診してよかった」 91.1% アカウント登録数 500万人

Slide 8

Slide 8 text

従来の encoding/jsonの課題

Slide 9

Slide 9 text

動作上の課題 標準仕様と安全性の問題 encoding/json v1には、JSON標準仕様への不完全な準拠と、潜在的なセキュリティリスクを引 き起こす複数の動作上の⽋陥が存在します。 ● 重複キーの受理:JSONオブジェクト内の重複キーを受け⼊れてしまい、最後の値の みが残るため、セキュリティ上の脆弱性となる ● 無効UTF-8の許容:RFC 8259が有効なUTF-8を要求しているのに対し、現⾏の実装で はデータ破損リスクを招く ● 大文字小文字無視: RFC 8259では区別するが現⾏の仕様では区別をしない ● 一貫性の欠落: ポインタレシーバーのMarshalJSONで一貫性が欠落する ● map/sliceのゼロ値: map/sliceのゼロ値はnullではなく{}/[]で扱いたい

Slide 10

Slide 10 text

動作上の課題例その1:重複キー問題の具体例 RFC 8259では、重複キー処理に関する具体的な規定はなく、各実装は「任意の値の選択」「値の結合」「値 の破棄」「エラー報告」などから選べます。この曖昧さは普遍的な意味付けを困難にします。 var m map[string]int err := json.Unmarshal([]byte(`{"x":1,"x":2}`), &m) fmt.Println(err) fmt.Println(m) // // map[x:2](最後の値が採用) この例では、同じキー "x" が2回出現していますが、エラーは発⽣せず、2回⽬の値が採⽤されます。v1 実装では⼊⼒の問題を黙って処理します。v2ではエラーになります。 セキュリティリスク: 採⽤値が⼊⼒順に依存して変動するため、署名検証‧監査‧差分適⽤のシナリオで問題が発⽣します。 CVE-2017-12635など、実際の脆弱性として悪⽤された事例もあります。

Slide 11

Slide 11 text

動作上の課題例その2:無効UTF-8の許容問題 RFC 8259では、全てのJSONテキストはUTF-8でエンコードされることを明確に規定しています。しかし、v1 のJSON処理は無効なUTF-8シーケンスを許容してしまいます。 // 無効なUTF-8シーケンスを含むJSON文字列 input := []byte(`{"key": "\uD800"}`) var result map[string]interface{} err := json.Unmarshal(input, &result) fmt.Println(err) fmt.Println(result["key"]) // // 不正なUTF-8文字が含まれた文字列 この例では、孤⽴したサロゲートペア(\uD800)を含む無効なUTF-8シーケンスがエラーなく処理されていま す。v1実装では、これらの問題を黙って受け⼊れてしまいます。v2ではこれもエラーになります

Slide 12

Slide 12 text

動作上の課題例その3:⼤⽂字⼩⽂字無視 RFC 8259では、キーは⼤⽂字⼩⽂字を区別すると定めていますが、Goのencoding/jsonでは構造体マッピン グ時に⼤⼩無視の⽐較を⾏うため、異なるキーが同⼀視されてしまい、解釈の不⼀致を⽣む要因となりま す。 type User struct { UserName string UserID int } data := []byte(`{"username":"admin", "UserID":345, "userid":12345}`) var user User err := json.Unmarshal(data, &user) fmt.Println(user, err) // {admin 12345} v1では「UserName」フィールドに「username」(⼩⽂字)のJSONキーがマッチします。前述の重複キーの問題と合わせ てuseridかUserIDのどちらを使うか不明瞭です。(前述の通り2回⽬の値が優先されます) v2では⼤⽂字⼩⽂字が⼀致するものが優先されます

Slide 13

Slide 13 text

動作上の課題例その4:map/sliceの零値 type User struct { Name string Hobbies []string Profile map[string]string } us := User{ Name: "John", } text, _ := json.Marshal(us) fmt.Println(string(text)) // {"Name":"John","Hobbies":null,"Profile":null} 外部システム連携を加味するとnullはエラーを誘発する要因になりえます。 加えて、空オブジェクト‧空配列になることが好ましいというアンケート結果が寄せられています。

Slide 14

Slide 14 text

アンケート結果 https://github.com/golang/go/discussions/63397#discussioncomment-7201222

Slide 15

Slide 15 text

動作上の課題例その4:⼀貫性の⽋落 type T bool func (v *T) MarshalJSON() ([]byte, error) { return []byte{'1'}, nil } type S struct { X T } func main() { v := S{true} e := json.NewEncoder(os.Stderr) e.Encode(v) // {"X":true} e.Encode(&v) // {"X":1} } https://github.com/golang/go/issues/22967 より ポインタレシーバー時に、vと&vで⼀貫性が失われる場合がある。 v1ではvが値のときはMarshalJSONが未実装扱いとなるので振る舞いに差がでる。

Slide 16

Slide 16 text

APIの扱いにくさ ● io.Readerからの正確なアンマーシャルが困難 - json.NewDecoder(r).Decode(v)では⼊⼒末尾のゴミデータ を拒否できない ● オプション設定の柔軟性不⾜ - EncoderとDecoderのオプションがMarshalやUnmarshal関数で使⽤不可 ● ユーティリティ関数の柔軟性不⾜ - Compact、Indent、HTMLEscape関数は柔軟性に⽋ける出⼒設計 実務上の課題 安全なJSONパースには追加コードが必要 カスタム処理に必要な独⾃実装の増加 複雑なデータ構造処理のための冗⻑なボイラープレートコード

Slide 17

Slide 17 text

APIの扱いにくさ:io.Readerアンマーシャル問題の具体例 r := strings.NewReader(`{"a":1} true`) dec := json.NewDecoder(r) var v map[string]int _ = dec.Decode(&v) if err := dec.Decode(&struct{}{}); err != io.EOF { return errors.New("extra data after top-level JSON value") } // 1値目のみ受理され、余剰データがあっても警告なし // 正しく実装するには追加のコードが必要 io.Readerから正確にアンマーシャルすることは難しく、標準的な⽅法では不⼗分です。 多くの開発者が json.NewDecoder(r).Decode(v) と書きますが、これは⼊⼒の末尾にある余分なデータを検出でき ません。 標準パッケージは余剰データを⾃動的に検出する機能を提供せず、追加コードが必要になります。

Slide 18

Slide 18 text

APIの扱いにくさ:オプション設定の制限 // v1では各インスタンスに個別設定が必要 dec1 := json.NewDecoder(reader1) dec1.DisallowUnknownFields() dec2 := json.NewDecoder(reader2) dec2.DisallowUnknownFields() // Marshal/Unmarshalには同様のオプションがない data, err := json.Marshal(v) // カスタムUnmarshalJSONメソッドにはオプションが引き継がれない func (c *Custom) UnmarshalJSON(data []byte) error { } encoding/json v1では、伝搬できないケースや都度デコードの設定をする必要があるのでプロジェクト以下で ⼀貫した設定反映をやることに⽀障がある。 ある型が MarshalJSON / UnmarshalJSON を実装するとオプションが無効化され、同じデータ型でも結果が変 わる可能性がある。

Slide 19

Slide 19 text

APIの扱いにくさ:ユーティリティ関数の柔軟性不⾜ src := []byte(`{"name":"John"}`) var buf bytes.Buffer _ = json.Compact(&buf, src) // json.Compact(os.Stdout, src) これはできない buf.WriteTo(os.Stdout) } ⼊⼒は []byte、出⼒は *bytes.Buffer 固定なので、⼊出⼒とも丸ごとメモリに載せる前提である。 そのため巨⼤データがそもそも扱いずらいし、ファイル書き込みなどの様々なユースケースへの対応する際に Io.Writerが直接使えないのでひと⼿間必要。

Slide 20

Slide 20 text

パフォーマンス制約 encoding/json v1のAPI設計は、内部実装の詳細を別としても、パフォーマンス⾯での根本的な 制約を強いています。 ● ストリーミング⾮対応: 実際には全JSONを内部バッファリング ● 強制メモリアロケーション: MarshalJSON が[]byteを返すI/Fなので ● ⼆度読み‧先読み強制: UnmarshalJSON の仕様で境界検知とパースで2回パース処理が⾛っている ⼆次関数的な性能劣化: 特にネストした構造体で各レベルがMarshalJSON/UnmarshalJSONを実装すると、再帰的な⼆重処理により計 算量が指数関数的に増加。OpenAPI仕様のパースなど複雑な構造では数秒→数分の遅延が発⽣。Kubernetesな ど実運⽤システムでも問題となっている(kubernetes/kube-openapi#315)。

Slide 21

Slide 21 text

v2の設計思想‧コンセプト

Slide 22

Slide 22 text

v2開発の歴史 2020年後半 同時期 2021年 2022年前半 Daniel Mart(encoding/jsonメンテナ)がv2の構想を最初にドラフト 2構想が⾮公式ながらGo開発者間で共有され始め、Google社外の有志も含めたプロジェクトとして動き出し 初期プロトタイプのデザインドキュメントが作成され、Go開発チーム内で議論開始 v2仕様の成熟と実運⽤での試⾏ 2023年 2024年 2025年 (現在) 公開発表とコミュニティでの議論。北⽶GopherConカンファレンスにて「The Future of JSON in Go」と題した講演 公式提案の準備と実験的実装の導⼊。Goプロジェクトの公式提案としてまとめ上げる 正式提案の承認とGo 1.25での実験的リリース

Slide 23

Slide 23 text

v2のコンセプト 構⽂と意味の分離 JSON処理を2つの主要コンポーネントに分解し、責務を明確に分離した設計: ● 構⽂機能(jsontext):JSONの⽂法に基づく処理に特化 ● 意味機能(json/v2):JSONとGo値の相互変換を定義

Slide 24

Slide 24 text

⼆層アーキテクチャの実装詳細 ⼆層アーキテクチャの具体的な実装とインターフェース設計 json/v2の⼆層アーキテクチャは、各レイヤーが明確な責任を持ち、相互に独⽴して動作することで⾼い柔軟性を実現し ています。 それぞれの層は専⽤のインターフェースと実装で構成されています。 jsontext(構⽂層) json/v2(意味層) トークンベースのローレベル操作 JSONトークンを直接扱うEncoder/Decoder RFC 8259に厳密に準拠した検証 位置情報を保持した詳細なエラー報告 Go型とJSONの相互変換 拡張可能なオプション機構 カスタムマーシャラー対応 リフレクションベースの型マッピング

Slide 25

Slide 25 text

v1との関係性 json/v2 新しい実装 encoding/json v2を基に再実装 統⼀実装: v1パッケージは内部的にv2の基に再実装されており、同じコードベースを共有 機能継承: v2に追加される後⽅互換機能はv1でも⾃動的に利⽤可能になる バグ修正共有: v2のバグ修正やパフォーマンス改善が両⽅のバージョンに適⽤される 重要:v1パッケージが廃⽌されることはありません。Go 1互換性の約束に基づき、v1の完全サポートは継続されます。

Slide 26

Slide 26 text

新アーキテクチャがもたらす恩恵 ● 柔軟性 構⽂層(jsontext)と意味層(json/v2)の明確な分離により、独⽴利⽤や低レベルカスタマイズが容易に。⽤途や規約に応じて最適なAPI を選択できます。 RFC 8259標準への厳密な準拠により、⼊⼒検証が強化。位置情報を含む詳細なエラーハンドリングで、より安全なJSON処理が可能にな ります。 完全なストリーミング⽅式によるデータ処理で、⼤規模JSONの低メモリ‧⾼速処理が実現。KubernetesのOpenAPI処理などで劇的な性 能向上が実証されています。 ● 正確性 ● 効率性 v1パッケージが内部的にv2ベースで再実装され、段階的な移⾏が容易に。v2の新機能が⾃動的にv1でも利⽤可能になり、バグ修正も両 バージョンに適⽤されます。 ● 互換性

Slide 27

Slide 27 text

柔軟性をもたらす構造 package jsontext type Encoder struct { ... } func NewEncoder(io.Writer, ...Options) *Encoder func (*Encoder) WriteValue(Value) error func (*Encoder) WriteToken(Token) error type Decoder struct { ... } func NewDecoder(io.Reader, ...Options) *Decoder func (*Decoder) ReadValue() (Value, error) func (*Decoder) ReadToken() (Token, error) type Kind byte type Value []byte func (Value) Kind() Kind type Token struct { ... } func (Token) Kind() Kind • 拡張性:標準的なインターフェースでアプリケーション固有の要件に対応可能 package json func Marshal(in any, opts ...Options) (out []byte, err error) func MarshalWrite(out io.Writer, in any, opts ...Options) error func MarshalEncode(out *jsontext.Encoder, in any, opts ...Options) error func Unmarshal(in []byte, out any, opts ...Options) error func UnmarshalRead(in io.Reader, out any, opts ...Options) error func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) error jsontext(構⽂層) json/v2(意味層)

Slide 28

Slide 28 text

柔軟性がもたらす恩恵 ユースケースの拡⼤ 従来では[]byteしか対応してなかったところを、io.Reader/io.Writerに直接書き込めるようになった。 ストリーム処理といった凝ったことをしたいことを⾏いたい場合でも、jsontext.Encoder/jsontext.Decoderを使う ことでカバーできるようになった 26 package json func Marshal(in any, opts ...Options) (out []byte, err error) func MarshalWrite(out io.Writer, in any, opts ...Options) error func MarshalEncode(out *jsontext.Encoder, in any, opts ...Options) error func Unmarshal(in []byte, out any, opts ...Options) error func UnmarshalRead(in io.Reader, out any, opts ...Options) error func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) error json/v2(意味層)

Slide 29

Slide 29 text

豊富なオプション(⼀部抜粋) ● jsontext.WithIndent … 出力のフォーマット内容にインデントを付与する ≒ json.MarshalIndent 26 dist, err := json.Marshal(src, jsontext.WithIndent("\t")) ● json.OmitZeroStructFields … ゼロ値のフィールド (IsZero() boolメソッド = true)をエンコードするときに割愛させる dist, err := json.Marshal(src, json.OmitZeroStructFields(true))

Slide 30

Slide 30 text

RFC 8259準拠の正確な検証 RFC 8259に完全準拠 JSONの仕様(RFC 8259)に完全準拠した⼊⼒検証が可能になりました。より厳格なバリデーションによ り、安全で正確なデータ処理を実現します。 ● 不正なJSONフォーマット検出:形式的に不正なJSONを厳格に検出し処理を中⽌ ● 重複キー拒否:同⼀オブジェクト内の重複キーを検出して拒否、意図しないデータの上書きを防⽌ ● 無効なUTF-8検出:RFC 8259が要求する有効なUTF-8のみを受け⼊れ RFC 8259 仕様準拠 :「JSONオブジェクト内のメンバーの名前は⼀意でなければならない」「JSONテキストはUTF-8でエン コードされなければならない」

Slide 31

Slide 31 text

正確性により詳細なエラー報告が可能に v2では、どのAPIやオプションを選んでも⼀貫した予測可能な動作を保証します。 これにより詳細なエラー報告が可能になりました ⾏と列の正確な位置情報:問題箇所への素早いアクセスが可能 コンテキスト付きエラー:何が期待され何が実際に起きたかを説明 デバッグ効率の⼤幅向上:開発‧テスト時間の短縮に貢献 err = json.Unmarshal(data, &value) // エラーメッセージ例: // "json: line 42, column 15: unexpected character '{' in field name"

Slide 32

Slide 32 text

厳密になった例 重複キーのエラー化 json/v2は最新のJSON標準仕様に厳格に準拠し、不正な⼊⼒に対する防御を強化しました。 // v1では重複キーを許容最後の値が採用される err := json.Unmarshal([]byte(`{ "name": "太郎", "name": "花子" }`), &data) fmt.Println(err) fmt.Println(data.Name) // v2では重複キーでエラー import "encoding/json/v2" err := json.Unmarshal([]byte(`{ "name": "太郎", "name": "花子" }`), &data) fmt.Println(err) // nil // "花子" // 重複キーエラー

Slide 33

Slide 33 text

厳密になった例 ⼤⽂字‧⼩⽂字のマッチの厳格化 json/v2では⼤⽂字⼩⽂字の区別をするようになりました。 // v1では大文字小文字の区別がない type User struct { Name string `json:"name"` } err := json.Unmarshal([]byte(`{"NAME": "太郎"}`), &user) fmt.Println(err) // nil fmt.Println(user.Name) // 太郎 // v2ではデフォルト大文字小文字をくべつする type User struct { Name string `json:"name"` } var user User err := json.Unmarshal([]byte(`{"NAME": "太郎"}`), &user) fmt.Println(err) // nil fmt.Println(user.Name) // “” // オプションにより互換性のある動作は可能 err = json.Unmarshal([]byte(`{"NAME": "次郎"}`), &user, json.MatchCaseInsensitiveNames(true) ) fmt.Println(user.Name) // 次郎

Slide 34

Slide 34 text

扱いが変わった例 map/sliceのnilの⾮null化 json/v2では直感的に期待されうる動作としてslice/mapのnull時の挙動が変わります // v1ではnilはnull type User struct { Name string Hobbies []string Profile map[string]string } payload, _ := json.Marshal(User{Name: "太郎"}) fmt.Println(string(payload)) // {"Name":"太郎","Hobbies":null,"Profile":null} // v2ではnilは空配列/空オブジェクト type User struct { Name string Hobbies []string Profile map[string]string } payload, _ := json.Marshal(User{Name: "太郎"}) fmt.Println(string(payload)) // {"Name":"太郎","Hobbies":[],"Profile":{}} // オプションにより互換性のある動作は可能 payload, _ = json.Marshal(User{Name: "次郎"}, json.FormatNilMapAsNull(true), json.FormatNilSliceAsNull(true), ) fmt.Println(string(payload)) // {"Name":"次郎","Hobbies":null,"Profile":null}

Slide 35

Slide 35 text

効率性により、 ストリーミング処理によるメモリ効率向上 v2では⼤きなJSONデータも「ストリーミング処理」で段階的に処理可能になりました。 巨⼤ファイルも低メモリで扱える:全データをメモリに⼀度に読み込まずに処理可能 バッファに全てを載せず、⼀部ずつ読み書き∕変換可能:データサイズの制約を⼤幅に緩和 IoTや⼤規模データ基盤でも活躍:限られたリソースでも効率的に動作 // ストリーミング処理の例 file, _ := os.Open("large_data.json") defer file.Close() var data MyStruct // ファイルから直接Unmarshal(メモリ効率良く) err := json.UnmarshalRead(file, &data)

Slide 36

Slide 36 text

直感的で⾼速なAPI仕様に json/v2ではAPI設計が根本から⾒直され、開発者の⽇常的な実装効率を⼤幅に向上させていま す。 streamでの変換∕検証を標準サポート:io.ReaderやWriterを直接活⽤するインター フェース設計により、余分なコードが不要に 並列処理やパイプライン構成にも強い設計:複雑なデータフローもシンプルに表現可能に // v1では重複キーを許容最後の値が採用される err := json.Unmarshal([]byte(`{ "name": "太郎", "name": "花子" }`), &data) fmt.Println(err) fmt.Println(data.Name) // v2では重複キーでエラー import "encoding/json/v2" err := json.Unmarshal([]byte(`{ "name": "太郎", "name": "花子" }`), &data) fmt.Println(err) // nil // "花子" // 重複キーエラー

Slide 37

Slide 37 text

v1とv2の変更点

Slide 38

Slide 38 text

変わらないもの

Slide 39

Slide 39 text

基本APIの⼿触りは維持 json/v2は多くの改善を含みつつも、既存の知識と資産を活かせるように基本的な 操作性を継承しています。 Marshal / Unmarshal の使い⼼地は概ね同じ 導⼊は段階的に可能(既存v1コードを新実装で検証可能)

Slide 40

Slide 40 text

コード例:基本APIとstructタグの互換性 v2は基本的なAPIの互換性を維持しつつ、structタグの使⽤⽅法も継承しています。v1からの移⾏時にコード の⼤幅な書き換えは必要ありません。 // v1 import "encoding/json" type User struct { Name string `json:"name"` Age int `json:"age,omitempty"` Email string `json:"-"` // 無視 Updated bool `json:",omitempty"` // フィールド名使用、空なら省略 } data, err := json.Marshal(user) // v2(互換性維持) import "encoding/json/v2" // 同じ構造体定義とタグ記法がそのまま使用可能 data, err := json.Marshal(user) v2では、v1と同じ基本APIやstructタグの記法をそのまま使⽤できます。これにより、既存コードから の段階的な移⾏が容易になります。

Slide 41

Slide 41 text

変わるもの

Slide 42

Slide 42 text

⼊⼒検証の厳密化 ● v2の厳格なバリデーション 従来のv1では曖昧な⼊⼒が許容されていましたが、v2では既定で以下の厳密な検証を実施します: 重複キーを拒否:JSONオブジェクト内の重複名は既定でエラーに(CVE脆弱性対策) 無効UTF-8を拒否:RFC 8259準拠でUTF-8の厳密検証を実施 フィールド名は⼤⽂字⼩⽂字を区別:既定で厳密な名前⼀致を要求 ● エラー種別の明確化 検証エラーがより詳細になり、問題の切り分けが容易になりました。⼊⼒段階での早期検出により、下 流での予期せぬ不整合を防⽌します。 互換性が必要な場合は、明⽰的なオプションで検証を緩和できます: ● jsontext.AllowDuplicateNames(true) ● jsontext.AllowInvalidUTF8(true) ● json.MatchCaseInsensitiveNames(true)

Slide 43

Slide 43 text

厳密になった例 (再掲) ⼤⽂字‧⼩⽂字のマッチの厳格化 json/v2では⼤⽂字⼩⽂字の区別をするようになりました。 // v1では大文字小文字の区別がない type User struct { Name string `json:"name"` } err := json.Unmarshal([]byte(`{"NAME": "太郎"}`), &user) fmt.Println(err) // nil fmt.Println(user.Name) // 太郎 // v2ではデフォルト大文字小文字をくべつする type User struct { Name string `json:"name"` } var user User err := json.Unmarshal([]byte(`{"NAME": "太郎"}`), &user) fmt.Println(err) // nil fmt.Println(user.Name) // “” // オプションにより互換性のある動作は可能 err = json.Unmarshal([]byte(`{"NAME": "次郎"}`), &user, json.MatchCaseInsensitiveNames(true) ) fmt.Println(user.Name) // 次郎

Slide 44

Slide 44 text

ストリーミング対応を始めとした新I/F ストリーミング処理対応 従来のv1は「全体をバッファリングしてから処理」が基本でした。v2では真の逐次処理を実現: Reader/Writerに対する必要箇所のみの処理が可能 Token/Valueベースの低アロケAPI(GCプレッシャー軽減) Unmarshal性能は最⼤10倍⾼速化 package json func Marshal(in any, opts ...Options) (out []byte, err error) func MarshalWrite(out io.Writer, in any, opts ...Options) error func MarshalEncode(out *jsontext.Encoder, in any, opts ...Options) error func Unmarshal(in []byte, out any, opts ...Options) error func UnmarshalRead(in io.Reader, out any, opts ...Options) error func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) error

Slide 45

Slide 45 text

io.Readerを使うことでメモリ効率の向上 // v1 func processJSON(r io.Reader) error { // 全データをメモリに読み込む必要がある data, err := io.ReadAll(r) if err != nil { return err } var value interface{} return json.Unmarshal(data, &value) } // v2 func processJSON(r io.Reader) error { // ストリーミング処理 dec := json.NewDecoder(r) var value interface{} return dec.Decode(&value) } v1では、ストリームからのデコードには全データを先 に読み込む必要があり、⼤きなJSONでメモリ消費が問 題になります。 v2では、ストリームから直接デコードでき、厳格な検証 も設定可能です。⼤規模JSONでもメモリ効率が向上し ます。

Slide 46

Slide 46 text

トークン単位で扱い逐次的に処理をすることも可能 // v2での効率的なストリーム処理例 func streamProcess(r io.Reader) error { dec := json.NewDecoder(r) // ストリームのトークンを順次処理 for { tok, err := dec.Token() if err == io.EOF { break } if err != nil { return err } // トークンごとの処理 fmt.Println(tok) } return nil } // 各要素を即時処理

Slide 47

Slide 47 text

⾮直感的な仕様の変更(ピックアップ) omitemptyの挙動変更 // v1 type Data struct { EmptySlice []int `json:"empty_slice,omitempty"` EmptyMap map[string]int `json:"empty_map,omitempty"` } // 空スライス・空mapは省略される // {"other_field": "value"} // v2 type Data struct { EmptySlice []int `json:"empty_slice,omitempty"` EmptyMap map[string]int `json:"empty_map,omitempty"` } // 空スライス・空mapは出力される // {"empty_slice":[],"empty_map":{}} v1では、omitemptyタグがあれば空スライスと空 マップはJSONから省略されます v2では、空スライスと空マップは初期化済みと⾒ なされ、JSONに出⼒されます

Slide 48

Slide 48 text

⾮直感的な仕様の変更(ピックアップ) map/sliceのnilの⾮null化 json/v2では直感的に期待されうる動作としてslice/mapのnull時の挙動が変わります // v1ではnilはnull type User struct { Name string Hobbies []string Profile map[string]string } payload, _ := json.Marshal(User{Name: "太郎"}) fmt.Println(string(payload)) // {"Name":"太郎","Hobbies":null,"Profile":null} // v2ではnilは空配列/空オブジェクト type User struct { Name string Hobbies []string Profile map[string]string } payload, _ := json.Marshal(User{Name: "太郎"}) fmt.Println(string(payload)) // {"Name":"太郎","Hobbies":[],"Profile":{}} // オプションにより互換性のある動作は可能 payload, _ = json.Marshal(User{Name: "次郎"}, json.FormatNilMapAsNull(true), json.FormatNilSliceAsNull(true), ) fmt.Println(string(payload)) // {"Name":"次郎","Hobbies":null,"Profile":null}

Slide 49

Slide 49 text

新しいもの

Slide 50

Slide 50 text

新しいもの導⼊ json/v2で追加された主要な新機能 json/v2では、v1の課題を解決しつつ、開発者の⽣産性と柔軟性を⾼めるための多くの新機能が 導⼊されています。 統合オプションシステム:すべての操作に⼀貫性のあるオプション設定が可能になりました 拡張されたタグ機能:より柔軟で強⼒なstructタグのオプションが提供されます エラー処理の向上:より詳細で有⽤なエラーメッセージが提供されるようになりました カスタマイズ可能なマーシャラー/アンマーシャラー:処理をより細かく制御できます

Slide 51

Slide 51 text

json/v2の新規追加機能:統合オプションシステム 全APIで⼀貫して使えるオプション機能 新機能 すべてのAPIで opts ...json.Options パラメータを使⽤できるようになりました。これに より関数API、io.Reader/Writer、Encoder/Decoderのどの形式でも同じ⽅法でJSONの処理⽅法 をカスタマイズできます。 b, _ := json.Marshal(v, json.Deterministic(true), // 出力順序を一定にする json.RejectUnknownMembers(true) // 未知のフィールドを拒否 ) // Readerにも同じオプションを適用可能 err := json.UnmarshalRead(r, &out, json.RejectUnknownMembers(true)) )

Slide 52

Slide 52 text

タグ機能の拡張 新しいタグ機能 v2では、従来より豊富なタグオプションが提供されます。複雑な表現や特殊ケースも公式なタグ記述 のみで対応可能になり、カスタム実装の必要性を⼤幅に減らします。 標準化された主要タグ 従来は⾃前処理で対応していた要件も、標準タグで直交的に表現できるようになりました。 これにより 保守性が向上し、コードの⼀貫性が⾼まります。 omitzero omitempty format inline unknown Goのゼロ値(=IsZero()がtrue)の場合に省略( omitemptyとは異なる判断基準) JSONの「空」値("", null, [], {} )の場合に省 略 time.Timeなどの日時フォーマット指定 (例: format:'2006-01-02') 構造体フィールドをインライン展開(ネストを平坦 化) 未知のフィールドの扱いを制御

Slide 53

Slide 53 text

タグ機能の拡張:コード例 type User struct { ID int64 `json:"id,string"` // 数値を"123"で入出力 Name string `json:"name"` UserID int `json:"userID,case:ignore"` // user_id なども受け入れ Count int `json:"count,omitzero"` // ゼロ値なら省略 Tags []string `json:"tags,omitempty"` // JSONで空なら省略 CreatedAt time.Time `json:"createdAt,format:RFC3339"` Extra map[string]any `json:",inline"` // 追加キーを親直下へ展開 } // Unmarshal: case:ignore で "user_id" → UserID に入る var u User _ = json.Unmarshal([]byte(`{"id":"123","user_id":7,"name":"A","x":1}`), &u) // Marshal: omitzero/omitempty/format/inline の挙動を確認 out, _ := json.Marshal(User{ ID: 123, Name: "A", CreatedAt: time.Date(2025, 9, 27, 15, 4, 5, 0, time.UTC), Extra: map[string]any{"x": 1}, }) fmt.Println(u.UserID) // => 7 fmt.Println(string(out)) // => {"id":"123","name":"A","createdAt":"2025-09-27T15:04:05Z","x":1}

Slide 54

Slide 54 text

v2への移⾏検証

Slide 55

Slide 55 text

v2への移⾏検証 システム構成 検証プロダクト:社内基盤プロダクト 構成:GraphQL APIサーバー 主な機能:GraphQL API、外部API連携 JSON処理:外部APIコールのためのシリアライズ(Marshal)とデシリアライズ(Unmarshal) 実際の社内プロダクトでjson/v2の移⾏検証を⾏いました。 移⾏対象のプロダクトについては以前blogに書いたので良かったらみてくだい。 https://zenn.dev/ubie_dev/articles/7f393a44ffb029

Slide 56

Slide 56 text

移⾏検証の段階的ステップ 段階的検証アプローチ 以下の3ステップで段階的に移⾏検証を進めます。 ステップ1:v1互換モードでの検証 ステップ2:encoding/jsonからencoding/json/v2への置換 ステップ3:v2による破壊的変更への対応 v2が提供するv1互換機能を使⽤して、既存コードに対する互換性を確認します。 インポートパスを変更し、必要に応じてコードを調整します。 零値の扱いの変化やomitemptyの挙動変更など、v2で発⽣する破壊的変更に対応し、動作検証を ⾏います。 報告先:互換性の問題を発⾒した場合は、 で報告することで、v2の改善と安定化に貢献できま す。 GitHub Issue #71497

Slide 57

Slide 57 text

ステップ1:v1互換モードでの検証 v1互換モードでの検証⼿順 テストコードを変更せずに環境変数を設定して実⾏:GOEXPERIMENT=jsonv2 go test ./... 既存のv1コードのままで互換性をチェック(内部的にはv2の実装が使⽤される) // 既存コードを変更せずにv2互換性を検証 $ GOEXPERIMENT=jsonv2 go test ./... // 特定のパッケージのみ検証 $ GOEXPERIMENT=jsonv2 go test ./pkg/api/...

Slide 58

Slide 58 text

ステップ2&3:encoding/jsonからencoding/json/v2への置換&修正 インポートパスの置換⽅法 encoding/json"を"encoding/json/v2"に置き換えちゃいます。 GOEXPERIMENT=jsonv2 go test ./... でテストを実行して破壊的変更を探しました。 置換前(v1) 置換後(v2) import ( "encoding/json" "fmt" ) func process(data []byte) { var obj map[string]interface{} json.Unmarshal(data, &obj) } import ( "fmt" "encoding/json/v2" ) func process(data []byte) { var obj map[string]interface{} json.Unmarshal(data, &obj) }

Slide 59

Slide 59 text

実際の変更点と留意点:零値の扱いとomitemptyの挙動変更 零値の扱いの変化 移⾏時の注意点 実装例と⽐較 v2では空スライス‧空mapの処理結果と、omitemptyタグの挙 動が変更されています。 値の型 v1での結果 v2での結果 空スライス []string{} omitemptyで出⼒なし 空map map[string]string{} omitemptyで出⼒なし [] として出⼒ {} として出⼒ テストケースの期待値の⾒直しが必要です クライアントアプリケーションが特定の形式を期待している場合 は注意が必要です 既存のJSONパース処理が空配列や空マップの存在に依存してい る場合、ロジックの修正が必要です。 オプションを使って、後⽅互換を維持するか検討しましょう。 // 変更前(v1): type Data struct { EmptySlice []string `json:"empty_slice,omitempty"` EmptyMap map[string]string `json:"empty_map,omitempty"` } d := Data{ EmptySlice: []string{}, EmptyMap: map[string]string{}, } encoded, _ := json.Marshal(d) // 結果: {} // 空オブジェクト // 変更後(v2): // 同じコードで結果が変わる // 結果: {"empty_slice":[],"empty_map":{}}

Slide 60

Slide 60 text

実際の変更点と留意点:io.Reader/io.Writerを使う場合の変化 エンコード/デコード処理の変化 v2では、JSON出⼒時のインターフェースが簡略化されているので、追従する必要があります。 v1: NewEncoderを使う⽅法 v2: MarshalWriteを使う⽅法 v1: NewDecoderを使う⽅法 v2: UnmarshalReadを使う⽅法 func writeJSON(w io.Writer, data MyStruct) error { return json.NewEncoder(w).Encode(data) } import "encoding/json/v2" func writeJSON(w io.Writer, data MyStruct) error { return json.MarshalWrite(w, data) } func readJSON(r io.Reader) (MyStruct, error) { var result MyStruct err := json.NewDecoder(r).Decode(&result) return result, err } import "encoding/json/v2" func readJSON(r io.Reader) (MyStruct, error) { var result MyStruct err := json.UnmarshalRead(r, &result) return result, err }

Slide 61

Slide 61 text

v2への移⾏検証感想 ● v2実装によるv1は従来と仕様へんこうがないので即日できることがわかっ た。 ● omitemptyやシリアライズなどの後方互換破れは影響を受けていたので、多 少の作業が必要なことがわかった。 ● 特にゼロ値関連の影響が大きく、エラー時のレスポンスが影響出ていたの で、念の為にもv2に上げる場合はオプションにより後方互換出ないようにし ておくのが無難そうということがわかった

Slide 62

Slide 62 text

公式ベンチマーク

Slide 63

Slide 63 text

公式JSONベンチマークの紹介 公式実験リポジトリの公開ベンチマーク結果の紹介です。Goエコシステム(CPU‧バージョン‧パラメータ等)の違いによる性能 差があることにご留意ください。実験はGo1.23.5で⾏われています。 参考: https://github.com/go-json-experiment/jsonbench ● JSONv1: encoding/json v1.23.5 ● JSONv1in2: github.com/go-json-experiment/json/v1 v0.0.0-20250127181117-bbe7ee0d7d2c ● JSONv2: github.com/go-json-experiment/json v0.0.0-20250127181117-bbe7ee0d7d2c ● JSONIterator: github.com/json-iterator/go v1.1.12 ● SegmentJSON: github.com/segmentio/encoding/json v0.4.1 ● GoJSON: github.com/goccy/go-json v0.10.4 ● SonicJSON: github.com/bytedance/sonic v1.12.7 ● SonnetJSON: github.com/sugawarayuuta/sonnet v0.0.0-20231004000330-239c7b6e4ce8

Slide 64

Slide 64 text

パフォーマンス⽐較(参考) ⽐較値は全てJSONv1を基準(値=1)として正規化されています。低い値ほど⾼速です。 marshal(具体型): 1.4倍速い〜1.2倍遅い marshal(interface型): 1.6〜3.6倍⾼速。マップのキーソートを⾏わないことによる恩恵あり。 2.3〜5.7倍⾼速 JSONv2はJSONv1と⽐較して ケースまで様々。 marshal(RawValue型): unmarshal(具体型): 2.7〜10.2倍⾼速 JSONv2は他の主要実装と⽐較して 5.6〜12.0倍⾼速 JSONv2はJSONv1と⽐較して 。他実装と⽐べても最速クラス。 JSONv2はJSONv1と⽐較して だが、unsafe最適化を使う他実装より遅いケースも。 unmarshal(interface型): JSONv2はJSONv1と⽐較して で、ほとんどの他実装より⾼速。 詳細なグラフやデータ⽐較は公式リポジトリをご覧ください: https://github.com/go-json-experiment/jsonbench 倍10.2 倍から 21.1 倍⾼速 unmarshal(RawValue型):JSONv2はJSONv1と⽐較して で、ほとんどの他実装より⾼速か同等

Slide 65

Slide 65 text

marshal(具体型) 具体の構造体をエンコードしたケース。 速かったり・遅かったりとまちまち

Slide 66

Slide 66 text

marshal(Interface) いわゆるany型のものをシリアライズしたとき。 jsonv2がほかと比較して1.6〜3.6倍高速

Slide 67

Slide 67 text

marshal(rawValue) 生jsonをシリアライズしたときのケース jsonv2とjsonv1を比較すると高速。unsafe実装を使ったものと同等高速

Slide 68

Slide 68 text

unmarshal(具体型) 具体の構造体にデコードしたケース。 v1と比較するとv2は2.7〜10.2倍高速だが、unsafeが使われるものに比べると遅い。

Slide 69

Slide 69 text

unmarshal(interface) anyにデコードしたケース。 v1に対しては2~5倍速い。他とも若干早い

Slide 70

Slide 70 text

unmarshal(RawValue) json文字列からデコードしたもの v1より10~20倍は早い、他は同等程度

Slide 71

Slide 71 text

正確性‧セキュリティの⽐較 各実装の安全性⽐較(参考情報) その他報告事項 実装 unsafe使⽤ UTF-8検証 重複キー検出 JSONv1(標準) なし 置換 許容 JSONv2(新標準) なし エラー エラー JSONv1in2 なし 置換 許容 GoJSON あり 無視 許容 SonicJSON あり 無視 許容 安全性重視:JSONv2は厳密なUTF-8バリデーションと重複キーの拒否で、最も安全な実装 unsafe利⽤の代償:GoJSONやSonicJSONは⾼速だが、メモリ破損やパニックの報告あり 仕様準拠:JSONv2のみがRFC 8259に完全準拠(JWTなど認証関連での利点) テスト結果:GoJSONではランダムなpanicや失敗が報告されており、プロダクション環境では注意が必要

Slide 72

Slide 72 text

まとめと感想 ● v1には改めて課題があることがわかった。 ● V2は約5年の歳⽉をかけられたコミュニティの悲願とも⾔える成果である ● v2といえど破壊的変更は最⼩限であり、v1との互換性担保の⽅法も⽤意 されてるので段階的に上げることが可能。 ● ぜひみなさんv2を触ってみて、コミュニティに情報を還元しよう!!

Slide 73

Slide 73 text

Ubieブース出展のお知らせ このカンファレンス会場にUbieのブースを出展しています!「テクノロジーで⼈々を適切な医療に案内する」を ミッションに医療×テクノロジーの最前線で挑戦するUbieの取り組みをぜひ体験しにお⽴ち寄りください。

Slide 74

Slide 74 text

74 宣伝 仲間になろう