Railsで、wrap_parametersで追加されたキーに対し、Strong Parametersのrequireやpermitを使ってみた

Rails製のAPI

curl -X POST 'http://localhost:3000/wrap_parameter/outputs' -H "Content-Type: application/json" -d '{"name":"foo", "age": 20}'

なリクエストを投げたところ、ログに

Started POST "/wrap_parameter/outputs" for 127.0.0.1 at 2022-05-23 22:26:50 +0900
Processing by WrapParameter::OutputsController#create as */*
  Parameters: {"name"=>"foo", "age"=>20, "output"=>{"name"=>"foo", "age"=>20}}

と出力されていました。

Parameters の構造をよく見ると

{
   "name" => "foo",
   "age" => 20,
   
   # 追加されている
   "output" => {
      "name" => "foo",
      "age" =>20
   }
}

と、リクエストしたデータ '{"name":"foo", "age": 20}' には無いキー output が自動で追加されていました。

なぜこれが追加されているのか気になったので、調べたときのメモを残します。

 
目次

 

環境

 

wrap_parameterによるキーの自動追加について

自動で追加されるキーについてRailsガイドを探してみたところ、以下の記載がありました。
4.2 JSONパラメータ | Action Controller の概要 - Railsガイド

データの送信先がCompaniesControllerであれば、以下のように:companyというキーでラップされます。

{ name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } }

 

そのため、自動的にキーが追加されるのは wrap_parameters 機能によるものだったようです。

wrap_parametersの詳細は、APIドキュメントにも記載されています。
ActionController::ParamsWrapper

 

改めて、今回 output というキーが自動追加されたコントローラを見ると

class WrapParameter::OutputsController < ApplicationController
  def create
    # 略

    head :created
  end
end

となっていました。

コントローラ名が OutputsController なため、 output というキーが追加されたと分かりました。

 

wrap_parametersとStrong Parametersについて

ここまでで、「自動でキーを追加したのは wrap_parameters 機能」と分かりました。

次は wrap_parameters で追加されたキーに対して、Strong Parametersの requirepermit を使って値を取り出してみます。

 

準備

今回は前述のコントローラ OutputsController を使って動作確認することにします。

そこで、動作確認しやすくするよう

  • CSRF対策を除外
  • 取り出した値を出力するメソッド write_log を用意

をコントローラに追加します。

class WrapParameter::OutputsController < ApplicationController
  # POSTする時にCSRFで引っかかってしまうので、今回は外しておく
  protect_from_forgery except: :create

  def create
    output_params

    head :created
  end

  private def output_params
    # ここに確認用の処理を書く
  end

  private def write_log(values)
    logger.info('============>')
    logger.info(values)
    logger.info('<============')
  end
end

 
これで準備ができました。

 

requireやpermitを使わずに、paramsから値を取り出す

準備ができたので、リクエストパラメータが含まれる params より値を取り出します。
4 パラメータ | Action Controller の概要 - Railsガイド

まずは requirepermit を使わずに、paramsから値を取り出してみます。

今回は、curl

curl -X POST 'http://localhost:3000/wrap_parameter/outputs' -H "Content-Type: application/json" -d '{"name":"foo", "age": 20}'

と OutputsController にHTTPリクエストし、その結果を取り出してみます。

コントローラでparamsを確認できるような実装をした上で curl を実行したところ、

# すべて
write_log(params)
# => {"name"=>"foo", "age"=>20, "controller"=>"wrap_parameter/outputs", "action"=>"create", "output"=>{"name"=>"foo", "age"=>20}}

# キーを指定
write_log(params[:name])
# => foo

# wrap parametersのキーを指定
write_log(params[:output])
# => {"name"=>"foo", "age"=>20}

という結果が出力されました。

これにより、wrap_parametersで追加されたキーであっても、普通のキー同様値を取り出せることが分かりました。

 

Strong Parametersで取り出す

続いて、Strong Parametersの機能を使って取り出してみます。

requireやpermitで複雑な形のデータを取り出せるか確認するため、curlでネストしているデータをPOSTしてみます。

