rails: Strong parameters: allow hashes with unknown keys to be permitted
From what I can tell, strong parameters currently has no ability to permit a hash with unknown keys. Where this would be particularly useful is for the newly supported Hstore and JSON data types – a good example of this use case can be found here: http://schneems.com/post/19298469372/you-got-nosql-in-my-postgres-using-hstore-in-rails
I would have expected that passing an empty hash as an permitted value would allow a hash to be passed through. e.g.
params = ActionController::Parameters.new(product: { name: 'Test', data: { weight: '12kg' } })
params.require(:product).permit(:name, data: {})
however this does not pass the data hash through (though it is not documented that it should work).
Assigning the data parameter separately is an option but it complicates my code unnecessarily – I would prefer to be able to stick with mass-assignment for all attributes.
Happy to work on a patch for this if this proposal is reasonable. I’ve only just started looking into strong parameters though so there may be drawbacks I haven’t considered.
About this issue
- Original URL
- State: closed
- Created 11 years ago
- Reactions: 39
- Comments: 70 (28 by maintainers)
Links to this issue
Commits related to this issue
- integrate the strong params README into the AC guide. The current ActionController guide does not mention strong parameters at all. I integrated the README into the guide to explain the API. I also ... — committed to senny/rails by senny 11 years ago
- Update example for whitelisting arbitrary hashes Since the ability to whitelist arbitrary hashes was added (https://github.com/rails/rails/issues/9454 was resolved by https://github.com/rails/rails/c... — committed to Meekohi/rails by Meekohi 6 years ago
None of the workarounds suggested in this thread seemed to work for me (Rails 4.2.4), so I devised the following workaround:
Hope that helps someone.
Hey @fxn and @senny, it’s the year of 2016 now. Rails got background jobs and even websockets, things that were originally neglected/ignored. Since 2013, the use of free-form JSON and Hstore values has rapidly grown too, and maybe it’s high time to not ignore them either. The values submitted to them are free-form by design, therefore not subject to any whitelisting/filtration. But they don’t live alone. They live inside models whose fields are whitelisted and filtered against mass assignment.
Consider Stripe gateway. They allow users to store free-form metadata on customer object, and that’s just a hash. How to achieve that in Rails if the JSON request looks like this: POST /customers, {description: …, source: …, metadata: {… free form data here…}}. Answer: fight the framework - and many people in this thread did it their own way. Currently, if there’s any JSON field without a predefined “schema” inside the params, Strong Parameters has to be worked around for each field. In 2016, letting a non-checked JSON in is a common situation.
There’s already @sdepold’s pull request to allow this here: https://github.com/rails/strong_parameters/pull/231. I hope you can reevaluate this feature and give it a green light. If there’s any issue/feedback about that PR, I can take it over from the original submitter and finish it up if he’s unreachable. Please let me know.
Another possibility which I think I like even better would be to allow
permit!
to take an optional parameter, to allow this:Implemented here.
Confirm, the API is designed to whitelist every single key. If your use case does not fit well then you need to resort to Ruby. That flexibility is also by design, in the end it is just Ruby.
For nested params I now use the following:
Edit: Updated version can be found in this gist
To avoid duplication set the trusted parameter in the helper (untested):
Rails 5 workaround -
I realize this doesn’t whitelist the data within the json_data param, but it excludes same-level unpermitted params.
Oh and hi @stevebissett 👍
Understand, but this doesn’t exactly seem like an uncommon use case (particularly with the introduction of Hstore and JSON types), and with
attr_accessible/attr_protected
being removed from Rails core, options are severely reduced.I would argue that this actually makes the API more consistent, since
permit!
would accept a list of keys, closely mirroringpermit
.Except you’d only be permitting an unknown hash on a single key within the params. It’s certainly more secure than doing a
permit!
on the whole params hash (which is supported behaviour).My main problem with this is that I go from having to test one message (
Product.create
) to having to test three messages (Product.new
,@product.data=
and@product.save
). The separate assignment for my hash also has to be duplicated across both mycreate
andupdate
actions.Here’s a concrete example of how my proposal would improve things. Before:
After:
I do think this needs more discussion. It’s all well and good to say just do it manually, but it feels wrong to have to work around a feature (and make my code more complicated), particularly when the alternatives have been removed.
I modified @PandaWhisperer solution to safeguard against non-existing key:
In the majority of these comments, the keys are known. This thread’s subject was for unknown keys.
A few of the better workarounds are:
permit!
of a nested hash : https://github.com/rails/rails/issues/9454#issuecomment-280853714permit_recursive_params
function : https://github.com/rails/rails/issues/9454#issuecomment-199357625Fix is coming in 5.1.2 https://github.com/rails/rails/commit/e86524c0c5a26ceec92895c830d1355ae47a7034
Running on
Rails 4.2.6
this works for me:This will allow the following params to be permitted:
This can be seen in the source here: https://github.com/rails/rails/blob/bdc73a438a97f2e0aceeb745f4a95f95514c4aa6/actionpack/lib/action_controller/metal/strong_parameters.rb#L522
If you don’t know what might be in
custom_attributes
you could doPerhaps this is overkill, but one simple workaround that I didn’t see here is as follows:
params.permit( ... ).merge(params.to_unsafe_hash.slice(:json_data))
The other workarounds don’t appear to work for arbitrarily nested hashes and arrays.
Patch versions don’t have new features, this is going to come with 5.1.
@Nowaker agree, we’ve been conservative, but time says this use-case deserves a dedicated API.
The
:*
proposal seems good to me. The patch would need some work, but the basic idea of a wildcard symbol to say “accept whatever here” might work.The patch is no big deal, I might write one based on that PR (and give credit of course).
@olmesm
need to add a if condition otherwise it add a nil value for json_data, which causes issue if you call
update
on an object with the parambut good workaround, thank you!
that’s a sufficient workaround but hardly a good solution. what about
params.require(:product).permit(:name, :data => Hash)
and allowingHash
as a permitted scalar value IF it is explicitly defined.This thread started quite a while ago, but it’s still active, so here’s something I just discovered. I was able to permit a
jsonb
attribute to my User model with the syntax normally used withaccepts_nested_attributes_for
:Where
data
is your json attribute, and the array of properties are what you wish to permit. I’m not sure if this is functioning as intended by Rails, or if it just works through some fluke. It may not work with every use case, or with deeply nested attributes, but I’m curious to see how it works for others here. I’m using Rails5.1.1
.@aliibrahim thanks, that worked for me!
it would be nice if it also allow to set structure incoming hash, like a
for next cases in params:
This monkey patch can help: https://gist.github.com/iagopiimenta/43a66712c021a9d3f540#file-monkey_patch_rails_parameters-rb-L21
It can be used like this:
@anklos your example work, saves the data correctly (thx alot)
but
It still complains about the concerned json column being unpermitted. Is that normal ?
@spohlenz i have faced similer problem and after lot of googling and going through various stackoverflow Q/A was still not able to figure out good solution. So, i tried myself some hack. So, here what i did params = ActionController::Parameters.new(driver: { driver_data: { name: “zyx”, address: “adfasf” } }) driver_data_keys = params[:driver][:name].keys params.require(:driver).permit(driver_data: [driver_data_keys])
Advantages
@Nowaker I can’t turn that into a hash, because I need multiple of them, (edited slightly).
@uberllama I have modified your answer a little and it worked nicely for any number of dynamic keys. Example params:
post: { something: {...}, something_else: {...} }
I think, This will help,
@fxn agree, that’s an acceptable solution. @nhattan, I ended up adding testing for nil value.
fyi, the approach in #12609 is not feasible for hashes of a depth greater than 2.
The solution mentioned by @fxn only works if you have decided to log instead of raise when un-permitted parameters are present. There are plenty of situations (metadata, configuration, etc.) where whitelisting a sub-hash is completely valid.
I think that @spohlenz solution might be the most similar and I will have to implement something like it.
@hakanensari I’m not sure if duplicating the guides in the README is a good thing. They will get out of sync quickly. I’d like to keep the additional examples in the guides because they are the reference for Rails.
Maybe we could link from the README to the relevant section in the guides? (http://guides.rubyonrails.org/action_controller_overview.html#more-examples)
@fxn what do you think?