> ## Documentation Index
> Fetch the complete documentation index at: https://docs.appsignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# params.expect: strong parameters in Rails 8

> Rails 8 adds params.expect to permit and require parameters in one step, validate their shape, and avoid NoMethodError for unexpected input.

export const YouTube = ({id, title, description, presenter, duration, republished}) => {
  const playlistId = "PLFHQSOKTqHXA";
  const playlistUrl = `https://www.youtube.com/playlist?list=${playlistId}`;
  if (!id || id === "YOUTUBE_ID") {
    return <div style={{
      border: "1px dashed currentColor",
      borderRadius: "0.5rem",
      opacity: 0.7,
      padding: "2rem 1.5rem",
      textAlign: "center",
      fontSize: "0.875rem"
    }}>
        <strong>Video not published yet.</strong>
        <br />
        {title ? `"${title}" has no YouTube id.` : "This tutorial has no YouTube id."}{" "}
        Replace <code>YOUTUBE_ID</code> in this page with the id from the
        video's URL, once it is in the{" "}
        <a href={playlistUrl}>GoRails x AppSignal playlist</a>.
      </div>;
  }
  const prettyDate = republished ? new Date(`${republished}T00:00:00Z`).toLocaleDateString("en-US", {
    year: "numeric",
    month: "long",
    day: "numeric",
    timeZone: "UTC"
  }) : null;
  const schema = {
    "@context": "https://schema.org",
    "@type": "VideoObject",
    name: title,
    embedUrl: `https://www.youtube-nocookie.com/embed/${id}`,
    contentUrl: `https://www.youtube.com/watch?v=${id}`,
    thumbnailUrl: [`https://i.ytimg.com/vi/${id}/maxresdefault.jpg`],
    creator: {
      "@type": "Organization",
      name: "GoRails",
      url: "https://gorails.com"
    },
    publisher: {
      "@type": "Organization",
      name: "AppSignal",
      url: "https://appsignal.com"
    }
  };
  if (description) schema.description = description;
  if (duration) schema.duration = duration;
  if (republished) schema.uploadDate = republished;
  if (presenter) schema.author = {
    "@type": "Person",
    name: presenter
  };
  return <div style={{
    marginBottom: "1.5rem"
  }}>
      <iframe width="100%" height="450" src={`https://www.youtube-nocookie.com/embed/${id}?list=${playlistId}`} title={title} style={{
    borderRadius: "0.5rem",
    border: 0,
    display: "block"
  }} allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen />
      <p style={{
    fontSize: "0.875rem",
    marginTop: "0.5rem"
  }}>
        <a href={`https://www.youtube.com/watch?v=${id}&list=${playlistId}`}>{`Watch "${title}" on YouTube`}</a>
      </p>
      <script type="application/ld+json" dangerouslySetInnerHTML={{
    __html: JSON.stringify(schema)
  }} />
    </div>;
};

<YouTube id="Q_0OpE_rcVY" title="Use params.expect for strong parameters" description="Rails 8 adds params.expect to permit and require parameters in one step, validate their shape, and avoid NoMethodError for unexpected input." presenter="Chris Oliver" duration="PT8M44S" republished="2026-09-19" />

