奮闘したこと
本記事では、Railsで作成したAPIのリクエストボディに配列をトップレベルで持った時、空配列でputリクエストを送れなくなり、詰まったのでそれを解説します。
(どうしてもトップレベルで持ちたい場合、#追記欄に最終的な着地点を記載しています。)
Railsのrequire
メソッドは、特定のパラメータがリクエストに含まれていることを確認するために使用されます。もし指定されたキーが存在しない、または値が空である場合(例えば空の配列[]
)、RailsはActionController::ParameterMissing
エラーを発生させます。これは、require
がリクエストパラメータの存在とその値の有効性を保証するために設計されているためです。
空配列がnil扱いされる問題
リクエストボディのトップレベルに配列を用いると意図しない挙動が発生することがあります。具体的には、配列がnil扱いさてしまうのです。これが原因でエラーが発生します。以下は、具体例を用いて説明します。
例: リクエストボディのトップレベルに配列を使用する場合
リクエストボディは以下のような形です。
{
"tag_ids": [1, 2, 3]
}
そしてrailsのcontroller層は以下。
# 存在かつ空でないことをrequire
params.require(:tags)
このコードでは、:tags
キーが存在し、その値が空でないことを要求しています。しかし、リクエストボディに空の配列を含めると、Railsはその配列を無視し、ActionController::ParameterMissing
エラーを発生させます。
params.permit(tags: [])
空配列を許可する必要がある場合は、permit
メソッドを使用して明示的に許可する必要があります。
しかし、これは以下Railsガイドにもあるように問題もあります。そのため、まとめでお話しするリクエストボディの変更が最善案ではないかと思います。
「paramsの値には許可されたスカラー値の配列を使わなければならない」ことを宣言するには、以下のようにキーに空配列を対応付けます。
params.permit(id: [])
ハッシュパラメータやその内部構造の正しいキーをすべて明示的に宣言できない場合や、すべて宣言するのが面倒な場合があります。次のように空のハッシュを割り当てることは一応可能です。
params.permit(preferences: {})
ただし、この指定は任意の入力を受け付けてしまうため、利用には十分ご注意ください。この場合permitによって、受け取った構造内の値が許可済みのスカラーとして扱われ、それ以外の値がフィルタで除外されます。
RSpecでの問題: 空文字が勝手に格納される
RSpecでテストを行う際、空の配列をリクエストボディに含めると、Railsがこの配列を空文字に変換してしまうことがあります。これにより、テストが失敗する原因となります。
例: RSpecでのテスト
let!(:params) do
{
tag_ids: []
}
end
この例では、tag_ids
キーに空の配列を指定していますが、RSpecではこれが空文字に変換される可能性があります。
Received parameters: #<ActionController::Parameters {"tag_ids"=>[""], "format"=>"json", "controller"=>"api/v1/articles", "action"=>"update"} permitted: false>
このように、tag_ids: []
が[""]
に変換されるため、require
を突破してしまいます。
context 'tag_ids=[]が指定されている場合' do
let!(:article) { create(:article) }
let!(:params) do
{
tag_ids: []
}.to_json
end
まとめ: 正しいリクエストボディの形式
正しいリクエストボディの形式を使用することで、空配列が正しく処理されることを確認できます。
require
はオブジェクトに対してを設定しようということかもですね。
{
"article": {
"tag_ids": [1, 2, 3]
}
}
ただ、params.require(:article)
としても以下の形はbad request
にならないため気が必要かと思われる。
{
article: {
"tag_ids": nil
}
}
RSpecでの解決策
RSpecで空文字変換を避けるには、以下のようにします。
let!(:params) do
{ article: { tag_ids: [] }}.to_json
end
この方法により、空配列が正しく受け取られることを確認できます。RSpecでのテストコードも含めて、適切なパラメータを使用することで、テストの信頼性を向上させることができます。
追記(2024/10/28)
上記にて、議論させていただいた結果、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
エラーを発生させる感じですね。