Rails - Strong parameters with empty arrays
Asked Answered
S

5

48

I'm sending an array of association ids, say foo_ids to my controller. To permit an array of values, I use:

params.permit(foo_ids: [])

Now, the problem is that if I send an empty array of foo_ids, the parameter is ignored. Instead of clearing all foos as an empty array should do, the association is left alone, because foo_ids isn't permitted.

This may be because an empty array is converted to nil in rails, and that nil value is ignored as strong parameters is looking for an array of scalar values, not a single scalar value.

Can anyone suggest a good way to solve this? Thanks!

Additional info

In an update controller action, I need to be able to handle two cases. I need to be able to set foo_ids to an empty array. I also need to be able to ignore foo_ids if I merely want to update another field. Setting foo_ids to an empty array if nil does not work for this second case.

Sinkage answered 23/11, 2013 at 15:49 Comment(0)
S
22

The temporary solution I've come down to is:

params[:foo_ids] ||= [] if params.has_key?(:foo_ids)
params.permit(foo_ids: [])

Here, foo_ids is set to an empty array only if is passed. If it is not passed in the request, it is ignored.

I'm still hoping to find a better solution to this, as this sort of thing will be quite common in the project I'm working on - please do suggest better ideas if you have any.

Sinkage answered 24/11, 2013 at 3:9 Comment(2)
i was considering a monkey patch before_filter where i would do something to address this for all keys that end in "_ids". thoughts?Monseigneur
That'd work. I had a few cases where I needed to do this for params nested within hashes, so you might want to handle those as well depending on your usage.Sinkage
S
39

This is quite late, but I just had this problem myself. I solved it by including both the scalar version and array version in the permit statement, like so:

params.require(:photo).permit(:tags, tags: [])

FYI - it has to have both in the same permit statement - if you chain them it'll get thrown out for some reason.

EDIT: I just noticed that an empty array submitted via this method will be turned into nil - I've now got a bunch of fields that should be empty arrays that are nil. So the solution I posted doesn't actually work for me.

EDIT the second: Thought I had already added this, but this problem is related to Rails performing deep_munge on params hashes. This comment explains how to fix it: https://mcmap.net/q/296001/-rails-converts-empty-arrays-into-nils-in-params-of-the-request

Spaetzle answered 26/5, 2014 at 1:32 Comment(0)
S
22

The temporary solution I've come down to is:

params[:foo_ids] ||= [] if params.has_key?(:foo_ids)
params.permit(foo_ids: [])

Here, foo_ids is set to an empty array only if is passed. If it is not passed in the request, it is ignored.

I'm still hoping to find a better solution to this, as this sort of thing will be quite common in the project I'm working on - please do suggest better ideas if you have any.

Sinkage answered 24/11, 2013 at 3:9 Comment(2)
i was considering a monkey patch before_filter where i would do something to address this for all keys that end in "_ids". thoughts?Monseigneur
That'd work. I had a few cases where I needed to do this for params nested within hashes, so you might want to handle those as well depending on your usage.Sinkage
H
1

This solution won't work for all cases:

params.require(:photo).permit(:tags, tags: [])

For example, if you are using MongoDB and you have a tag_ids array, which stores the ids in a has_many collection, your tag_ids attribute MUST be an array if you specify "type: Array" for the attribute in your model. Consequently, it won't work to send tag_ids with a nil value even if you do this:

 params.require(:photo).permit(:tag_ids, tag_ids: [])

Mongoid, the official Ruby adapter for MongoDB, will complain the value of tag_ids must be an array.

The solution is you can indeed send an empty array via your form! And it doesn't need to be a json request. You can simply use remote: true on your form and send it via type: :js. How to do it? Simple. Just add a hidden input in your form and set its value to an empty string:

<%= form_for @user, remote: true, html: { class: 'form' } do |f| %>
  <%= select_tag("#{f.object_name}[tag_ids][]", options_for_select(Tag.all.collect {|t| [t.name, c.id]}, selected: f.object.tag_ids), { class: 'form-control', multiple: 'multiple' }) %>
  <%= hidden_field_tag "#{f.object_name}[tag_ids][]", '' %>
  <%= f.submit class: 'btn ink-reaction btn-raised btn-primary' %>
<% end %>

This here is the key:

<%= hidden_field_tag "#{f.object_name}[tag_ids][]", '' %>

Your attribute will be stored as an empty array in your database. Note I only tested this with Mongoid, but I assume it carries the same functionality in ActiveRecord.

Heterogamy answered 14/8, 2019 at 20:35 Comment(0)
T
0

I had the same problem recently, but none of the answers here worked for me. This is my solution. If you have javascript handling HTTP requests, this may work for you, too.

In your client side:

if (photo.tags.length === 0){
  photo.tags = ["null"]
}

And on your PhotosController

def photo_params
  p = params.require(:photo).permit(tags: [])
  p["tags"].reject! { |tag| tag == "null" }
  p
end
Tocology answered 7/7, 2016 at 7:11 Comment(0)
P
0

I ran into the same issue and found a similar solution as Donato, albeit while constructing a multipart FormData in JS. The trick was to put an empty string in the array.

const formData = new FormData()
formData.append('dish[tag_ids][]', '')

On the controller side, params arrives with "dish"=>{"tag_ids"=>[""]}, which dish.update interprets as "remove all tags".

Popeyed answered 7/9, 2020 at 5:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.