GoSuda

Go 1.25 encoding/json v1 vs v2 比較

By lemonmint
views ...

Goのencoding/jsonパッケージv2は、既存のv1が抱えていた複数の欠点(一貫性の不足、予期せぬ動作、パフォーマンスの問題)を改善するために設計された新しい実装です。これは、goexperiment.jsonv2ビルドタグを通じて有効化される実験的な機能です。

最も重要な点は、v2が有効化されると、v1はv2実装の上にv1の動作をエミュレートする互換性レイヤーとして機能することです。これは、v2_v1.goファイルのDefaultOptionsV1()関数を通じて実現されます。すなわち、v2はv1の動作を完全に再現するオプションを提供しつつ、同時に、より厳格で予測可能な新しいデフォルトの動作を提示します。

v2の主な目標は以下の通りです。

  1. 正確性および予測可能性の向上: デフォルトでより厳格なルール(例: 大文字小文字の区別、重複キーの禁止)を適用することで、予期せぬ動作を削減します。
  2. パフォーマンスの改善: パースおよびエンコーディングエンジンを再設計することで、効率性を高めました。
  3. 柔軟性および制御権の拡大: 詳細なオプションシステムを導入することで、開発者が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)状態」を基準にフィールドを省略します。ここでいう「空状態」とは、false0nilポインター/インターフェース、長さが0の配列/スライス/マップ/文字列を意味します。
  • v2の動作: エンコードされたJSON値の「空状態」を基準にフィールドを省略します。すなわち、null""{}[]としてエンコードされる場合に省略されます。
  • 変更理由: v1の定義はGoの型システムに依存的です。v2はJSON型システムを基準とすることで、より一貫した動作を提供します。例えば、v1ではbool型のfalse値が省略されますが、v2ではfalseは空ではないJSON値であるため省略されません。v2ではomitzeroオプションを追加することで、v1のomitempty0falseに適用されていた動作を代替できます。
  • 関連オプション: 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バイトが存在する場合、ユニコード代替文字(\uFFFD)に静かに置換します。
  • v2の動作: デフォルトで無効なUTF-8に遭遇した場合、エラーを返します
  • 変更理由: データのサイレントな損傷を防ぎ、より厳格なJSON標準(RFC 7493)に準拠するためです。
  • 関連オプション: jsontext.AllowInvalidUTF8(true)オプションを通じてv1の動作を模倣できます。

8. 重複したオブジェクトメンバー名の処理

  • v1の動作: JSONオブジェクト内に同一のメンバー名が重複して現れることを許容します。最後に現れた値で上書きされます。
  • v2の動作: デフォルトで重複したメンバー名が存在する場合、エラーを返します
  • 変更理由: RFC 8259標準は重複名の動作を定義しておらず、実装ごとに異なる動作をする可能性があります。これはセキュリティ脆弱性の原因となる可能性があります。v2はこれを明示的に拒否することで、正確性とセキュリティを高めました。
  • 関連オプション: jsontext.AllowDuplicateNames(true)オプションでv1の動作を模倣できます。

実装およびアーキテクチャの相違点

  • v1: decode.godecodeStatescanner.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(...): 複数のオプションを1つに結合します。
  • WithMarshalers / WithUnmarshalers: Marshaler/Unmarshalerインターフェースを実装せずに、特定の型に対する直列化/逆直列化ロジックを注入できる強力な機能です。これは外部パッケージの型を処理する際に特に有用です。
  • 新しいオプション群: RejectUnknownMembersDeterministic(false)FormatNilSliceAsNullなど、v1では不可能だった多様な動作制御が可能になりました。

結論

encoding/json v2は、v1の経験を基に正確性、パフォーマンス、柔軟性を大幅に向上させた現代的な実装です。デフォルトの動作はより厳格になりましたが、精巧なOptionsシステムを通じてv1のすべての動作を完全にサポートするため、既存のコードとの互換性を維持しながら、段階的にv2の利点を導入できます。

  • 新規プロジェクトであれば、v2を基本として使用することをお勧めします。
  • 既存プロジェクトは、jsonv1をそのまま使用するか、jsonv2へ移行しつつ、DefaultOptionsV1()を通じて段階的にv2の厳格な動作を導入する戦略を採用できます。