In an Eloquent model, how do $hidden, $visible and $appends shape toArray() and JSON output, and what is Laravel 13's attribute form?
answer
- denylist versus allowlist
- appends needs a matching accessor
- makeVisible / makeHidden per instance
- #[Hidden], #[Visible], #[Appends]
- only output, never the query
basics
~20 s$hidden removes attributes and relations from toArray() and JSON, $visible keeps only those listed, and $appends adds accessor values that have no column. Laravel 13 also offers #[Hidden], #[Visible] and #[Appends] class attributes; none of them changes what is queried.
solid answer
~40 s`toArray()` (and `toJson()`, and returning a model from a route) serializes the model's attributes after casts and accessors, plus its loaded relations. `$hidden = ['stripe_customer_id']` is a denylist: those keys are dropped, and a relation name can be listed too. `$visible` is an allowlist: only the listed keys survive. An accessor whose name has no column is **not** included until you list it in `$appends`, and appended values still respect hidden and visible. Per instance, `makeVisible()`, `makeHidden()`, `append()` and `setAppends()` adjust the lists. In Laravel 13 the same lists can be declared as class attributes, `#[Hidden([...])]`, `#[Visible([...])]` and `#[Appends([...])]`, as the skeleton's `User` does with `#[Hidden(['password', 'remember_token'])]`. These settings shape output only: the columns are still selected and available as properties in PHP.
go deeper
Recall that $hidden drops keys from JSON, $visible keeps only listed keys, and $appends adds computed accessors.
Explain what toArray() assembles, snake_case append names, per-instance makeVisible() and the Laravel 13 attribute form.
Treat hidden as an output filter rather than a security boundary, watch appended accessors that query, and spot leaks through other paths.
Decide where response shaping belongs, model-wide lists versus per-endpoint transformers, for consistent contracts across clients.
## What toArray() includes Eloquent models serialize themselves. `$invoice->toArray()` returns an array, `$invoice->toJson()` a JSON string, and a model or collection returned from a route or controller becomes a JSON response the same way. The array is built from: 1. every attribute in the model's raw attributes, after **casts** and after **accessors** that match a column; 2. every **loaded** relation, recursively serialized; 3. any **appended** computed attributes; minus whatever `hidden` removes or `visible` does not allow. Dates are serialized as UTC ISO-8601 strings unless a cast format or `serializeDate()` override says otherwise. ## The three lists | Setting | Kind | Effect on output | |---|---|---| | `$hidden` / `#[Hidden]` | denylist | listed attributes and relations are dropped | | `$visible` / `#[Visible]` | allowlist | only listed keys are kept | | `$appends` / `#[Appends]` | additions | accessor values with no column are added | For a subscription invoice: ```php #[Hidden(['stripe_customer_id', 'internal_notes'])] #[Appends(['is_overdue'])] class Invoice extends Model { protected function isOverdue(): Attribute { return Attribute::get(fn () => $this->status !== InvoiceStatus::Paid && $this->due_at?->isPast()); } } ``` The appended name is the **snake_case** serialized key (`is_overdue`) even though the method is camelCase. Appended values also respect `hidden` and `visible`. ## Laravel 13's attribute form Laravel 13 added class attributes for these lists, and the skeleton's `User` model uses `#[Fillable([...])]` and `#[Hidden(['password', 'remember_token'])]` instead of properties. When the model initialises, Eloquent merges the attribute's columns into the hidden, visible and appends lists, so the property form keeps working and the two can coexist. ## Per-instance and per-collection changes - `$invoice->makeVisible('internal_notes')` exposes a normally hidden key for this instance; `makeHidden()` does the reverse, and `makeVisibleIf()` / `makeHiddenIf()` take a condition. - `setVisible()` and `setHidden()` replace the lists outright; `mergeVisible()` and `mergeHidden()` add to them. - `append('is_overdue')`, `mergeAppends([...])`, `setAppends([...])` and `withoutAppends()` control computed keys at runtime. - Eloquent collections forward `makeVisible()` and `makeHidden()` to every model. ## What these settings do not do - **They do not change the query.** Hidden columns are still selected and still readable as `$invoice->stripe_customer_id` in PHP; hide is not a security boundary for code that reads properties directly. - **They do not stop leaks through other paths.** A Blade view printing a property, a log line with the raw attributes, or a manual array built by hand bypasses them. - **Appends cost work.** Every appended accessor runs for every serialized model; appending one that reads a relation can trigger a query per model. ## A worked output For an invoice with `stripe_customer_id` hidden, `is_overdue` appended, `amount` cast to `decimal:2`, `status` cast to an enum, `due_at` cast to `datetime` and the `customer` relation loaded, `toJson()` produces something like: ```json {"id": 42, "amount": "19.90", "status": "open", "due_at": "2026-10-01T00:00:00.000000Z", "is_overdue": true, "customer": {"id": 7, "name": "Ada"}} ``` Notice what the settings did: the Stripe ID is absent, the amount is a string because of the decimal cast, the enum became its backing value, the date became UTC ISO-8601, and the computed flag appeared only because it was appended. The relation appears because it was loaded, not because of any list. ## When to reach for something else The hidden and visible lists are a model-wide default. When different endpoints need different shapes — an admin view with internal notes, a customer view without them — a dedicated transformation layer (API resources) expresses that per response instead of mutating the model's lists.
- Why does an accessor like isOverdue() not appear in JSON by default?Serialization starts from the model's raw attributes and applies accessors only to keys that exist as columns. A computed accessor has no column, so it is skipped until its snake_case name is listed in `$appends` or `#[Appends]`, or added per instance with `append('is_overdue')`.
- Does listing a column in $hidden stop it being loaded from the database?No. Hidden only filters `toArray()` and JSON output. The column is still selected, still in the model's attributes and readable as a property. To avoid loading it, select specific columns in the query; to keep it from a response, hidden or a response transformer is the right tool.
saying these in an interview costs you the question
- Believing $hidden excludes the column from the SELECT
- Expecting a computed accessor in JSON without appending it
- Listing the camelCase method name in $appends
- Thinking appended attributes ignore $hidden and $visible
- Claiming Laravel 13 removed the $hidden property