概要
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を応用したハッシュ化は十分注意して使う