Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails 8: strong parametersにexpectと二重配列構文が追加された

こんにちは、hachi8833です。Rails 8で追加されたStrong Parametersのexpectと二重配列構文について自分でもまとめてみました。

🔗 概要

Rails 8では、以下のプルリクでParameters#expectメソッドが新しく追加されました。

これにより、従来はActionController::Parametersrequirepermitの2本立てで記述していたstrong parametersを、expectでまとめて書けるようになります。

# 従来
params.require(:user).permit(:name, :age)

# Rails 8: 上は以下と同等
params.expect(user: [:name, :age])

expectメソッドの追加と同時に、独自の[[:属性名]]という二重配列記法も新たに導入されました。

また、expectに加えてexpect!メソッドも追加されています。
expectで発生するエラーはhandled errorとして扱われるので"400 Bad request"ページが表示されますが、expect!で発生するエラーは、unhandled errorを発生して"500 Internal Server Error"ページが表示されます。

参考: 500 Internal Server Error - HTTP | MDN
参考: 400 Bad Request - HTTP | MDN

また、この改修に伴って、Railsガイドのstrong parametersに関する記述もRails 8でかなり更新されました↓。

参考: §5 Strong Parameters -- Action Controller の概要 - Railsガイド

🔗 1: expectを使うとpermitrequireよりもシンプルに書ける

従来のstrong parametersでは、省略不可の必須キーをrequireで指定し、許可したい属性をpermitで指定する方法が使われていました。

# 従来のpermit+requireによる書き方
params.require(:person).permit(:name, :age)

Rails 8.0のexpectを使うと、上と同等のことを以下のように簡潔に書けます。

# expectで書く場合
params.expect(person: [:name, :age])

上の(:name, :age)[:name, :age]は配列を返すという意味ではなく、{ name: "John Doe", age: 42 }のような、スカラー値を持つハッシュを返すべきであることを意味しています。

なお、従来のpermitrequireによるStrong Parametersでネストするときの書き方については、以下の記事が参考になります。

参考: ネストするStrong Parametersの書きかた #Rails - Qiita

🔗 2: paramsの構造も詳細に強制できるようになった

expectと二重配列記法[[:属性名]](後述)が導入されたことで、strong parametersで以下のような複雑にネストした属性でハッシュと配列を区別して指定することも可能になりました。詳しくは後述の二重配列記法を参照してください。

# Railsガイド「Action Controller の概要」より
# 期待されるパラメータの例:
params = ActionController::Parameters.new(
  name: "Martin",
  emails: ["me@example.com"],
  friends: [
    { name: "André", family: { name: "RubyGems" }, hobbies: ["keyboards", "card games"] },
    { name: "Kewe", family: { name: "Baroness" }, hobbies: ["video games"] },
  ]
)

# パラメータは以下のexpectによって許可済みであることが保証される:
name, emails, friends = params.expect(
  :name,                 # 許可済みのスカラー値
  emails: [],            # 許可済みのスカラー値の配列
  friends: [[            # 許可済みのParameterハッシュの配列
    :name,               # 許可済みのスカラー値
    family: [:name],     # family: { name: "許可済みのスカラー値" }
    hobbies: []          # 許可済みのスカラー値の配列
  ]]
)

🔗 3: expectは500エラーを削減できる

従来のRailsにおけるrequirepermitは、以下のようにrequirepermitの順にチェインすると、たとえばパラメータにハッシュではなくスカラー値が渡されたときにNoMethodError、つまり"500 Internal Server Error"ページが表示されました。これは嬉しくありませんね。

Parameters.new(user: "hax").require(:user).permit(:name, :email)
#=> undefined method `permit' for an instance of String (NoMethodError)

従来のRailsでも、たとえば以下のようにpermitrequireの順にチェインすることで、ParameterMissingにすることは「一応」可能です(デフォルトでは"400 Bad Request"を表示します)。

Parameters.new(user: "hax").permit(:name, :email).require(:user)
#=> param is missing or the value is empty or invalid: user (ActionController::ParameterMissing)

しかし新しいexpectなら、上のような工夫をしなくても、デフォルトで後者のParameterMissingになります。"500 Internal Server Error"が必要ならexpect!を使えば済みます。

Parameters.new(user: "hax").expect(user: [:name, :email])
#=> param is missing or the value is empty or invalid: user (ActionController::ParameterMissing)

詳しくは以下の記事をご覧ください。

Rails 8: strong parametersの新しいparams.expectの使い方(翻訳)

🔗 4: 新しい二重配列記法[[:属性名]]

🔗 従来のpermitの振る舞い

従来のstrong parametersでpermit->requireの順にチェインする場合は、permitで指定する属性をpermit(user: [:name])のように書くことで、userキーの値をハッシュまたは配列のどちらかに許容する指定が可能でした。

しかし言い換えれば、この方法ではpermitで「ハッシュのみ」や「配列のみ」は指定できません。これでは、複雑にネストした属性を精密にチェックできません。

# 従来のpermit: [:name]にはハッシュを渡せる
Parameters.new(user: { name: "Martin" }).permit(user: [:name]).require(:user)
# => {"name"=>"Martin"}

# 従来のpermit: [:name]には配列も渡せる
Parameters.new(user: [{ name: "Martin" }]).permit(user: [:name]).require(:user)
# => [{"name"=>"Martin"}]

