skip to content

In an Eloquent model, what is the difference between $fillable and $guarded, and what happens to a key neither allows?

level: middleimportance: must knowfreq 82%

answer

  1. allow-list versus deny-list
  2. defaults: [] and ['*']
  3. totally guarded throws MassAssignmentException
  4. otherwise unlisted keys silently dropped
  5. preventSilentlyDiscardingAttributes()

basics

~20 s

$fillable is an allow-list of keys fill() and create() may set; $guarded is a deny-list. A default model is totally guarded and throws MassAssignmentException, while a model with a list silently drops disallowed keys by default.

solid answer

~40 s

Both properties filter mass assignment, meaning `fill()`, `new Model([...])`, `create()` and `update([...])`. `$fillable` is an allow-list: only the named keys are set. `$guarded` is a deny-list: everything except the named keys is set, and `$guarded = []` turns protection off. A model that declares neither keeps the defaults `$fillable = []` and `$guarded = ['*']`, which is totally guarded, so `create()` throws `MassAssignmentException`. Once a list exists, a key it does not allow is silently discarded unless `Model::preventSilentlyDiscardingAttributes()` is on, in which case it throws too. Direct assignment and `forceFill()` bypass both lists. Laravel 13 adds `#[Fillable]`, `#[Guarded]` and `#[Unguarded]` attributes, and the skeleton `User` model uses `#[Fillable]`.

go deeper

for a junior

Recall that $fillable lists what may be set by create() or fill(), and $guarded lists what may not.

for a middle

Explain the default totally guarded state, when fill() throws versus silently drops a key, and which writes bypass the lists.

for a senior

Argue for allow-lists on models touched by user input, turn on strict discarding outside production, and keep server-controlled fields out of request arrays entirely.

for a principal

Set a team rule for mass assignment, weighing allow-list upkeep and developer friction against the cost of one exposed privilege column.

## What mass assignment is **Mass assignment** means setting many model attributes from one array in a single call. In Eloquent it happens in `fill()`, in `new Reservation([...])`, in `Reservation::create([...])` and in `$reservation->update([...])`, and in helpers built on them. It is convenient, and it is dangerous: if the array comes from a request, a user can add keys you never meant to accept, such as `is_vip` or `rate_cents`. Eloquent therefore filters every mass-assigned array through two lists on the model. | Setting | Meaning | Style | |---|---|---| | `$fillable` / `#[Fillable([...])]` | only these keys may be mass assigned | allow-list | | `$guarded` / `#[Guarded([...])]` | every key except these may be mass assigned | deny-list | | `#[Unguarded]` or `$guarded = []` | every key may be mass assigned | no protection | Use one of the two lists per model. The allow-list is the safer habit because a column added later is closed until someone opens it. ## The defaults and the three outcomes A fresh model declares `$fillable = []` and `$guarded = ['*']`. That combination is **totally guarded**, and it changes how `fill()` reacts to a key it will not accept. For each key, `fill()` gives one of three outcomes: 1. **Accepted** — the key passes `isFillable()`, so `setAttribute()` runs. 2. **Exception** — the model is totally guarded (or strict discarding is on), so `fill()` throws `Illuminate\Database\Eloquent\MassAssignmentException` with the message "Add [guest_name] to fillable property to allow mass assignment on [App\Models\Reservation]." 3. **Silently dropped** — the model has a list, the key is not allowed, and nothing is thrown. The value simply never reaches the model. That third outcome surprises people: once `$fillable` names at least one column, an unlisted key disappears without a word. `Model::preventSilentlyDiscardingAttributes()` turns those silent drops into `MassAssignmentException` too, which is why the docs call it from `AppServiceProvider::boot()` for local development. ## How each list is evaluated - `$fillable` is checked first. When it is not empty, `fill()` intersects the incoming array with it, so only listed keys are even considered. - `$guarded = ['*']` blocks everything not listed as fillable. - A specific `$guarded` list blocks the named columns, case-insensitively, and also blocks any key that is not an actual column of the table (Eloquent reads the column listing from the schema once per model class, unless the key has a mutator or class cast). That stops keys such as `options->is_vip` from slipping past a deny-list. - Nested JSON keys such as `options->enabled` must be named in `$fillable`; the docs state they are not supported through `$guarded`. ## What it does not protect Mass-assignment protection only filters arrays. It never checks: - direct assignment, as in `$reservation->is_vip = true;` followed by `save()`; - `forceFill()` and `forceCreate()`, which exist to bypass the lists on purpose; - anything inside `Model::unguarded(fn () => ...)`, which disables the checks globally for the callback. `db:seed` runs seeders this way, so seeders can mass assign freely. ## Laravel 13 attributes Laravel 13 lets the same settings live in class attributes. The skeleton's `User` model now ships with `#[Fillable(['name', 'email', 'password'])]` instead of a `$fillable` property. `#[Fillable]` is merged into the `$fillable` array; `#[Guarded]` and `#[Unguarded]` only apply while `$guarded` still holds its default. Behaviour is the same as the properties. ## A worked reservation ```php #[Fillable(['guest_name', 'check_in', 'check_out'])] class Reservation extends Model {} $r = new Reservation([ 'guest_name' => 'Ana', 'check_in' => '2026-10-01', 'is_vip' => true, // not listed: silently dropped ]); ``` `$r->is_vip` is `null`; nothing was thrown because the model has a fillable list and strict discarding is off. ## Details that come up in follow-ups - **Pivot models** are the exception to the default: an `Illuminate\Database\Eloquent\Relations\Pivot` subclass starts with `$guarded = []`, because pivot rows are written by the relationship methods rather than from request arrays. - `isFillable('is_vip')`, `getFillable()` and `getGuarded()` let a test assert the model's policy directly, so a later edit that opens a sensitive column fails a test. - `totallyGuarded()` returns true exactly when the fillable list is empty and the guarded list is `['*']`; that is the state in which `fill()` throws rather than drops. - A key listed in both `$fillable` and `$guarded` is fillable, because the allow-list is consulted first, which is one more reason to keep a single list per model.

  • Why does a deny-list age worse than an allow-list on an Eloquent model?
    With `$guarded`, a column added in a later migration is mass assignable the moment it exists, so a new `is_vip` or `discount_pct` column is exposed unless someone remembers to add it to the list. With `$fillable`, the new column stays closed until it is deliberately opened. The failure mode of the allow-list is a dropped value; the failure mode of the deny-list is a privilege escalation.
  • How do you mass assign a nested JSON key such as `options->late_checkout` in Eloquent?
    Name the exact path in `$fillable` or `#[Fillable]`, for example `#[Fillable(['options->late_checkout'])]`. The docs state nested JSON updates are not supported through `$guarded`, and a specific guarded list rejects any key that is not a real column name, which `options->late_checkout` is not.
  • What does `Model::unguarded()` do, and where does Laravel itself use it?
    It switches off mass-assignment protection for every model while the given callback runs, then restores it. The `db:seed` command wraps seeders in it, which is why seeders can pass arbitrary arrays to `create()` without declaring fillable lists.

A hotel door working from a guest list admits only the names on it and quietly turns everyone else away; a door working from a banned list admits anyone not named, including people who arrived after the list was written. With no list at all, the default, the doorman refuses everyone and calls the manager.

saying these in an interview costs you the question

  • A model with no $fillable or $guarded accepts every attribute
  • Keys missing from $fillable always throw an exception
  • $fillable also blocks $model->column = value assignments
  • You should declare both $fillable and $guarded on each model
  • $guarded = [] is safe as long as the form only has safe fields