LoginSignup
2
1

More than 1 year has passed since last update.

【Rails】strong parametersからHashを生成する方法のまとめ

Last updated at Posted at 2021-09-04

概要

Strong parameterをハッシュ化する方法と注意点のまとめです。
Railsのソースコードを引用しながら説明します。

許可されているパラメータのみハッシュ化したい

to_hを使う

to_hを使えば、許可されたパラメータ(key)のみのハッシュ(HashWithIndifferentAccess)を取得する事ができます。メソッドの定義は以下のようになっており、permitted?がtrueで無ければUnfilteredParametersをraiseします。 このメソッドを使う方法ではpermitメソッドを予め実行しておく必要があるため、取得できるのは許可されたパラメータのみのハッシュという事になります。

# strong_parameter.rbのto_hメソッド
def to_h
  if permitted?
    convert_parameters_to_hashes(@parameters, :to_h)
  else
    raise UnfilteredParameters
  end
end

以下の実行サンプルの通り、to_hメソッドは、許可してないkeyが含まれる場合は例外となり、許可したkeyにみハッシュとして取得する事ができます。

# permitを実行してないと例外となりハッシュは取得できない
irb(main):445:0> ActionController::Parameters.new(name: 'Bob').to_h
Traceback (most recent call last):
        1: from (irb):442
ActionController::UnfilteredParameters (unable to convert unpermitted parameters to hash)

# 取得できるクラスは、ActiveSupport::HashWithIndifferentAccess
irb(main):441:0> ActionController::Parameters.new(name: 'Bob').permit(:name).to_h.class
=> ActiveSupport::HashWithIndifferentAccess

# 許可したkeyのみハッシュとして取得できる
irb(main):451:0> ActionController::Parameters.new(name: 'Bob', status: 'busy').permit(:name).to_h
=> {"name"=>"Bob"}

to_hを使うためにpermitを予め実行しておく必要がありますが、このpermitメソッドの内部処理について理解できて無い場合は、permitを実行していてもUnfilteredParameters例外で引っかかる場合があるので補足説明します。こちらは、permitメソッドの処理ですが、1行目で自身のクラスインスタンスをnewして代入しています。これによって元のparamsが持つ@permittedでは無くコピー後の新しく生成した方に変更が加えられるという事になります。

def permit(*filters)
  params = self.class.new #コピーを作成

  filters.flatten.each do |filter|
    case filter
    when Symbol, String
      permitted_scalar_filter(params, filter)
    when Hash
      hash_filter(params, filter)
    end
  end

  unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters
  params.permit!
end

従って、以下の例の通りpermitの返り値であるcopied_paramsはto_hが成功しますが、初期化したparamsはpermited:falseのままになっているのでto_hは失敗します。どのインスタンスに対してto_hをしているのかを注意する必要があります。

irb(main):416:0> params = ActionController::Parameters.new(name: 'Bob', status:"busy")
irb(main):417:0> copied_params = params.permit(:name)

# 元のparamsは変更されないため、permitした後でもUnfilteredParametersになる
irb(main):423:0> params
=> <ActionController::Parameters {"name"=>"Bob", "status"=>"busy"} permitted: false>
irb(main):419:0> params.to_h  
ActionController::UnfilteredParameters (unable to convert unpermitted parameters to hash)

# 新しく作ったcopied_paramsはpermit処理されているので、目的のハッシュが得られる
irb(main):425:0> copied_params
=> <ActionController::Parameters {"name"=>"Bob"} permitted: true>
irb(main):420:0> copied_params.to_h
=> {"name"=>"Bob"}

to_hashを使う

to_hashの定義は以下の通りです。先ほど説明したstrong_parmaeter.rbで定義されているto_hを呼んでHashWithIndifferentAccessを取得してからto_hashでHashオブジェクトを取得します。HashWithIndifferentAccessの特徴は、keyがシンボルでも文字列でもアクセス可能である事ですが、こちらで取得できるのはHashなので文字列をキーにしたアクセスが前提になります。 単純なエイリアスでは無いという事に注意が必要です。

