ActionController::Parameters#with_defaults supplies values for keys that did not arrive in the request. It is an alias for reverse_merge: the hash you pass loses to anything the browser submitted.
It earns its place because the browser submits nothing at all in several ordinary cases. A controller that treats “absent” and “false” differently gets those cases wrong.
What this covers
- Supplying defaults for parameters the browser may omit.
- How Rails checkboxes already solve this with a hidden field.
- Why a default is not an authorization mechanism.
Requirements
Supplying a default
status arrives, it is used. If it does not, "draft" is. Unchecked checkboxes and unselected multi-value inputs are the usual reason a key is missing. A default is tidier than a chain of conditionals in the action.
ActionController::Parameters is not a Hash, though it goes to some trouble to behave like one. It implements merge, values_at, compact, blank?, and a long list of others. with_defaults is part of that surface.
What Rails checkboxes already do
Before reaching for a default on a checkbox, it is worth knowing that Rails has usually handled it. An unchecked HTML checkbox submits nothing, and a form has no way to say “this was unchecked”. Soform.check_box renders two inputs: a hidden field with value 0, and the checkbox itself with value 1.
Both are submitted in document order when the box is checked, so Rails receives 0 then 1 and takes the last. When it is unchecked, only the 0 arrives. The result is that a Rails checkbox always submits something, and you rarely need a default for one.
A default is not authorization
This is the part to get right. Defaults lose to submitted values. That is what makes them defaults. So a default cannot stop a user setting a value: anyone who submits the key overrides it. For attributes the user must not control, such as the author of a record, do not express the intent as a default:user_id is not permitted, so it cannot be mass-assigned at all, and ownership comes from the session rather than from the request. Use with_defaults for genuine defaults, where a user overriding the value is acceptable.
Seeing what was submitted
When a default is not behaving as you expect, the question is almost always what the request contained. Locally, you can read that off the network panel. In production, AppSignal records request parameters on error and performance samples, so an incident shows the parameters that produced it rather than the ones you assumed. Because parameters routinely carry personal data, review what is being sent. AppSignal supports filtering parameters so sensitive keys are never transmitted, and this is worth configuring before you rely on parameter data in incidents.Transcript
Transcribed from the video and lightly edited: the automatic captions misheard a number of product and API names, and those have been corrected.Read the transcript
Read the transcript
0:02 Hey guys, this episode we’re gonna be talking about Action Controller parameters, and specifically the method down here at the bottom with defaults. Now this is just an alias for reverse merge, but it is one of those things that I commonly see and commonly do myself in our controllers when I need to assign some default values. So let’s take a look at an example. Let’s generate a scaffold for a post model, and we want the post to belong to a user. And we also want the post to have a title and a body that’s a text column.0:41 So we’ll go ahead and create that. We’ll
rails db:migrate, and we’ll pull up this in our editor, and our post controller here will generate that. But the important part here is that we want this new post to actually assign a default value to that user references. So we can say current user here, and just assign that. But what would be really nice is if we could do this as part of the parameters that we have coming in, and that is exactly what we can do.1:12 So instead of this line here in our create action, kind of making that a bit more complicated, we can use that with defaults here, and say user is going to be current user. Now, I don’t have a current user method, so we’re just gonna say user.first for that. And we don’t want the user to be able to choose who wrote the post, so we’re gonna make sure that user ID is not in the permitted parameters here. If we go to our post form, we can get rid of the user ID, and this will basically use this hash first, and then merge in any of the title and body fields that came in the parameters. You can also use this for other things like maybe you have a checkbox or an array or something that may or may not get submitted by the browser, you can specify your default hash of parameters here, and then they can be overridden by things that are submitted by the browser.2:11 So this is a super duper cool little feature. So let’s open up our Rails server now, and we’ll go to slash posts slash new. We can say test and test. If we create that post, we’re gonna get user ID number one assigned here because of that with defaults. So this is really just the reverse merge that is built into a hash, even though the Action Controller parameters are not actually inheriting from a hash.2:41 So if we go to GitHub for this, you’ll see that if we go look at the implementation of this, we have all of these methods that match with hashes like merge, values at compact, blank, compact, you name it. We have all of these implementation details that work, make this work just like a hash in most cases. But if we come back up to the class and we look for class parameters, you’ll see this doesn’t actually inherit from a hash, it just works like one. So that is a really useful little thing that Colin taught me the other day, basically using with defaults, we can have this as a base hash of values. If we want the user in here, we can do that.3:29 We could do this for default values on radio buttons or checkboxes or whatever the case might be an array. And this is just a great way to do that in case the browser doesn’t actually submit something. The other option that you can do is like checkboxes do in the browser, if you have a F.checkbox in your post form, if we just go in here and we say form.checkboxhello, checkboxes do not get submitted to the browser unless they’re actually checked. So there’s no way to uncheck that in your forms. Check underscore box.4:11 So what you’ll see here is that we have this hello input that’s a hidden value and it sets the value to zero. So if this is not checked, then this hidden value will be submitted and they come over an order in the form. So whenever this is checked, it will send over value zero and then value one and then the Rails parameters are able to figure out that yep, the last value is the one that you actually want in your Rails app. So if we update our post and we go and look at the network request, we’ll see that post hello zero was submitted, but also post hello one. And if we edit this post without checking the box, we can check out that payload.5:02 Post hello zero was just zero. There was no one for the checked box because it was not checked. So you can use inputs inside of your HTML to try and make this work for default values. But if there are any cases where you want the server to set them, for example, we wanna make sure the current user is set here by the server and we don’t want the user to submit that, then we can use the with defaults method to take care of that, which I thought was incredibly useful and incredibly helpful. And I was surprised that I had not learned about it.5:38 So I figured this would make a perfect screencast to share with you guys. So that’s it for this episode. I hope you enjoyed it. And I will talk to you in the next one. Peace.Related tutorials
About this tutorial
This tutorial summarizes a GoRails screencast onwith_defaults for Action Controller parameters, by Chris Oliver. The screencast is the original work. GoRails publishes it, and the rest of the series, at gorails.com.