58

I make a http put request with following parameters:

{"post"=>{"files"=>{"file1"=>"file_content_1", "file2"=>"file_content_2"}}, "id"=>"4"}

and i need to permit hash array in my code. based on manuals I've tried like these:

> params.require(:post).permit(:files) # does not work
> params.require(:post).permit(:files => {}) # does not work, empty hash as result
> params.require(:post).permit! # works, but all params are enabled

How to make it correctly?

UPD1: file1, file2 - are dynamic keys

rdo
  • 3,872
  • 6
  • 34
  • 51

13 Answers13

84

Rails 5.1+

params.require(:post).permit(:files => {})

Rails 5

params.require(:post).tap do |whitelisted|
  whitelisted[:files] = params[:post][:files].permit!
end

Rails 4 and below

params.require(:post).tap do |whitelisted|
  whitelisted[:files] = params[:post][:files]
end
Orlando
  • 9,374
  • 3
  • 56
  • 53
49

In rails 5.1.2, this works now:

params.require(:post).permit(:files => {})

See https://github.com/rails/rails/commit/e86524c0c5a26ceec92895c830d1355ae47a7034

Derek
  • 1,735
  • 17
  • 14
  • 2
    but what if `files` is nested multiple levels deep? e.g. `params['post']['mymodel_attributes']['things']['files']` and `files` can be a hash with any keys? – Tilo Apr 05 '18 at 23:22
  • The syntax `:files => {}` will allow nested params in `files` to any level. – Derek Apr 06 '18 at 03:51
  • 2
    unfortunately that didn't work for me. It resulted in a literal `{}` in the filtered params. I worked around it though :P Deeply nested input data for StrongParams is not fun to deal with. Improvements in the StrongParameter syntax would be good. – Tilo Apr 07 '18 at 17:12
  • 1
    Hmm, it's working fine for me in rails `5.1.5`. Glad you found a work-around. – Derek Apr 09 '18 at 16:13
  • 4
    Please note that if you have several permitted parameters in `post`, the hash one (`files` in this case) has to be the last one in the `permit()` list or you'll get a syntax error. – gfd Jan 20 '20 at 17:54
18

I understand that this is an old post. However, a Google search brought me to this result, and I wanted to share my findings:

Here is an alternative solution that I have found that works (Rails 4):

params = ActionController::Parameters.new({"post"=>{"files"=>{"file1"=>"file_content_1", "file2"=>"file_content_2"}}, "id"=>"4"})
params.require(:post).permit(files: params[:post][:files].keys)
# Returns: {"files"=>{"file1"=>"file_content_1", "file2"=>"file_content_2"}}

The difference between this answer and the accepted answer, is that this solution restricts the parameter to only 1 level of dynamic keys. The accepted answer permits multiple depths.

[Edit] Useful tip from comment

"Oh, and you need to verify that params[:post][.files] exists otherwise keys will fail"

Val Entin
  • 953
  • 10
  • 18
wes.hysell
  • 1,199
  • 10
  • 14
  • 4
    Oh, and you need to verify that params[:post][.files] exists otherwise keys will fail – Christer Fernstrom Apr 28 '16 at 13:39
  • A neat idea actually (with caveats) – prusswan Sep 08 '16 at 09:05
  • In the callback you can do whatever you want, the idea is to provide a way to do things, not to allow people to copy paste stuff – Orlando Feb 13 '17 at 17:10
  • @ChristerFernstrom, I believe if you change the suggested solution from: `params.require(:post).permit(files: params[:post][:files].keys)` to `params.require(:post).permit(files: params[:post].require(:files).keys)` then verifying that `params[:post][.files]` exists will already happen for you, so you won't ever call `.keys` on `nil`. – rmacklin Aug 22 '17 at 00:42
6

Orlando's answer works, but the resulting parameter set returns false from the permitted? method. Also it's not clear how you would proceed if you were to later have other parameters in the post hash that you want included in the result.

Here's another way