# strong_parameter.rb のto_hashメソッド
def to_hash
  to_h.to_hash
end
# hash_with_indifferent_access.rbのto_hashメソッド

# Convert to a regular hash with string keys.
def to_hash
  _new_hash = Hash.new
  set_defaults(_new_hash)

  each do |key, value|
    _new_hash[key] = convert_value(value, for: :to_hash)
  end
  _new_hash
end

to_hの処理と同様にpermitしてない場合は例外となり、許可されたkeyでのみハッシュが取得できます。

# permitしてない場合はUnfilteredParameters例外がraiseされる
irb(main):461:0> ActionController::Parameters.new(name: 'Bob', status: 'busy').to_hash.class
Traceback (most recent call last):
        1: from (irb):458
ActionController::UnfilteredParameters (unable to convert unpermitted parameters to hash)

# 取得されるのはHashクラス
irb(main):457:0> ActionController::Parameters.new(name: 'Bob', status:'busy').permit(:name).to_hash.class
=> Hash

# 許可されたkeyでハッシュが取得できる
irb(main):456:0> ActionController::Parameters.new(name: 'Bob', status: 'busy').permit(:name).to_hash
=> {"name"=>"Bob"}

許可されていないパラメータも含めてハッシュ化したい

to_unsafe_h, to_unsafe_hashを使う

こちらのメソッドを使用すれば、許可されていないパラメータも含めてハッシュを取得する事ができます。to_hとは異なり、ここではpermitted?で条件分岐するような処理は含まれていません。

to_hashとto_hの関係のように、to_unsafe_hashはto_unsafe_h(HashWithIndifferentAccess)からHashを取得するような処理になっていると思いきやこちらは単純なエイリアスです。どうしてこうなったのかわかりませんが注意が必要かもしれません。

# Returns an unsafe, unfiltered
# <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of the
# parameters.
#
#   params = ActionController::Parameters.new({
#     name: "Senjougahara Hitagi",
#     oddity: "Heavy stone crab"
#   })
#   params.to_unsafe_h
#   # => {"name"=>"Senjougahara Hitagi", "oddity" => "Heavy stone crab"}
def to_unsafe_h
  convert_parameters_to_hashes(@parameters, :to_unsafe_h)
end
alias_method :to_unsafe_hash, :to_unsafe_h

permit!を使う

permit!メソッドを使用すればあらゆるkeyに対して許可した事にできるので、to_hでもUnfilteredParametersエラーにならずに目的の結果が得られます。

irb(main):366:0> params = ActionController::Parameters.new(name: 'Bob', status:"busy")
irb(main):367:0> params.to_h
Traceback (most recent call last):
        1: from (irb):367
ActionController::UnfilteredParameters (unable to convert unpermitted parameters to hash)
irb(main):368:0> params.permit!.to_h
=> {"name"=>"Bob", "status"=>"busy"}

以下のドキュメントにもあるように使用には十分注意する必要があります。

こうすることで、:log_entryパラメータハッシュとすべてのサブハッシュが「許可済み(permitted)」としてマーキングされ、許可済みスカラーであるかどうかがチェックされなくなってあらゆる値を受け付けるようになります。ただし、permit!はくれぐれも慎重にお使いください。現在のモデルの属性はもちろん、将来モデルに追加される属性も一括で許可してしまうためです。

モデルを生成する前など、マスアサインメントを防ぎたいタイミングでpermitを実行しておけば、既に許可されたkeyがpermit!によってpermitted=trueになっていたとしてもそれを弾く事ができます。

また、permit!はpermitと違って内部でインスタンスがコピーされないので、例えば以下のようなシナリオなどが起こってしまいます。

