1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ruby で文字列キーとシンボルキーを同列で扱えるようにする

Posted at

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.presencetrue のまま返るので、true == "true"false

3. presence の誤用

  • presence は空文字や nil を除去するためのヘルパー。boolean 判定に使っても意味が薄かった。

修正方針

A. キー不一致を吸収する(Indifferent Access)

  • with_indifferent_accessdeep_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 明示的に処理
可読性/保守性 暗黙的・依存度高 明示的・型安全・テスト容易
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?