•4 min read

Laravel Mass Assignment: Fixing the Add [field] to Fillable Error

The error I inherited

Last month I opened a client admin panel that has been running since 2023, added one field to a signup form, and immediately got hit with Illuminate\Database\Eloquent\MassAssignmentException: Add [role] to fillable property to allow mass assignment on [App\Models\User]. Every Laravel developer meets this error in their first week. What made me actually stop was the model itself. A previous developer had commented out $fillable, set $guarded = [], and left a comment above it that said fixed. It had been running that way for three years.

That comment is why I'm writing this. The exception is not Laravel being difficult. It is one of the few places where the framework refuses to run code that would work, and the workarounds people use to silence it are the actual bug.

What mass assignment is

When you pass an array straight into create(), fill() or update(), every key in that array becomes a column write. If the array comes from the request, the client decides what's in it.

// The line every beginner writes
$user = User::create($request->all());

Now imagine the users table has an is_admin column. Nothing stops a request from containing it:

POST /register
name=Ada&[email protected]&password=s3cret&role=admin

Without the guard, that POST creates an administrator. With it, Eloquent throws instead of writing the field. The error message names the exact key it refused, which is a nice touch. It is telling you that unvetted input reached a model, not that your syntax is wrong.

The fix, and the line that makes it stick

Whitelist the fields the form is allowed to set, and stop passing the raw request:

class User extends Model
{
    protected $fillable = ['name', 'email', 'password'];
}

// in the controller
$user = User::create($request->validated());
$user->role = 'admin'; // direct assignment skips fillable, by design
$user->save();

Two details here matter more than they look. First, validated() returns only the keys your validation rules covered, so the whole class of stray-input problems dies before it reaches the model. Second, assigning role directly is not a workaround. It's the intended path for fields no user input should ever set. Privileged writes stay visible in the diff.

The silent version is worse

Here's the part I got burned by. Once $fillable is defined, keys that aren't on the list don't throw. They get dropped. Quietly.

My role field never saved on that first deploy because I had forgotten to add it to the list, and nothing told me. The page reloaded fine, the row existed, the column was just empty. In development that's the nastiest possible failure mode, because everything looks like it worked.

One line in AppServiceProvider changes that:

use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
}

Now any attempt to fill a discarded attribute throws in local environments. Production keeps the forgiving behavior, which you may actually want for fields that vary between form versions. I run this on every project now. It caught two more forgotten keys the same week.

$guarded = [] is the opposite of a fix

Search "mass assignment" and you'll find answers suggesting $guarded = []. That empties the block list and makes every column writable, including id, role, is_admin, and whatever column someone adds next year. The exception goes away. So does the thing that caused it.

A block list only works if you remember every field to block, forever. An allow list just asks the new column to earn its place in a review. That's the whole reason $fillable ages better, and it's why my inherit-three-years-later moment happened at all.

If you want a quick audit, grep your codebase for guarded = [] and for forceFill(). The first is almost always wrong on models that touch requests. The second bypasses the protection on purpose, which is fine in a seeder and worth a raised eyebrow in a controller. On a related note, when you're auditing a model, check its query count too. I wrote about that in finding and fixing N+1 queries.

Laravel 13 makes it an attribute

Since Laravel 13 you can also declare this on the class itself:

#[Fillable(['name', 'email', 'password'])]
class User extends Model
{
}

It's optional and backward compatible, so the property version isn't going anywhere. I like the attribute for small models because the list sits right at the top where the eye lands. On models with fifteen fillable fields I still use the property, mostly because a long attribute list reads badly.

What I do now

Form request at the boundary, validated() everywhere I would have written all(), and $fillable as the second line of defense rather than the only one. The fillable-plus-validated controller pattern is one of the snippets I keep in Snippet Ark so new controllers start from it instead of from memory. Do that and the exception shows up once, in development, on the day you add a column. Which is exactly when you want to see it.