permitted_params = params.require(:post).permit(:other, :parameters)
permitted_params.merge(params[:post][:files])
Toby 1 Kenobi
  • 4,717
  • 2
  • 29
  • 43
5

Here's what we had to do in Rails 5.0.0, hope this helps someone.

files = params[:post].delete(:files) if params[:post][:files]
params.require(:post).permit(:id).tap do |whitelisted|
  whitelisted[:files] = files.permit!
end
Damion Gomez
  • 338
  • 4
  • 8
3

In my case, there was just one attribute which had dynamic keys,

def post_params
  marking_keys = Set.new
  params[:post][:marking].keys.collect {|ii| marking_keys.add(ii)}
  params.require(:post).permit(:name, marking: marking_keys.to_a)
end
Mukarram Ali
  • 387
  • 5
  • 24
2

You can use a temporary variable to build your permitted list like so:

permitted = params.require(:post).permit(:id)
permitted[:post][:files] = params[:post][:files].permit!
Cam
  • 81
  • 1
  • 5
1

Here is another way to get around this:

  def post_params
    permit_key_params(params[:post]) do
      params.require(:post)
    end
  end

  def permit_key_params(hash)
    permitted_params = yield
    dynamic_keys = hash.keys
    dynamic_keys.each do |key|
      values = hash.delete(key)
      permitted_params[key] = values if values
    end
    permitted_params
  end

This should work for post: { something: {...}, something_else: {...} }

Sash
  • 4,448
  • 1
  • 17
  • 31
1

Here's a simple way to do it (works for rails 5):

  def my_params
    data_params = preset_data_params

    params.require(:my_stuff).permit(
      :some,
      :stuff,
      data: data_params
    )
  end

  def preset_data_params
    return {} unless params[:my_stuff]
    return {} unless params[:my_stuff][:data]

    params[:my_stuff][:data].keys
  end
Krzysztof Karski
  • 749
  • 1
  • 12
  • 19
0
    Send params as array type like name=date[]**strong text**
      def user_post
        dates = params[:date]
        #render json: { 'response' => params }
        i = 0
        dates.each do |date|
          locations = params['location_'+"#{i}"]
          user_names = params['user_'+"#{i}"]
          currency_rates = params['currency_'+"#{i}"]
          flags = params['flag_'+"#{i}"]
          j = 0
          locations.each do |location|
             User.new(user_name: user_names[j], currency_name: flags[j],
             currency_rate: currency_rates[j], currency_flag: flags[j], location: location).save
            j =+ 1
          end
          i =+ 1
        end
   def
vikas pal
  • 75
  • 1
  • 9
0

I could not get any of the many proposed answers to work (Rails 5) without either:

  1. knowing all the hash keys in advance, or
  2. virtually negating the value of strong parameters by allowing arbitrary params.

I'm using this solution.
It uses the standard strong parameters rig to clean up most of the params, and the Hash attribute is added back in explicitly.

# Assuming:
class MyObject < ApplicationRecord
  serialize :hash_attr as: Hash
  #...
end

# MyObjectsController method to filter params:
def my_object_params
  # capture the hashed attribute value, as a Hash
  hash_attr = params[:my_object] && params[:my_object][:hash_attr] ?
      params[my_object][:hash_attr].to_unsafe_h : {}
  # clean up the params
  safe_params = params.require(:my_object).permit(:attr1, :attr2) # ... etc
  # and add the hashed value back in
  safe_params.to_unsafe_h.merge hash_attr: hash_attr
end
Tom Wilson
  • 797
  • 9
  • 26
0

Let's use a more complicated subset of data:

task: {
  code: "Some Task",
  enabled: '1',
  subtask_attributes: { 
    '1' => { field: 'something', rules: {length_10: true, phone: false, presence: false }} ,
    '2' => { field: 'another', rules: {length_10: true, phone: false, presence: false }} 
  }
}

So we send it to Strong Parameters for processing:

params = ActionController::Parameters.new({
  task: {
    code: "Some Task",
    enabled: '1',
    subtask_attributes: { 
     '1' => { field: 'something', rules: {length_10: true, phone: false, presence: false }} ,
     '2' => { field: 'another', rules: {length_10: true, phone: false, presence: false }} 
    }
  }
})

