TL;DR
文字列キー vs シンボルキー、そしてtrue
(真偽値) vs "true"(文字列) の不一致が原因で、variants
がフィルタ段階で全落ちしていた。with_indifferent_access
(またはdeep_symbolize_keys
)と、確実な真偽値変換(ActiveModel::Type::Boolean.new.cast
など)で修正。
もちろん、文字列キーかシンボルキーどちらかに統一すべきだし、したほうが良いというのは大前提ですが...
背景
- サーバー側で受け取るパラメータ(JSON)には、
"is_selected" => true
のように 文字列キー と 真偽値(boolean) が混在している。 - これを Ruby/Rails で処理する際、以下のようなコードがあった:
variants.select do |variant|
variant[:is_selected].presence == "true"
end
- 結果、期待していた
variants
が空配列[]
になり、後続処理で 422 エラー(Unprocessable Entity)などが発生。
問題の症状
- 入力:
"variants"=>[{"id"=>1, "is_selected"=>true}, ...]
- 出力(整形後):
"variants"=>[]
- ログを追うと、
group_info[:variants]
などがすべて空になっている。 - そのため、グループ内に 1 つも選択されたバリアントが無い扱いとなり、バリデーションで弾かれるなどのエラーにつながっている。
根本原因(Root Cause)
1. キーの種類の違い
- Ruby の Hash は 文字列キー と シンボルキー を区別する。
- 受け取ったパラメータは
{"is_selected"=>true}
(文字列キー)だが、コードはvariant[:is_selected]
(シンボルキー)で参照していたためnil
。
2. 型の違い(boolean vs string)
- たとえ取り出せても、条件が
== "true"
では boolean のtrue
は一致しない。 -
true.presence
はtrue
のまま返るので、true == "true"
はfalse
。
3. presence の誤用
-
presence
は空文字や nil を除去するためのヘルパー。boolean 判定に使っても意味が薄かった。
修正方針
A. キー不一致を吸収する(Indifferent Access)
-
with_indifferent_access
やdeep_symbolize_keys
を使って、キーアクセスを統一する。 - 配列内の各要素(Hash)にも適用する必要がある。
v = v.with_indifferent_access
v[:is_selected] # これで "is_selected" も :is_selected も同じように取れる
B. 真偽値を正しくキャストする
- Rails 5+ なら
ActiveModel::Type::Boolean.new.cast(val)
- Rails 4.2 でも
ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(val)
を使える
def truthy?(val)
ActiveModel::Type::Boolean.new.cast(val) # Rails 5+
end
C. 比較ロジックを見直す
-
== "true"
など文字列比較をやめ、統一的な truthy 判定関数を通す。
修正後コード例
# before
def variants_infos(variants)
variants.select { |variant| variant[:is_selected].presence == "true" }
.map { |v| { id: v[:id].to_i, quantity: v[:selected_count].to_i } }
end
# after (Rails 7 なら)
def variants_infos(raw_variants)
Array(raw_variants)
.map { |v| v.with_indifferent_access }
.select { |v| ActiveModel::Type::Boolean.new.cast(v[:is_selected]) }
.map { |v| { id: v[:id].to_i, quantity: v[:selected_count].to_i } }
end
Rails 4.2 でも同じ発想で
TRUE_VALUES
を使えばOK。
# Rails 4.2 用の truthy? 実装
TRUE_VALUES = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES
def truthy?(val)
TRUE_VALUES.include?(val) || val == true
end
既存コードとの違いまとめ
観点 | 旧コード | 新コード |
---|---|---|
キーアクセス |
variant[:is_selected] 固定 |
with_indifferent_access で吸収 |
真偽判定 | presence == "true" |
Boolean.cast / truthy? で一元化 |
透過範囲 | 最上位のみ | 配列要素を含め再帰的 or 明示的に処理 |
可読性/保守性 | 暗黙的・依存度高 | 明示的・型安全・テスト容易 |