(本記事では戻り値のpermitted: falseは省略しています)

🔗 8.0のexpectでは[[:属性名]]で配列を指定できるようになった

新しいexpectでは、新登場の二重配列記法[[:属性名]]を使うと、属性を(ハッシュではなく)配列1にすることを厳密に要求できるようになります。これにより、パラメータのネスト構造に縛りをかけやすくなっています。

  • [[:属性名]]: その属性は配列でなければならない
# expectの場合: [[:name]]は配列でなければならない
Parameters.new(user: [{ name: "Martin" }]).expect(user: [[:name]])
# => {"name"=>"Martin"}

# ハッシュを渡すとエラーになる
Parameters.new(user: { name: "Martin" }).expect(user: [[:name]])
# => param is missing or the value is empty: user (ActionController::ParameterMissing)

さらに、従来の[:属性名]は、expectではその属性をハッシュにすることを厳密に要求するのに使われます。

  • [:属性名]: その属性はハッシュでなければならない
# expectの場合: [:name]はハッシュでなければならない
Parameters.new(user: { name: "Martin" }).expect(user: [:name])
# => {"name"=>"Martin"}

# 配列を渡すとエラーになる
Parameters.new(user: [{ name: "Martin" }]).expect(user: [:name])
# => param is missing or the value is empty or invalid: user (ActionController::ParameterMissing

🔗 参考: 8.0のpermitの場合

Rails 8.0のpermitで二重配列記法[[:属性名]]を試してみたところ、厳密に配列を要求するようになっていました↓。

# permitで[[:属性名]]を指定してからハッシュを渡すとエラーになる
Parameters.new(user: { name: "Martin" }).permit(user: [[:name]]).require(:user)
# => param is missing or the value is empty or invalid: user (ActionController::ParameterMissing)

# permitで[[:属性名]]を指定してから配列を渡すのはOK
Parameters.new(user: [{ name: "Martin" }]).permit(user: [[:name]]).require(:user)
# => [{"name"=>"Martin"}]

また、後方互換性のため、permit[:属性名]を指定した場合は、従来通りハッシュと配列のどちらを渡しても許容されます↓。

# 以下はどちらも許容される
# permitで[:属性名]にハッシュを渡した場合
Parameters.new(user: { name: "Martin" }).permit(user: [:name]).require(:user)
# => {"name"=>"Martin"}

# permitで[:属性名]に配列を渡した場合
Parameters.new(user: [{ name: "Martin" }]).permit(user: [:name]).require(:user)
# => [{"name"=>"Martin"}]

参考までに、Rails 7.2以下でpermitに二重配列記法[[:属性名]]を書いてみたところ、エラーにならず、通常の[:属性名]と同じ振る舞いになります。

# Rails 7.2での実行結果:
Parameters.new(user: { name: "Martin" }).permit(user: [:name]).require(:user)
#=> {"name"=>"Martin"}

Parameters.new(user: { name: "Martin" }).permit(user: [[:name]]).require(:user)
#=> {"name"=>"Martin"}

Parameters.new(user: [{ name: "Martin" }]).permit(user: [:name]).require(:user)
#=> [{"name"=>"Martin"}]

Parameters.new(user: [{ name: "Martin" }]).permit(user: [[:name]]).require(:user)
#=> [{"name"=>"Martin"}]

🔗 今後はどちらを使えばよいか?

上述したように、Rails 8のガイドではサンプルコードのrequirepermitexpectに変更されています。
expectのAPIドキュメント↓にも、従来のrequirepermitによる方式よりも好ましいと書かれています。

Rails 8 API: ActionController::Parameters(翻訳)

一方、従来のrequirepermitを廃止するような動きは今のところ特になさそうです。たぶん今後もしないのではと予想しています。

現在動いているrequirepermitの置き換えをそれほど急ぐ必要はないかもしれませんが、この機能を実装したMartin Emdeさんの記事では「オプショナルなパラメータでない限り、基本的にexpectに置き換える方がよい」とのことです↓。置き換えることで、不要な500エラーが減ることも期待できます。

Rails 8: strong parametersの新しいparams.expectの使い方(翻訳)

また、以下の動きを見ても、今後はexpectを推す方向に向かっているようです。

  • Rails 8のscaffoldで生成されるコントローラでも、requirepermitの代わりにデフォルトでexpectが使われるようになりました。

  • Rails 8のガイドのstrong parametersでは、従来のrequirefetchを使う書き方が削除されています。また、permitは、スカラー値の指定方法の説明部分以外では削除されています。

参考: 4.6 Strong Parameters -- Action Controller の概要 - Railsガイド

なお、パラメータが空の場合のみ、scaffoldで生成されるコントローラでexpectの代わりにfetchを使うよう微修正が入りました↓

参考: Revert params.fetch to params.expect conversions in scaffold by martinemde · Pull Request #52932 · rails/rails

関連記事

Rails 8 API: ActionController::Parameters(翻訳)

保存版: Railsアプリケーションのセキュリティベストプラクティス(翻訳)


  1. 厳密には[{ name: "Martin" }]のような「ハッシュの配列」ですが、本記事では煩雑さを避けるため「配列」と略記します。 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。