【トリビア】Railsのコントローラに出てくるparamsはハッシュじゃない

## はじめに
Railsに関するトリビア的なネタです。

QiitaやZennを見ていると、ときどきRailsのコントローラに出てくるparamsをハッシュ（Hashオブジェクト）だと説明している記事を見かけます。

**しかし、paramsはハッシュではありません。**

## 確かめてみよう

こんな感じでparamsの中身をputsしてみましょう。

```ruby 
def show
  puts params
end
```

うん、とてもハッシュっぽいですね。

```
{"id"=>"1", "controller"=>"projects", "action"=>"show"}
```

では、こうするとどうでしょうか？
paramsがハッシュならどちらもtrueになるはずです。

```ruby 
def show
  # paramsがハッシュそのものならtrue
  puts params.instance_of?(Hash)
  # paramsがHashクラスを継承していればtrue
  puts params.is_a?(Hash)
end
```

結果はこうなりました。予想に反してどちらもfalseですね 🤔

```
false
false
```

## paramsの正体は ActionController::Parameters

ではparamsは何者なんでしょうか？
クラス名を確認してみましょう。

```ruby 
def show
  puts params.class
end
```

すると、以下のような出力になりました。

```
ActionController::Parameters
```

そうなんです、paramsはハッシュではなく ActionController::Parameters クラスのインスタンスなんです。
このクラスはHashクラスを継承したりしていないので、オブジェクト指向プログラミングでいうところのis-aの関係が成り立ちません。

念のため、 ActionController::Parameters クラスの継承関係を確認してみましょう。

```ruby 
def show
  # ActionController::Parametersの継承関係を確認する
  puts params.class.ancestors
end
```

ご覧のとおり、Hashがどこにも含まれていないことがわかります。

```
ActionController::Parameters
ActiveSupport::DeepMergeable
ActiveSupport::Dependencies::RequireDependency
ActiveSupport::ToJsonWithActiveSupportEncoder
Object
PP::ObjectMixin
ActiveSupport::Tryable
JSON::Ext::Generator::GeneratorMethods::Object
DEBUGGER__::TrapInterceptor
Kernel
BasicObject
```

https://api.rubyonrails.org/classes/ActionController/Parameters.html

## paramsは変数でもない

さらにいうと、paramsは変数でもありません。
ふだん滅多に意識することはないかもしれませんが、paramsはメソッドです。

その証拠に、`()`を付けたり`self.`を付けて呼び出すことができます。

```ruby 
def show
  # メソッドなので、()を付けたりself.を付けて呼び出せる
  puts params()
  puts self.params
end
```

```
{"id"=>"1", "controller"=>"projects", "action"=>"show"}
{"id"=>"1", "controller"=>"projects", "action"=>"show"}
```

paramsは ActionController::Base クラスに（厳密にはそのクラスがincludeしている ActionController::StrongParameters モジュールに）定義されているメソッドです。

https://api.rubyonrails.org/classes/ActionController/StrongParameters.html#method-i-params

ためしに以下のようなコードを書いて確認してみましょう。

```ruby 
def show
  # paramsメソッドの情報を確認する
  puts method(:params)
end
```

実行結果を見るとparamsがコントローラ内でメソッドで定義されたメソッドであることがわかります。

```
#<Method: ProjectsController(ActionController::StrongParameters)#params() /Users/jnito/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/actionpack-7.1.2/lib/action_controller/metal/strong_parameters.rb:1281>
```

## メソッドなので、paramsと[]の間にスペースが入るとエラーになる

もし、paramsがローカル変数だったら、paramsと`[]`の間にスペースが入っても値を取得できます。

```ruby 
# paramsをローカル変数として定義
params = {id: 123}

# paramsがローカル変数ならスペースが挟まれてもOK（ふつうはしませんが）
params [:id]
#=> 123
```

しかし、Railsのparamsはメソッドなので、スペースが入るとエラーになります。

```ruby 
def show
  # [ の手前にスペースが入るとエラー
  params [:id]
end
```

```
ArgumentError: wrong number of arguments (given 1, expected 0)
```

これは`[:id]`がparamsメソッドの引数と見なされるためです。

```ruby 
# params [:id] は以下のようなコードを書いたのと同じ
params([:id])
```

paramsメソッドは引数を受け取らないメソッドなので、引数の個数が合わず(given 1, expected 0)、エラーになります。

よってRailsのparamsは必ずスペースなしで`[]`を呼び出さなければいけません。

```ruby 
def show
  # [ の手前にスペースを入れてはいけない
  params[:id]
end
```

コードを書くときに、わざわざ`params [:id]`のようにスペースを空けて呼び出すことはまずありませんが、Rails初心者さんはたまにこういううっかりミスを犯してエラーを起こすことがあるので注意してください。

## ハッシュじゃないのでシンボルでも文字列でも値を取得できる

すでに説明したとおり、Railsのparamsはハッシュではなく ActionController::Parameters クラスのインスタンスを返すメソッドです。

ハッシュではないので、 ActionController::Parameters クラスの`[]`メソッドに指定するキーはシンボルでも文字列でも同じように動作します。

```ruby 
def show
  # シンボルでキーを指定する
  params[:id]
  
  # 文字列でキーを指定してもOK
  params['id']
end
```

これは ActionController::Parameters クラスが内部的にパラメータを ActiveSupport::HashWithIndifferentAccess オブジェクトとして保持しているためです（下から4行目の `@parameters = parameters.with_indifferent_access` の部分に注目）。