We will not be able to specify :rules in Strong Params in Rails 4 because it is a hash of data:

permitted = params.require(:task).permit(:code, :enabled, subtask_attributes: [:field, :rules])
Unpermitted parameter: rules
Unpermitted parameter: rules

So what if you want to whitelist specific attributes AND a COLLECTION of hashes of data. The accepted answer does not whitelist specified attributes. You have to do this:

params.require(:task).permit(
    :code, :enabled,
    subtask_attributes: [:field, :rules],
)

# whitelist the validation rules hash
params.require(:task).tap do |whitelisted|
  params[:task][:subtask_attributes].each do |k,v|
    whitelisted[:subtask_attributes][k] = params[:task][:subtask_attributes][k]
    whitelisted.permit!
  end
end

After trying several of the solutions here, none worked. Only aboved worked for nested attributes in a has_many association which contains arbitrary hash data.

Daniel Viglione
  • 8,014
  • 9
  • 67
  • 101
0

I know this is an old post, one of many with different ways to update a serialize hash field. I thought I give my version that I accidently found by piecing together some methods. I'll just use my application. This is Rails 7.0.4 and Ruby 3.0. I also use slim templates.

I have a Taxable model that contains semi-persistent tax rates for different Departments. All items are Sales Tax taxable, but in my case, Liquor adds an additional tax. The Taxable table only has two fields with tax being a serialized JSON field.

create_table "taxables", force: :cascade do |t|
  t.date "date"
  t.string "tax"
  ...
end

If a Tax is changed or added, the I would add a new record to reflect the change that took place on some date. Any ticket that had a tax in the past would use the record that is the earliest record before the ticket date. Anything new will the new changed record

The Taxable model has a constant that names all taxes that may be used:

TaxesUsed = %w(sales county federal city liquor)

The records would be something like:

[#<Taxable:0x0000000111c7bfc0
  id: 2,                   
  date: Sun, 01 Jan 2023,  
  tax: {"sales"=>"8.0", "county"=>"2.0", "federal"=>"0.0", "city"=>"0.0", "liquor"=>"3.0"} ...
 #<Taxable:0x0000000111c7b980
  id: 3,                   
  date: Fri, 01 Jan 2021,  
  tax: {"sales"=>"8.0", "county"=>"2.0", "federal"=>"0.0", "city"=>"0.0", "liquor"=>"4.0"}...
]

I initially had a kludge that worked, which was creating the hash from some un-permitted parameter and updating the record. I then found mention of using form_with to describe the Tax field and to my surprise it worked! The form:

= form_with(model: @taxable) do |form|
 
  div
    = form.label :date, style: "display: block"
    = form.date_field :date

  div
    = form.label :tax, style: "display: block", class:"font-bold"
    = form.fields_for :tax do |tax|
      # @taxable.tax is the existing serialize tax hash or a new default hash
      - @taxable.tax.each do |k,v|
        div.flex.gap-2
          div.w-36.font-bold.text-right = k
          div
            = tax.text_field k, value:v
            
  div[class="#{btn_submit}"]
    = form.submit

I had to define a new taxable_parmam that states that :tax is a Hash

def taxable_params
  params.require(:taxable).permit(:date, :tax => {})
end

Submitting the form give me params:

Parameters: {"authenticity_token"=>"[FILTERED]",
 "taxable"=>{"date"=>"2021-01-01", "tax"=>{"sales"=>"8.0",
 "county"=>"2.0", "federal"=>"0.0", "city"=>"0.0",
 "liquor"=>"4.0"}}, "commit"=>"Update Taxable", "id"=>"3"}

and it works! I forgot about form_with but this is about a simple as you can get just using plain ol Rails.

Update: I forgot that stuff coming from form fields is text. I had to get the params to a new hash, change the float values (percents) and update using the new hash

appleII717
  • 328
  • 3
  • 12