Rails: controller層でパラメータをrequireに設定すると空配列が弾かれてしまう

# 奮闘したこと
本記事では、Railsで作成したAPIのリクエストボディに配列をトップレベルで持った時、空配列でputリクエストを送れなくなり、詰まったのでそれを解説します。
（どうしてもトップレベルで持ちたい場合、#追記欄に最終的な着地点を記載しています。）

Railsの`require`メソッドは、特定のパラメータがリクエストに含まれていることを確認するために使用されます。もし指定されたキーが存在しない、**または値が空である場合**（例えば空の配列`[]`）、Railsは`ActionController::ParameterMissing`エラーを発生させます。これは、`require`がリクエストパラメータの存在とその値の**有効性**を保証するために設計されているためです。

## 空配列がnil扱いされる問題

リクエストボディのトップレベルに配列を用いると意図しない挙動が発生することがあります。具体的には、配列がnil扱いさてしまうのです。これが原因でエラーが発生します。以下は、具体例を用いて説明します。

### 例: リクエストボディのトップレベルに配列を使用する場合
リクエストボディは以下のような形です。
```json
{
  "tag_ids": [1, 2, 3]
}
```
そしてrailsのcontroller層は以下。
```ruby
# 存在かつ空でないことをrequire
params.require(:tags)
```

このコードでは、`:tags`キーが存在し、その値が空でないことを要求しています。しかし、リクエストボディに空の配列を含めると、Railsはその配列を無視し、`ActionController::ParameterMissing`エラーを発生させます。

```ruby
params.permit(tags: [])
```

空配列を許可する必要がある場合は、`permit`メソッドを使用して明示的に許可する必要があります。
しかし、これは以下[Railsガイド](https://railsguides.jp/action_controller_overview.html)にもあるように問題もあります。そのため、まとめでお話しするリクエストボディの変更が最善案ではないかと思います。
>「paramsの値には許可されたスカラー値の配列を使わなければならない」ことを宣言するには、以下のようにキーに空配列を対応付けます。
params.permit(id: [])
ハッシュパラメータやその内部構造の正しいキーをすべて明示的に宣言できない場合や、すべて宣言するのが面倒な場合があります。次のように空のハッシュを割り当てることは一応可能です。
params.permit(preferences: {})
ただし、この指定は任意の入力を受け付けてしまうため、利用には十分ご注意ください。この場合permitによって、受け取った構造内の値が許可済みのスカラーとして扱われ、それ以外の値がフィルタで除外されます。

## RSpecでの問題: 空文字が勝手に格納される

RSpecでテストを行う際、空の配列をリクエストボディに含めると、Railsがこの配列を空文字に変換してしまうことがあります。これにより、テストが失敗する原因となります。

### 例: RSpecでのテスト

```ruby
let!(:params) do
  {
    tag_ids: []
  }
end
```

この例では、`tag_ids`キーに空の配列を指定していますが、RSpecではこれが空文字に変換される可能性があります。

```ruby
Received parameters: #<ActionController::Parameters {"tag_ids"=>[""], "format"=>"json", "controller"=>"api/v1/articles", "action"=>"update"} permitted: false>
```

このように、`tag_ids: []`が`[""]`に変換されるため、`require`を突破してしまいます。

```ruby
context 'tag_ids=[]が指定されている場合' do
  let!(:article) { create(:article) }
  let!(:params) do
    {
      tag_ids: []
    }.to_json
  end
```

## まとめ: 正しいリクエストボディの形式

正しいリクエストボディの形式を使用することで、空配列が正しく処理されることを確認できます。
`require`はオブジェクトに対してを設定しようということかもですね。

```json
{
  "article": {
    "tag_ids": [1, 2, 3]
  }
}
```
ただ、`params.require(:article)`としても以下の形は`bad request`にならないため気が必要かと思われる。
```json
{
  article: {
    "tag_ids": nil
  }
}
```

### RSpecでの解決策

RSpecで空文字変換を避けるには、以下のようにします。

```ruby
let!(:params) do
  { article: { tag_ids: [] }}.to_json
end
```

この方法により、空配列が正しく受け取られることを確認できます。RSpecでのテストコードも含めて、適切なパラメータを使用することで、テストの信頼性を向上させることができます。

# 追記（2024/10/28）
https://ruby-jp.github.io/

上記にて、議論させていただいた結果、`permit` と `fetch` を組み合わせるとトップレベルのキーもうまく扱えることがわかりました。
```
irb(main):001> ActionController::Parameters.new({ foo: [1] }).permit(foo: []).fetch(:foo)
=> [1]
irb(main):002> ActionController::Parameters.new({ foo: [] }).permit(foo: []).fetch(:foo)
=> []
irb(main):003> ActionController::Parameters.new({ foo: nil }).permit(foo: []).fetch(:foo)
(irb):3:in `<main>': param is missing or the value is empty: foo (ActionController::ParameterMissing)
irb(main):004> ActionController::Parameters.new({}).permit(foo: []).fetch(:foo)
(irb):4:in `<main>': param is missing or the value is empty: foo (ActionController::ParameterMissing)
```
`fetch(:foo)` を使用して、キーが存在しない場合や `nil` である場合には、`ActionController::ParameterMissing` エラーを発生させる感じですね。