```ruby 
# https://github.com/rails/rails/blob/v7.1.3/actionpack/lib/action_controller/metal/strong_parameters.rb#L268C69-L279C8
def initialize(parameters = {}, logging_context = {})
  parameters.each_key do |key|
    unless key.is_a?(String) || key.is_a?(Symbol)
      raise InvalidParameterKey, "all keys must be Strings or Symbols, got: #{key.class}"
    end
  end

  @parameters = parameters.with_indifferent_access
  @logging_context = logging_context
  @permitted = self.class.permit_all_parameters
end
```

ちなみに ActiveSupport::HashWithIndifferentAccess クラスはHashクラスを拡張してシンボルのキーと文字列のキーを同列に扱うようにしたRailsの（正確にはActiveSupportの）クラスです。
詳しくは以下のAPIドキュメントを参照してください。

https://api.rubyonrails.org/classes/ActiveSupport/HashWithIndifferentAccess.html

### 参考：Sinatraのparamsも同様にシンボルでも文字列でも値を取得できる

Ruby製のwebフレームワークであるSinatraでも`params`はメソッドで、その値はシンボルでも文字列でも取得できるようになっています。

```ruby 
get '/' do
  # params[:name]でもparams['name']でもOK
  "Hello, #{params[:name]}!"
end
```

なお、Sinatraの場合はSinatra::IndifferentHashという独自のクラスを使って実装されているようです。

https://rubydoc.info/gems/sinatra/Sinatra/IndifferentHash

## そもそもなんでparamsはハッシュじゃないの？

paramsが単純なハッシュではなく ActionController::Parameters オブジェクトを返す理由はなぜでしょうか？
それはマスアサインメント脆弱性を防止するためのStrong Parametersという仕組みを実現するためです。
ひとことで言えば、Railsが標準で用意しているセキュリティ対策ですね。

> strong parametersは、Action ControllerのパラメータをActive Modelの「マスアサインメント」で利用することを禁止します（許可されたパラメータは除く）。したがって、開発者は、マスアップデートを許可する属性をコントローラで明示的に指定しなければなりません。strong parametersは、ユーザーがモデルの重要な属性を誤って更新してしまうことを防止するための、より優れたセキュリティ対策です。
> 
> https://railsguides.jp/action_controller_overview.html#strong-parameters

「Strong Parametersって何？初めて聞いたんですけど！」という方はRailsガイドを参照してください。

https://railsguides.jp/action_controller_overview.html#strong-parameters

## まとめ

この記事で説明した内容は以下の通りです。

- Railsのコントローラに出てくるparamsはハッシュではなく ActionController::Parameters オブジェクト
- paramsで参照しているのは変数ではなくメソッド
- ハッシュではないのでparamsから値を取り出すときはシンボルでも文字列でもOK
- paramsがハッシュになってないのはマスアサインメント脆弱性を防止するため

実際問題、Railsのコードを書くときにparamsがハッシュかそうでないかを意識する機会はそこまで多くありません。
しかし、「paramsはハッシュではない」ということを知っておくと、特殊な要件に対処したり、ややこしいエラーをデバッグしたりするときに役立つことがたまにあります。

また、他の人にparamsの役割を説明したりするときに「paramsはハッシュなんだよ」と教えてしまうと、微妙に間違った情報を伝えてしまうことになります。
そうではなく、「paramsはハッシュによく似たオブジェクトです」とか「paramsはハッシュじゃないけど、最初のうちはとりあえずハッシュと同じようなものと考えても良い」といった説明の方がベターですね。

もしみなさんの周りは「paramsはハッシュだ」と勘違いしている人がいたら、この記事をそっと差し出してみてください。

### おまけ
この記事と同じテーマでChatGPTに記事を書かせてみました。

https://blog.jnito.com/entry/2024/01/17/115546

## 追記：Railsガイドの説明は・・・「paramsという名前のハッシュ」！！

本記事を書き追えて公開したあと、ふと思いました。

「そういえばRailsガイドではparamsのことをどういうふうに説明しているんだろう？」と。

調べました。

見つけました。

その結果はなんと・・・「paramsという名前のハッシュ」！！

> Railsでは、パラメータをクエリ文字列で受け取ることもPOSTデータで受け取ることもできます。いずれの場合も、コントローラ内ではparamsという名前のハッシュでパラメータにアクセスできます。
> 
> https://railsguides.jp/action_controller_overview.html

https://railsguides.jp/action_controller_overview.html

Railsガイドはparamsをハッシュだと説明していました（びっくり）。


念のため本家の英語版も確認しておきましょう。

> Rails does not make any distinction between query string parameters and POST parameters, and both are available in the params hash in your controller:
>
> https://guides.rubyonrails.org/action_controller_overview.html

https://guides.rubyonrails.org/action_controller_overview.html

やはりこちらも"hash"だと説明されていました。

なるほど、これならたしかに「paramsはハッシュです」と説明するQiita/Zennの記事が続出するのも納得がいきます。

ということは、Railsの開発チームとしては「paramsはハッシュだと思ってくれたらいい」と考えているのかもしれません。

事実ベースで技術的な解説をするなら「paramsはハッシュではない」と断言して良いと思うのですが、Rails開発チームが「params = ハッシュと考えても別にええんやで」というスタンスなのであれば、この記事の説得力がちょっと低下してしまいますね😅