Go 1.25 encoding/json/v2 の動作変更点と v1 互換性レイヤーの分析
By lemonmint views ...

Goのencoding/jsonパッケージv2は、既存のv1が抱えていた複数の欠点(一貫性の不足、驚くべき動作、性能上の問題)を改善するための新しい実装です。これはgoexperiment.jsonv2ビルドタグによって有効化される実験的な機能です。
最も重要な点は、v2が有効化されると、v1はv2の実装の上にv1の動作をエミュレートする互換性レイヤーとして動作するということです。これはv2_v1.goファイルのDefaultOptionsV1()関数を通じて実現されます。つまり、v2はv1の動作を完全に再現できるオプションを提供すると同時に、より厳格で予測可能な新しいデフォルト動作を提示します。
v2の主な目標は次のとおりです。
- 正確性および予測可能性の向上: デフォルトでより厳格な規則(例: 大文字小文字の区別、重複キーの禁止)を適用し、予期しない動作を減らします。
- 性能の改善: パーシングおよびエンコーディングエンジンを再設計し、効率を高めました。
- 柔軟性および制御権の拡大: 詳細なオプションシステムを導入し、開発者がJSON処理の方式を細かく制御できるようにします。
主要な意味・動作の相違点
v2_test.goファイルを中心に、v1とv2の動作の相違点を項目別に整理しました。
1. フィールド名のマッチング(大文字小文字の区別)
- v1の動作: JSONオブジェクトのメンバーをGoの構造体フィールドにアンマーシャルする際、**大文字小文字を無視(case-insensitive)**してマッチングします。
"FirstName"と"firstname"はいずれもFirstNameフィールドにマッピングされます。 - v2の動作: デフォルトでは**大文字小文字を区別(case-sensitive)**し、正確に一致するフィールドのみをマッピングします。
- 変更の理由: 大文字小文字を無視したマッチングは予期しない動作の原因となり得るうえ、一致しないフィールドを処理する際に性能低下を引き起こします。v2はより明確で予測可能な動作をデフォルトとして採用しました。
- 関連オプション: v2では、
json:"...,case:ignore"タグオプションを使用してフィールド単位で大文字小文字の無視を明示的に有効化するか、json.MatchCaseInsensitiveNames(true)オプションをグローバルに適用できます。
2. omitemptyタグオプションの意味の変更
- v1の動作: Go値の「空(empty)の状態」を基準にフィールドを省略します。ここで「空の状態」とは、
false、0、nilのポインタ/インターフェース、長さが0の配列/スライス/マップ/文字列を意味します。 - v2の動作: エンコードされたJSON値の「空の状態」を基準にフィールドを省略します。すなわち、
null、""、{}、[]としてエンコードされる場合に省略されます。 - 変更の理由: v1の定義はGoの型システムに依存しています。v2はJSONの型システムを基準とすることで、より一貫した動作を提供します。例えば、v1では
bool型のfalse値は省略されますが、v2ではfalseは空ではないJSON値であるため省略されません。v2ではomitzeroオプションを追加し、v1のomitemptyが0やfalseに適用していた動作を代替できるようにしました。 - 関連オプション: v2でv1と同一の動作を望む場合は、
json.OmitEmptyWithLegacyDefinition(true)オプションを使用します。
3. stringタグオプションの動作の変更
- v1の動作: 数値、真偽値、文字列型のフィールドに適用されます。該当する値をJSON文字列の中に再びエンコードします(例:
int(42)->"42")。複合型(スライス、マップなど)の内部の値には再帰的に適用されません。 - v2の動作: 数値型にのみ適用され、再帰的に適用されます。すなわち、
[]intのようなスライス内部の数値もすべてJSON文字列としてエンコードされます。 - 変更の理由:
stringオプションの主な用途は、64ビット整数の精度損失を防ぐために数値を文字列として表現することです。v1の動作は限定的で一貫性に欠けていました。v2はこの中核的な用途に集中し、動作を再帰的に拡張してより有用なものとしました。 - 関連オプション:
json.StringifyWithLegacySemantics(true)オプションによりv1の動作を模倣できます。
4. nilスライスおよびマップのマーシャリング
- v1の動作:
nilスライスとnilマップはnullとしてマーシャリングされます。 - v2の動作: デフォルトでは、
nilスライスは[](空の配列)、nilマップは{}(空のオブジェクト)としてマーシャリングされます。 - 変更の理由:
nilはGo言語の実装の詳細であり、言語に依存しないJSON形式にこれを露出させることは望ましくありません。空のコレクションを表す[]や{}の方がより一般的な表現です。 - 関連オプション: v2では
json.FormatNilSliceAsNull(true)またはjson.FormatNilMapAsNull(true)オプションを使用することで、v1と同様にnullとしてマーシャリングできます。
5. 配列のアンマーシャリング
- v1の動作: Goの配列(
[N]T)へアンマーシャルする際、JSON配列の長さがGo配列の長さと異なってもエラーは発生しません。長さが短い場合は余った領域をゼロ値で埋め、長い場合は超過分を破棄します。 - v2の動作: JSON配列の長さはGo配列の長さと正確に一致しなければなりません。 一致しない場合はエラーが発生します。
- 変更の理由: Goにおいて固定サイズの配列は、その長さが重要な意味を持つ場合が多くあります。v1の動作はデータの無音の損失を引き起こし得ます。v2はより厳格な規則により正確性を高めました。
- 関連オプション:
json.UnmarshalArrayFromAnyLength(true)オプションによりv1の動作を模倣できます。
6. time.Durationの処理
- v1の動作:
time.Durationは内部的にint64として扱われ、ナノ秒単位のJSON数値としてエンコードされます。 - v2の動作:
time.Duration.String()メソッドを使用し、"1h2m3s"のような形式のJSON文字列としてエンコードされます。 - 変更の理由: ナノ秒の数値は可読性が低く、
time.Durationの標準的な文字列表現の方がより有用です。 - 関連オプション:
json:",format:nano"タグオプション、またはjson.FormatTimeWithLegacySemantics(true)オプションを使用することで、v1の動作を利用できます。
7. 無効なUTF-8の処理
- v1の動作: マーシャリング/アンマーシャリングの際、文字列内に無効なUTF-8バイトがあると、Unicode置換文字(
\uFFFD)に黙って置き換えます。 - v2の動作: デフォルトでは、無効なUTF-8に遭遇するとエラーを返します。
- 変更の理由: データの無音の破損を防ぎ、より厳格なJSON標準(RFC 7493)に従うためです。
- 関連オプション:
jsontext.AllowInvalidUTF8(true)オプションによりv1の動作を模倣できます。
8. 重複したオブジェクトメンバー名の処理
- v1の動作: JSONオブジェクト内に同じ名前のメンバーが重複して現れることを許容します。最後に現れた値で上書きします。
- v2の動作: デフォルトでは、重複したメンバー名があるとエラーを返します。
- 変更の理由: RFC 8259標準は重複名の動作を定義していないため、実装ごとに異なる動作をする可能性があります。これはセキュリティ上の脆弱性の原因となり得ます。v2はこれを明示的に拒否することで、正確性と安全性を高めました。
- 関連オプション:
jsontext.AllowDuplicateNames(true)オプションによりv1の動作を模倣できます。
実装およびアーキテクチャの相違点
- v1:
decode.goのdecodeStateとscanner.goの手動で記述されたステートマシン(state machine)に大きく依存します。これはパーシングのロジックと意味解析が強く結合したモノリシックな構造です。 - v2: アーキテクチャがより モジュール化されました。
encoding/json/jsontext: 低レベルで高性能なJSONトークナイザ(Decoder)とエンコーダ(Encoder)を提供します。このパッケージはJSONの構文(syntax)的な側面のみに集中します。encoding/json/v2:jsontextを基盤として、Goの型とJSON値の間の意味的(semantic)変換を処理します。- このような分離により、構文解析と意味解析が分離され、コードの明確性と性能が向上しました。
v2の新しいAPIおよび機能
v2はjson.Optionsシステムを通じて非常に柔軟な制御機能を提供します。
json.Options: マーシャリング/アンマーシャリングの動作を変更するオプションの集合です。json.JoinOptions(...): 複数のオプションを一つに統合します。WithMarshalers/WithUnmarshalers:Marshaler/Unmarshalerインターフェースを実装せずとも、特定の型に対するシリアライズ/デシリアライズのロジックを注入できる強力な機能です。これは外部パッケージの型を扱う際に特に有用です。- 新しいオプション群:
RejectUnknownMembers、Deterministic(false)、FormatNilSliceAsNullなど、v1では不可能であった多様な動作制御が可能になりました。
結論
encoding/json v2は、v1の経験を基に正確性、性能、柔軟性を大きく向上させた現代的な実装です。デフォルト動作はより厳格になりましたが、精巧なOptionsシステムを通じてv1のすべての動作を完全にサポートするため、既存コードとの互換性を維持しながら、段階的にv2の利点を導入することができます。
- 新規プロジェクトであれば、v2をデフォルトとして使用することが望ましいです。
- 既存プロジェクトは
jsonv1をそのまま使用するか、jsonv2へ移行しながらDefaultOptionsV1()を通じてv2の厳格な動作を段階的に導入する戦略を採用できます。