[Rails] ActiveModel::Errors#added? でコールバックオプション付きのクエリが正しく動作しない問題を修正

rails/rails

背景

RailsのActiveModelでは、バリデーションエラーを追加する際に allow_nilifunless などのコールバックオプションを指定できます。しかし、これらのオプション付きでエラーを追加した後、同じオプションを指定して added? メソッドでエラーの存在を確認すると、エラーが存在するにもかかわらず false が返される不具合がありました。

この問題は #54483 で報告されており、開発者がバリデーションの動作を正確に確認できない状況を引き起こしていました。

技術的な詳細

問題の原因

不具合の原因は ActiveModel::Error#strict_match? メソッドの実装にありました。このメソッドは、保存されているエラーからコールバックオプションとメッセージオプションをフィルタリングしていましたが、クエリ側のオプションからはフィルタリングしていませんでした。

変更前:

def strict_match?(attribute, type, **options)
  return false unless match?(attribute, type)

  options == @options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS)
end

このコードでは、右辺の @options(保存されているエラーのオプション)からはコールバックオプションが除外されていますが、左辺の options(クエリのオプション)からは除外されていません。そのため、以下のような比較が行われていました:

# 実際の比較
{ allow_nil: true } == {}  # => false

修正内容

修正では、両辺のオプションから同様にコールバックオプションとメッセージオプションをフィルタリングするように変更されました。

変更後:

def strict_match?(attribute, type, **options)
  return false unless match?(attribute, type)

  options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS) == @options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS)
end

これにより、両辺が対称的に比較されるようになりました:

# 修正後の比較
{} == {}  # => true

使用例

修正後は、コールバックオプション付きのエラーチェックが正しく動作します:

# コールバックオプションのみ
errors.add(:name, :too_long, allow_nil: true)
errors.added?(:name, :too_long, allow_nil: true)  # => true(修正前: false)

# コールバックオプションと他のオプションの組み合わせ
errors.add(:name, :too_long, count: 25, allow_nil: true)
errors.added?(:name, :too_long, count: 25, allow_nil: true)  # => true
errors.added?(:name, :too_long, count: 25)  # => true(コールバックオプションは無視される)

# メッセージオプション
errors.add(:name, :too_long, message: proc { "foo" })
errors.added?(:name, :too_long, message: proc { "foo" })  # => true

影響範囲

この修正により、以下のようなシナリオで正しい動作が保証されます:

  1. 条件付きバリデーション: ifunless オプションを使用したバリデーションのテスト
  2. null許容バリデーション: allow_nilallow_blank オプションの動作確認
  3. カスタムメッセージ: Procを使用した動的メッセージの検証

コールバックオプションやメッセージオプションは、エラーの「種類」を識別するための情報ではなく、バリデーションの「実行条件」や「表示方法」に関する情報です。そのため、added? メソッドでのエラーチェックではこれらのオプションを無視することが適切な設計となります。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

ガイドライン準拠 ⚠ WARNING

記事構成とDiffDaily Styleへの準拠状況

記事構成、対象読者への適合性は完璧です。技術的な詳細も明確に記述されています。ただし、カスタムMarkdown構文において、PRリンクの記法がガイドラインの推奨形式([#123])とわずかに異なっていました([PR #123])。可読性に大きな問題はありませんが、一貫性のために修正を推奨します。

  • 記事構成(Title、Context、Technical Detail)
  • DiffDaily Styleガイド準拠
  • カスタムMarkdown活用
  • 対象読者への適合性
技術的整合性 ✓ PASS

技術的な正確性と表現の適切性

技術的に非常に正確です。記事で引用されているコード(`strict_match?`メソッドの変更前後)はPRの核心を的確に捉えています。問題の原因(非対称な比較)と解決策(対称な比較)の説明は論理的で、技術的な誤りはありません。

  • 技術用語の正確性
  • コード例の正確性
  • 説明の技術的正確性
PR内容との整合性 ✓ PASS

元のPR情報との一致度

PRの内容との整合性は完璧です。記事のタイトル、背景説明、技術的な詳細のすべてが、提供されたPR情報(Title, Number, URL)と一致しています。Issue番号(#54483)も正確に引用されており、ハルシネーションは検出されませんでした。

  • タイトル・説明の一致
  • Diff内容の正確な反映
  • 推測の排除