irb(main):565:0> params = ActionController::Parameters.new(name: 'Bob', status:"busy", unpermitted_key:"evil value")
irb(main):568:0> params.permit!.to_h
=> {"name"=>"Bob", "status"=>"busy", "unpermitted_key"=>"evil value"}
irb(main):569:0> params
=> <ActionController::Parameters {"name"=>"Bob", "status"=>"busy", "unpermitted_key"=>"evil value"} permitted: true>

# permitでparamsに許可を与える(permitの仕様を理解しておらず許可したつもりになる)
irb(main):570:0> params.permit(:name, :status)
=> <ActionController::Parameters {"name"=>"Bob", "status"=>"busy"} permitted: true>

# ここでモデルを生成すると、許可したくないキーが使えてしまう
irb(main):577:0> User.new(params)
=> <ActionController::Parameters {"name"=>"Bob", "status"=>"busy", "unpermitted_key"=>"evil value"} permitted: true>

これがもしpermit!を実行していなければ、UnfilteredParametersが発生するはずなので、開発時に気がづく事ができます。

irb(main):658:0> params = ActionController::Parameters.new(name: 'Bob', status:"busy", unpermitted_key:"evil value")
irb(main):659:0> params.permit(:name, :status)
=> <ActionController::Parameters {"name"=>"Bob", "status"=>"busy"} permitted: true>
irb(main):660:0> User.new(params)
<ActionController::Parameters {"name"=>"Bob", "status"=>"busy", "unpermitted_key"=>"evil value"} permitted: false>
Traceback (most recent call last):
        1: from (irb):660
ActiveModel::ForbiddenAttributesError (ActiveModel::ForbiddenAttributesError)

to_unsafe_h, to_unsafe_hashであれば元のインスタンスのpermittedを書き換える事なく許可してないキーを含めたハッシュが取得できるので、敢えてこのやり方を使用する必要は無いんじゃ無いかなと思います。

permit_all_parametersを使う

ActionController::Parameters.permit_all_parameters = trueと設定する事で、strong parameterを初期化した段階で全てのkeyを許可する事ができます。 permit!と場合と同様にto_hと組み合わせれば許可して無いkeyも含めてハッシュを取得する事ができます。permit_all_parametersはクラスの値を直接書き換えているので、これを設定した後に作成したインスタンスも全て許可されます。

irb(main):604:0> ActionController::Parameters.permit_all_parameters = true
irb(main):609:0> params = ActionController::Parameters.new(name: 'Bob', status:"busy")
irb(main):610:0> params
=> <ActionController::Parameters {"name"=>"Bob", "status"=>"busy"} permitted: true>
irb(main):611:0> params.to_h
=> {"name"=>"Bob", "status"=>"busy"}
irb(main):616:0> params2 = ActionController::Parameters.new(name: 'Bob', status:"busy")
irb(main):617:0> params
=> <ActionController::Parameters {"name"=>"Bob", "status"=>"busy"} permitted: true>
irb(main):618:0> params.to_h
=> {"name"=>"Bob", "status"=>"busy"}

permit.keysを使う

以下のようにpermitパラメータに全てのkeysを許可すれば、ハッシュ化する事ができます。permit!の場合は、入れ子構造の場合でも再起的に呼び出してpermittedを処理するので、以下のようにネストしたハッシュが取得できますが、permitはそうでは無いという事に注意してください。

irb(main):643:0> params = ActionController::Parameters.new(name: 'Bob', status:"busy", profile: {age: 100})
irb(main):644:0> params.keys
=> ["name", "status", "profile"]
irb(main):645:0> params.permit(params.keys).to_h
=> {"name"=>"Bob", "status"=>"busy"}

irb(main):648:0> params.permit!.to_h
=> {"name"=>"Bob", "status"=>"busy", "profile"=>{"age"=>100}}

まとめ

許可したkeyのみのハッシュが欲しい場合

to_h, to_hashを使う

許可してないkeyを含めてハッシュが欲しい場合

to_unsafe_h, to_unsafe_hashを使う
permit!やpermitを応用したハッシュ化は十分注意して使う

2
1
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
2
1