Coders::JSONでHTMLエスケープが無効化されたJSONシリアライズ
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 引数が直接エンコーダに渡され、デフォルトオプションが無視されていました。 修正によりエンコーダは @options(DEFAULT_OPTIONS とユーザ指定のマージ結果)を受け取り、設計どおりの振る舞いになります。 変更行は 1 行で、他のロジックに影響を与えない純粋なデータフローの修正です。
結果として、Rails の JSON 永続化における一貫性が回復し、開発者は追加設定や回避策を意識せずに済むようになりました。 この変更は、フレームワーク内部のオプション伝播の原則を遵守した例として評価できます。 現在の実装では、@options がエンコーダに正しく伝播することが保証されています。
まとめ
このPRは、ActiveRecord::Coders::JSON が HTML エスケープを無視していたバグを、エンコーダに正しいオプション @options を渡す修正を行いました。 変更はコード一行の差し替えとテスト追加に留まり、既存 API への影響はありません。 その結果、JSON カラムへの永続化がネイティブ実装と完全に一致し、データの整合性が確保されます。