curl -X POST 'http://localhost:3000/wrap_parameter/outputs' -H "Content-Type: application/json" -d '{"name":"foo", "age": 20, "pages": [1,2,3], "article": {"title": "hello", "authors": [{"name": "bob", "age": 25, "pages": [1,3]}, {"name": "alice", "age": 30, "pages": [2,4]}]}}'

 
curlワンライナーだとどんなデータか分かりづらいため、データ部分のみ整形したのが以下です。

{
   "name":"foo",
   "age":20,
   "pages":[
      1,
      2,
      3
   ],
   "article":{
      "title":"hello",
      "authors":[
         {
            "name":"bob",
            "age":25,
            "pages":[
               1,
               3
            ]
         },
         {
            "name":"alice",
            "age":30,
            "pages":[
               2,
               4
            ]
         }
      ]
   }
}

これを使って確認します。

 

permitだけ使う

まずは permit だけを指定して、値を取り出してみます。

write_log(params.permit(:name, :age))
# => {"name"=>"foo", "age"=>20}

取り出せました。

なお、permitしてないパラメータについては、ログに出力されていました。

Unpermitted parameters: :name, :age, :article, :output. Context: { controller: WrapParameter::OutputsController, action: create, request: #<ActionDispatch::Request:0x000000010ac06380>, params: {"name"=>"foo", "age"=>20, "pages"=>[1, 2, 3], "article"=>{"title"=>"hello", "authors"=>[{"name"=>"bob", "age"=>25, "pages"=>[1, 3]}, {"name"=>"alice", "age"=>30, "pages"=>[2, 4]}]}, "controller"=>"wrap_parameter/outputs", "action"=>"create", "output"=>{"name"=>"foo", "age"=>20, "pages"=>[1, 2, 3], "article"=>{"title"=>"hello", "authors"=>[{"name"=>"bob", "age"=>25, "pages"=>[1, 3]}, {"name"=>"alice", "age"=>30, "pages"=>[2, 4]}]}}} }

 
他に

  • 配列
  • ネストしたオブジェクト
  • 複雑なネスト

を試してみます。

## 配列
write_log(params.permit(pages: []))
# => {"pages"=>[1, 2, 3]}

## ネストしたオブジェクト
write_log(params.permit(article: [:title, :content]))
# => {"article"=>#<ActionController::Parameters {"title"=>"hello"} permitted: true>}

## 複雑なネスト
write_log(params.permit(:name, :age, article: [:title, :content, authors: [:name, pages: []]]))
# => {"name"=>"foo", 
#     "age"=>20,
#     "article"=>#<ActionController::Parameters {
#       "title"=>"hello", 
#       "authors"=>[
#         #<ActionController::Parameters {"name"=>"bob", "pages"=>[1, 3]} permitted: true>,
#         #<ActionController::Parameters {"name"=>"alice", "pages"=>[2, 4]} permitted: true>
#       ]
#     } permitted: true>
#    }

いずれも取得できました。

 

require + permit を使う

最後に、 wrap_parametersで追加されたキーに対して、 require + permit を使って値を取り出してみます。

同じように値が取り出せました。

## フラット
write_log(params.require(:output).permit(:name, :age))
# => {"name"=>"foo", "age"=>20}

## 配列
write_log(params.require(:output).permit(pages: []))
# => {"pages"=>[1, 2, 3]}

## ネストしたオブジェクト
write_log(params.require(:output).permit(article: [:title, :content]))
# => {"article"=>#<ActionController::Parameters {"title"=>"hello"} permitted: true>}

## もう一回ネスト
write_log(params.require(:output).permit(:name, :age, article: [:title, :content, authors: [:name, pages: []]]))
# => {"name"=>"foo", 
#     "age"=>20,
#     "article"=>#<ActionController::Parameters {
#       "title"=>"hello", 
#       "authors"=>[
#         #<ActionController::Parameters {"name"=>"bob", "pages"=>[1, 3]} permitted: true>,
#         #<ActionController::Parameters {"name"=>"alice", "pages"=>[2, 4]} permitted: true>
#       ]
#     } permitted: true>
#    }

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi-sandbox/rails_7_0_minimal_app

今回のPRはこちらです。
https://github.com/thinkAmi-sandbox/rails_7_0_minimal_app/pull/1