Presented by Chris Oliver for [GoRails](https://gorails.com). Republished on AppSignal September 19, 2026.

Rails 8 scaffolds generate `params.expect` where they used to generate `require` and `permit`. The new form is shorter. It also fixes an ordering problem that has raised avoidable exceptions in Rails applications for as long as strong parameters have existed.

<h2 id="what-this-covers">
  What this covers
</h2>

* The exception `require` then `permit` raises on tampered input.
* Why reversing the order fixes it, and why `expect` is better than doing that yourself.
* Declaring a parameter as an array so it cannot arrive as a hash.

<h2 id="requirements">
  Requirements
</h2>

| What     | Value                                     |
| -------- | ----------------------------------------- |
| Rails    | 8.0 or later                              |
| Adoption | Opt-in. `require` and `permit` still work |

<h2 id="the-problem-with-require-then-permit">
  The problem with require then permit
</h2>

The familiar form reads:

```ruby theme={null}
params.require(:post).permit(:title, :body)
```

`require(:post)` returns whatever is at that key and asserts only that it is present. `permit` is then called on the result.

That is fine while `post` is a hash. It is not fine when someone sends `?post=hello`. `require` happily returns the string `"hello"`, `permit` is called on a `String`, and the request dies with:

```
NoMethodError: undefined method `permit' for an instance of String
```

A `NoMethodError` is the wrong outcome. The parameters were malformed, which is a client error, and strong parameters exist precisely to reject malformed input. Instead the code sanitizing the input is the code that crashes. Bots probing an application find this quickly, which is why these show up in error tracking in volume.

<h2 id="why-reversing-the-order-fixes-it">
  Why reversing the order fixes it
</h2>

Call `permit` first and the shape is checked before anything is extracted:

```ruby theme={null}
params.permit(post: [:title, :body]).require(:post)
```

Now `permit` evaluates `post` against a declared shape of "a hash that may contain `title` and `body`". A string does not match, so it is dropped rather than passed on, and `require` then raises `ActionController::ParameterMissing`, which Rails already handles as a `400`.

<h2 id="what-expect-does">
  What expect does
</h2>

`params.expect` is that ordering, built in:

```ruby theme={null}
params.expect(post: [:title, :body])
```

One call. Malformed input raises `ParameterMissing` rather than `NoMethodError`, without you having to remember which way round to write two methods.

<h2 id="declaring-arrays">
  Declaring arrays
</h2>

The second thing `expect` fixes cannot be expressed with `permit` at all.

Consider nested categories. With `permit`, sending `post[categories][name]=foo` produces a single hash where your code expects an array of them. The parameters are permitted, the types are wrong, and the failure surfaces later, somewhere else.

`expect` lets you require an array, using a second pair of brackets:

```ruby theme={null}
params.expect(post: [:title, :body, categories: [[:name]]])
```

The outer brackets say "this is an array". The inner ones list the attributes permitted on each element. Send a hash where an array was declared and it is not permitted, rather than quietly arriving in the wrong shape.

<h2 id="migrating">
  Migrating
</h2>

`expect` is opt-in, and `require` and `permit` are not deprecated. Nearly every existing Rails application uses them, so they will be around for a long time.

* **On Rails 8:** move controllers to `expect`. Rails moved its own internal controllers, including Action Mailbox, in the same change. The edit is mechanical: replace `require` and `permit` with `expect`, and add a second pair of brackets around anything that should be an array.
* **Not yet on Rails 8:** write `permit` before `require`. You get the ordering benefit without the upgrade.

<h2 id="what-this-changes-in-your-error-tracking">
  What this changes in your error tracking
</h2>

This is one of the few refactors with a directly visible effect on your incident list.

`NoMethodError: undefined method 'permit' for an instance of String` is noise. It comes from whoever is probing your application, not from your users. It carries no information after the first occurrence, and it competes for attention with real regressions. Moving to `expect` turns those into `ParameterMissing`, which Rails answers with a `400` without raising through your application.

Two things worth doing alongside the change:

* Watch the incident in [error tracking](/errors) to confirm it stops after the deploy. Add a [deploy marker](/guides/deploy-markers) so the before-and-after is visible on the graph. The [request parameters](/guides/custom-data/request-parameters) on each incident show what was submitted, so you can tell a probe from a form your own application posts.
* If some volume remains, [ignore the error](/guides/filter-data/ignore-errors) rather than letting it sit in your list. Ignore it because you have established it is client-side noise, not because it is loud.

<h2 id="transcript">
  Transcript
</h2>

Transcribed from the video and lightly edited: the automatic captions misheard a number of product and API names, and those have been corrected.

<Accordion title="Read the transcript">
  **0:02** Hey guys, this episode we're gonna be talking about the brand new `params.expect` in Rails 8 that you may or may not have upgraded to yet. You will see this in your scaffolds where you get `params.expect` ID, you also see `params.expect` your model name like post with title and body. And this addresses a couple of issues with the old way of doing things. So let's take a look at some of those problems. The old way that you might do this is you might say require post, permit, title and body attributes, right?

  **0:35** So if we were to go into our Rails application, here I've taken the new action and I'm just printing out the params that are permitted through there. So right now we see `params.expect` is raising an error saying there is no post. Now we are gonna use that `params.require`. And it will also say that this is invalid because there is no post in the params. So let's add post and normally we do something like this where a title, the attribute is in square brackets next to post so it knows to put it inside of post and make that a hash.

  **1:10** However, we have oftentimes malicious users or bots or whatever poking around changing things up. And so if we say post equals hello where it's a string instead of a hash, our params are going to blow up with this no method error rather than actually sanitizing those params like it's supposed to. So this is a problem where we have `params.require` post. This is going to give us, let's blow this up a little bit. This is gonna give us the string back of hello and then obviously permit is not a method on that string.

  **1:42** It needs to be on a parameter object. So the way to fix this is actually to flip flop it. So the way we can do that is we can say `params.permit` post with title and body. And then we also tell it to do that require of post. So in order to try that out, we will see if this require fails with the correct error message and the parameter missing error.

  **2:13** And that is because when `params.permit` looks at this, it's going to then evaluate post and see that, oh, this needs to be a hash with title and body is optional inside of it. And that is a string and not a hash. So we should not permit that parameter. So there we go, we get an empty item here, which then allows us to require post. But all of our scaffolds and controllers have done the opposite where they've done the require first since strong parameters came out.

  **2:47** And this is one of the issues that `params.expect` handles for us. If we run this, we will get that missing param method automatically or missing param post with the parameter missing error just like we see here out of the box. But we don't have to like double specify this require. It is going to be just this portion of everything for us, which is great. So that simplifies this a little bit and makes this a little bit more secure so that people tampering with parameters are going to get the correct errors back with these being invalid requests.

  **3:29** So that will sanitize things a little bit smoother. But what happens when we have something like maybe we have a nested item like categories and categories could have a name on them. So if we were to do this and we say post with let's do title and post categories as foo. This right here is going to then see that categories was not a hash, it was just a string. So it is not permitted in this parameters.

  **4:01** But if we were to say categories name foo, this is going to set categories to that sub permitted parameters. But this is wrong because we want an array instead of a single item. And so our parameters are still something that can be messed with a little bit here because we need that empty square brackets inside to tell rack and action dispatch to make sure that this is parsed as an array instead of an object like a hash. So this is something that is not really doable in params permit, but we can do that in here. And the way we do it is we would say categories and we would do double square brackets so that the first square bracket says hey this is going to be an array and then the attributes that are permitted inside of that array for each of the objects.

  **4:59** So now if we refresh this we will get the correct thing where it is square brackets for the category. So we get an array, if we were trying to mess with this and assign categories to a hash instead it is going to say that is not permitted and take care of that completely. And you will see the old way of doing things if we re-enable that we'll still set categories but it will set it to a single item instead of an array which is not what we want. That is not properly being trusted and sanitized here for us. So params expect is a great improvement just to make this more consistent and less tamperable.

  **5:41** So we have to do it correctly in order to give us those types. It either needs to be a hash or an array or an individual value and we can tell it that and it will enforce it like it is supposed to. Now one thing I want to point out before we go is that params expect is opt in, your existing Rails application controllers do not have to use this permit and require probably going to exist for a long time because most all Rails applications use them currently and it would be a very big change to require people to move to expect. And while permit and require do continue to work params expect is really the recommended way to do this just to keep things a little bit safer in your Rails apps. So if you can upgrade to Rails 8 definitely upgrade your controllers to the new syntax and if you can't upgrade to Rails 8 quite yet use the permit and then the require in that order so that you will get a little bit more of that benefit of order of operations for permitting those parameters.

  **6:46** So when people are doing those malicious post equals name or foo or whatever and submitting a string instead of a hash or an array you will be protected by those and you will have less errors in your error monitoring and this will be taken care of automatically for those situations which is great. So I highly recommend doing at least this but if you can get up to Rails 8 then take care of all those params methods and make sure that you use params expect everywhere you can. If you want to see some of the internals of this Martin Emde works on RubyGems and Bundler and made this PR to Rails back in like April finally got merged in September and there's a lot of great discussion talking about how it should work and the back and forth on those various things 'cause this is quite a core feature of Rails at this point to be changing. So it's a pretty big PR here and it goes and updates all of the Action Mailbox and other internal controllers inside of Rails to use params expect as well and all of these are really pretty simple.

  **7:59** It's pretty much adding expect, removing require and permit and then wrapping any of those arrays with an extra square brackets to handle that. But take a look at this PR it's really great, great discussion and stuff if you want to see what goes on on a core feature like this by the Rails core team and stuff. So that is it, params expect I think is gonna be a great improvement just protecting our code just a little bit more and we will benefit from that with less of those undefined method permit issues whenever somebody is poking around where they shouldn't be. So that's it for this episode, I hope you enjoyed it and I will talk to you in the next one. Peace!
</Accordion>

<h2 id="related-tutorials">
  Related tutorials
</h2>

* [Parameters with defaults](/tutorials/ruby/parameters-with-defaults)
* [Impersonation](/tutorials/ruby/impersonation)

<h2 id="about-this-tutorial">
  About this tutorial
</h2>

This tutorial summarizes a GoRails screencast on `params.expect` in Rails 8, by Chris Oliver. The screencast is the original work. GoRails publishes it, and the rest of the series, at [gorails.com](https://gorails.com).
