✍️

Rails comparison of Date with nil failed

に公開

日付範囲の入力欄の実装でI18n対応がうまくいかず原因を調査したところ、
バリデーション時に、Rails内部でエラーが起きていることが分かりました。
どのようにI18nの修正を行なったのかメモを残します。

不具合の再現

1:ディレクトリ指定でGemインストール

bundle config --local path vendor/bundle

2:スキャフォールド実施

bundle exec rails g scaffold Sample started_on:date ended_on:date

3:バリデーションの追加

class Sample < ApplicationRecord
  validates :ended_on, comparison: { greater_than: :started_on },
                       allow_blank: true
end

4:migate up

bundle exec rails db:migrate:up VERSION=XXXXX

started_onよりもended_onの値を小さくして保存を行う。
「started_onは、ended_onよりも大きい」に近いメッセージが表示されることを期待していたが、実際は「Ended on comparison of Date with nil failed」と表示された。

5:rails console

record = Sample.new(ended_on: Time.current)
record.valid? # => false
record.errors.full_messages # => ["Ended on comparison of Date with nil failed"]

原因を調べた流れ

full_messageを調べることにした。

full_messagesメソッドの位置を調べた。
activemodel-7.0.8.7/lib/active_model/error.rb:160

messageメソッドの位置を調べた。
activemodel-7.0.8.7/lib/active_model/error.rb:131

raw_typeメソッドの位置を調べた。
activemodel-7.0.8.7/lib/active_model/error.rb:126

コメントを元にerrors.addメソッドを調べることにした。

addメソッドの位置を調べた。
activemodel-7.0.8.7/lib/active_model/errors.rb:317

addメソッドの第二引数にtypeとしてcomparison of Date with nil failedが渡ってきている。
callerを読んで呼び出し元を調べた。
activemodel-7.0.8.7/lib/active_model/validations/comparison.rb:29

validate_eachメソッドの中身を読んだ。
Dateのインスタンス.public_send(:>, nil)を呼び出しraiseしていた。
それをrescueで拾っていた。

comparisonオプションの引数を属性にする場合、
ifオプションで比較対象が存在する時に絞らないとI18nが動作しないことが分かった。

修正して動作確認した

バリデーションを修正

validates :ended_on, comparison: { greater_than: :started_on, if: -> { started_on? } }, allow_blank: true

rails console

record = Sample.new(ended_on: Time.current)
record.valid? # => true
record = Sample.new(started_on: Time.current.tomorrow, ended_on: Time.current)
record.valid? # => false
record.errors.full_messages # => ["Ended on は2025-11-23より大きくしてください"]

gemの変更を取り消した

bundle pristine model
bundle pristine i18n

Discussion