JSONカラムのシリアライズで二重エンコードを防止する Guard 変更

rails/rails

この PR は serialize に対する JSON コーダの互換性ガードを拡張し、coder: ActiveRecord::Coders::JSON が JSON カラムで二重エンコードされるバグを修正します。

背景

serialize で JSON カラムに coder: JSON を指定すると ColumnNotSerializableError が発生し、適切に保護されますが、同等とみなされる coder: ActiveRecord::Coders::JSON がガードをすり抜けて二重エンコードが起きていました。

二重エンコードによりデータベースには "{\"a\":1}" のような JSON 文字列が保存され、シリアライズされていないモデルは文字列を取得してデータ破損が顕在化します。

この不整合は type_incompatible_with_serialize?::JSON 定数しか比較していなかったことが根本原因です。

技術的な変更

ガードチェックに渡す引数を正規化された column_serializer に置き換え、比較対象を統一しました。

変更前:

if type_incompatible_with_serialize?(cast_type, coder, type)
  raise ColumnNotSerializableError.new(attr_name, cast_type)
end

変更後:

if type_incompatible_with_serialize?(cast_type, column_serializer, type)
  raise ColumnNotSerializableError.new(attr_name, cast_type)
end

build_column_serializer では JSON コーダを Coders::JSON に正規化しているため、column_serializer は常に同一形式になります。正規化に合わせてガードが同じオブジェクトを受け取るようになりました。

type_incompatible_with_serialize? の条件式を拡張し、Coders::JSON === coder を含めることで ActiveRecord::Coders::JSON も検出できるようになりました。

def type_incompatible_with_serialize?(cast_type, coder, type)
  cast_type.is_a?(ActiveRecord::Type::Json) && Coders::JSON === coder ||
    cast_type.respond_to?(:type_cast_array, true) && type == ::Array
end

追加されたテスト test_not_compatible_with_serialize_active_record_coders_jsonColumnNotSerializableError を期待通りに発生させることを確認しています。

def test_not_compatible_with_serialize_active_record_coders_json
  new_klass = Class.new(klass) do
    serialize :payload, coder: ActiveRecord::Coders::JSON
  end
  assert_raises(ActiveRecord::AttributeMethods::Serialization::ColumnNotSerializableError) do
    new_klass.new
  end
end

設計判断

既存キー coder の正規化ロジックに合わせてガードを拡張する方針が採られ、API の破壊的変更は回避されました。

条件式に === を使用したケース比較により、将来的に Coders::JSON を継承したクラスが追加されても同様に判定できる拡張性が確保されています。

変更箇所は数行のロジック追加とメソッド呼び出しの差し替えに留まり、既存コードベースへの侵入リスクは最小限です。

まとめ

本 PR は serialize の互換性ガードを正規化 coder に合わせて拡張し、coder: ActiveRecord::Coders::JSON が JSON カラムで二重エンコードするバグを根本的に解消しました。データ破損リスクが除去され、Rails のシリアライズ機構の整合性が向上します。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
3cac84ee

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文が要旨を示し、背景・技術的変更・設計判断・まとめの各セクションが揃っている。まとめは単なる繰り返しでなく、修正の意義を再提示している。

カスタムMarkdown構文 ✓ PASS

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

コードブロックは ``ruby:ファイルパス`` 形式で正しく記述され、PRリンクは [#番号](URL) 形式で正しくハイパーリンク化されている。

対象読者への適合性 ✓ PASS

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

Rails の内部実装に焦点を当てた記述で、専門的なエンジニア向けの内容になっている。初心者向けの余分な説明はない。

パラグラフ・ライティング ✓ PASS

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

各セクションは総論→各論→結論の流れで構成され、段落は1文または数文で完結し、トピックセンテンスが冒頭にある。1段落1トピックが守られ、段落間は空行で区切られている。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは Diff の変更点と完全に一致している。変更前/変更後の差分、メソッド修正、テスト追加すべてが正しく反映されている。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

ActiveRecord::Coders::JSON、column_serializer、type_incompatible_with_serialize? などの用語が正確に使用され、PR の記述と一致している。

説明の技術的正確性 ✓ PASS

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

二重エンコードの原因と修正方法が Diff と PR の説明に基づき正しく述べられている。技術的な主張に根拠がある。

事実の突合 ✓ PASS

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

全ての主張は PR のタイトル・説明・Diff で裏付けられている。外部知識の付加はなく、推測や捏造は見られない。

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

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

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

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

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

記事タイトルは日本語で PR の内容(二重エンコード防止)を正確に表現しており、内容と一致している。

外部知識の正確性 ✓ PASS

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

バージョンサポート状況やリリース日程等、PR に記載されていない外部知識は含まれていない。

時間表現の正確性 ✓ PASS

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

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