Coders::JSONでHTMLエスケープが無効化されたJSONシリアライズ

rails/rails

Rails 7.x で JSON をデータベースに永続化する ActiveRecord::Coders::JSON は、デフォルトで HTML エスケープを無効化する設計です。本PRは、エンコーダ生成時に正しくマージ済みオプションが渡されていなかったバグを修正し、エスケープなしのシリアライズを実現します。

背景

このPRが対象とした問題は、ActiveRecord::Coders::JSON がデフォルトで HTML エスケープを無効に設定しているにも関わらず、エンコード結果にエスケープが含まれていたことです。DEFAULT_OPTIONS では { escape: false } が定義されており、initialize@options = DEFAULT_OPTIONS.merge(options) とマージされます。 しかしエンコーダの生成時に options(引数そのまま)が渡され、マージ前の設定が使用されていました。 その結果、Coder::JSON.new.dump({"k"=>"<>&"}){"k":"\u003c\u003e\u0026"} を返していました。

HTML エスケープが適用された JSON は \u003c などのエンティティに変換され、データベース上で期待する文字列と異なります。 この不一致は、JSON カラムの比較や検索、外部サービスへの送信時に予期しない挙動を引き起こす可能性があります。 さらに、ActiveRecord::Type::Json はエスケープなしでシリアライズする実装になっているため、二つのシリアライズ結果が乖離していました。

したがって、エンコーダがデフォルトオプションを無視するバグは、Rails の JSON 永続化における一貫性を欠く根本的な原因となっていました。 この問題を解消すれば、JSON カラムと Type::Json のシリアライズ結果が統一されます。 また、アプリケーションコード側で追加のエスケープ除去処理を書く必要がなくなります。

技術的な変更

修正は activerecord/lib/active_record/coders/json.rb のエンコーダ初期化ロジックを一行変更し、マージ済みの @options を渡すようにしただけです。この変更により escape: false が正しく適用され、HTML エスケープが抑制されます。 さらに、既存の公開 API は一切変更されていないため、利用者側のコードはそのまま動作します。

initialize メソッドではエンコーダを ActiveSupport::JSON::Encoding.json_encoder.new(options) で生成していました。 この options はメソッド引数のままで、DEFAULT_OPTIONS とマージされた @options ではありません。 修正後は ActiveSupport::JSON::Encoding.json_encoder.new(@options) とし、マージ済み設定を渡すように変更しました。

変更前:

@encoder = ActiveSupport::JSON::Encoding.json_encoder.new(options)

変更後:

@encoder = ActiveSupport::JSON::Encoding.json_encoder.new(@options)

この差し替えにより、Coders::JSON の出力は ActiveRecord::Type::Json と同一になり、テスト test_dump_does_not_html_escape が緑化しました。 さらに、エンコーダ生成ロジックの唯一の変更であるため、他の機能への副作用はありません。 既存のシリアライズロジックはそのまま再利用され、パフォーマンスにも影響しません。

設計判断

設計上の判断は、既存の @options オブジェクトをエンコーダに渡すだけの最小限の変更に留めることでした。このアプローチは公開 API を変更せず、後方互換性を完全に保持します。 また新たな設定キーやラッパーメソッドを導入しないことで、コードベースの複雑化を防止しました。

元の実装では options 引数が直接エンコーダに渡され、デフォルトオプションが無視されていました。 修正によりエンコーダは @optionsDEFAULT_OPTIONS とユーザ指定のマージ結果)を受け取り、設計どおりの振る舞いになります。 変更行は 1 行で、他のロジックに影響を与えない純粋なデータフローの修正です。

結果として、Rails の JSON 永続化における一貫性が回復し、開発者は追加設定や回避策を意識せずに済むようになりました。 この変更は、フレームワーク内部のオプション伝播の原則を遵守した例として評価できます。 現在の実装では、@options がエンコーダに正しく伝播することが保証されています。

まとめ

このPRは、ActiveRecord::Coders::JSON が HTML エスケープを無視していたバグを、エンコーダに正しいオプション @options を渡す修正を行いました。 変更はコード一行の差し替えとテスト追加に留まり、既存 API への影響はありません。 その結果、JSON カラムへの永続化がネイティブ実装と完全に一致し、データの整合性が確保されます。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
00f5becf

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
3回 (改善を経て承認)
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文、背景(Context)、技術的変更(Technical Detail)およびまとめ(結論)が明確に存在し、全体が「総論→各論→結論」の流れになっている。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックのファイル名付きハイライトは正しい形式(```ruby:activerecord/lib/active_record/coders/json.rb`)ですが、GitHubリンクが "[PR #57991](url)" となっており、期待される "[#57991](url)" 形式と少し異なる。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Rails の内部実装に関する記述で、専門エンジニア向けの情報量・深さが適切。初心者向けの過剰な解説はない。

パラグラフ・ライティング ⚠ WARNING

トピックセンテンス・1段落1トピック・段落長

背景セクションの最初の段落が7文で構成されており、6文以内の段落という基準を超えている。段落を分割すれば基準を満たす。その他の段落はトピックセンテンスで始まり、1段落1トピック、空行で区切られている。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のコードブロックは Diff の変更点と完全に一致しており、ファイル名・変更内容ともに正確に反映されている。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている技術用語(HTMLエスケープ、DEFAULT_OPTIONS、@options など)は PR の記述と合致しており、誤用はない。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的な主張は PR の説明・Diff と整合し、因果関係も論理的に正しい。パフォーマンスへの影響がない旨も妥当な推測であり、誤りではない。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の全ての事実は PR のタイトル・説明・Diff で裏付けられており、根拠のない推測や外部情報は含まれていない。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR 番号 #57991 が正確に記載されている。その他数値や固有名詞の誤りはない。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルは PR の内容(Coders::JSON の HTML エスケープ抑制)を正確に日本語で表現している。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事は PR に記載された情報以外のバージョンサポート状況やリリース日程等の外部知識を付加していない。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の記述はなく、PR の記述と矛盾する表現も見られない。