Laravel 13's session config sets serialization to json; what does that change about stored values, and what happens when an upgraded app switches?
answer
- json_encode instead of serialize
- objects come back as arrays
- deserialization gadget chains
- framework fallback is still php
- switching logs every user out
basics
~20 sWith 'serialization' => 'json', the session payload is json_encode'd, so objects such as models or value objects come back as plain arrays. It removes PHP unserialize() from the session path; switching an existing app from php invalidates every active session.
solid answer
~40 sLaravel 13's skeleton `config/session.php` sets `'serialization' => 'json'`; when the key is absent, the session manager still falls back to `php`. With `json`, `Store::save()` writes `json_encode($attributes)` and loading uses `json_decode(..., true)`, so an Eloquent model, a Carbon date or an enum stored in the session comes back as an array or scalar, not an object. The benefit is security: nothing in the session path calls `unserialize()`, which closes off PHP deserialization gadget chains if the payload can be forged, for example after an `APP_KEY` leak with the cookie driver. When an upgraded app changes `php` to `json`, the old payloads fail to decode, load as empty sessions, and every user is logged out. The fix in code is to store IDs and scalars, not objects.
code
php · 19 lines<?php
use App\Enums\CoverLevel;
use App\Models\Vehicle;
use Illuminate\Http\Request;
public function storeVehicle(Request $request, Vehicle $vehicle)
{
// with 'serialization' => 'json' these come back as array / string / scalar:
// $request->session()->put('quote.vehicle', $vehicle);
// $request->session()->put('quote.cover', CoverLevel::Comprehensive);
$request->session()->put([
'quote.vehicle_id' => $vehicle->id, // reload with Vehicle::find()
'quote.cover' => CoverLevel::Comprehensive->value, // rebuild with CoverLevel::from()
]);
return redirect('/quote/drivers');
}go deeper
Know that the session can be stored as JSON and that objects then come back as arrays, so store IDs rather than models.
Explain how the Store encodes and decodes under each setting, including the errors bag special case and the php fallback.
Plan the php-to-json switch as a global logout, audit object usage first, and explain the gadget-chain risk that motivates it.
Weigh upgrade disruption against removing a deserialization attack path, and set a policy that session payloads hold only scalars.
## Two ways to turn a session into a string Every session driver stores a string. Laravel's `Illuminate\Session\Store` builds that string from the attribute array in one of two ways, chosen by the `serialization` option in `config/session.php`: | Setting | Write | Read | Objects | |---|---|---|---| | `php` | `serialize($attributes)` | `unserialize($data)` | restored as objects of their class | | `json` | `json_encode($attributes)` | `json_decode($data, true)` | come back as arrays or scalars | Laravel 13's skeleton ships `'serialization' => 'json'`. The session manager reads the option with a fallback of `php`, so an app whose `config/session.php` predates the option keeps PHP serialization until someone adds the key. ## Why the skeleton moved to JSON PHP's `unserialize()` instantiates whatever classes a payload names and runs their magic methods. If an attacker can control the serialized string, a chain of existing classes (a **gadget chain**) can be abused to run code. Session payloads are normally out of reach, but not always: - with the `cookie` driver, the payload travels in an encrypted cookie; anyone holding a leaked `APP_KEY` can forge one; - with a shared Redis or database, anyone with write access to the store can plant a payload. JSON decoding never instantiates classes, so it removes that path entirely. The upgrade guide frames it exactly this way, as protection against deserialization gadget chains, and the same Laravel 13 release hardens the cache with its `serializable_classes` option. ## What changes for your code In the insurance quote wizard, compare what comes back on the next request: 1. `put('quote.vehicle', $vehicleModel)` with `json`: the model is JSON-encoded through its array form, so `get('quote.vehicle')` returns an **array**, and calling `->save()` on it fails. 2. `put('quote.start_date', now())`: a Carbon instance becomes an ISO-8601 **string**. 3. `put('quote.cover', CoverLevel::Comprehensive)` with a backed enum: it becomes the backing **value**. 4. Scalars and arrays of scalars round-trip unchanged. Laravel handles its own special case: the validation `errors` bag is converted to a plain structure before encoding and rebuilt after decoding, so `$errors` in Blade keeps working. The robust pattern works with either setting: - store **identifiers** (`quote.vehicle_id`) and reload models; - store dates as strings you parse deliberately; - store enum **values** and rebuild with `CoverLevel::from()`. ## Upgrading an existing app An app upgraded from an earlier release typically has `serialization` missing or set to `php`. If you sync `config/session.php` with the Laravel 13 skeleton: 1. every existing payload is a PHP-serialized string; 2. `json_decode()` cannot parse it, the store falls back to an empty array; 3. users lose their login and anything else in the session, and get a fresh CSRF token. The upgrade guide therefore recommends keeping `php` for a seamless upgrade, or switching to `json` when the app stores no PHP objects in the session and a one-off re-authentication is acceptable. A sensible rollout: - audit `put()` calls for objects first; - deploy the switch at a quiet time, since it logs everyone out; - do not flip it back and forth, as each switch repeats the logout. ## Interview angle A strong answer names both halves: JSON removes an unserialize-based attack path and makes stored objects come back as arrays, and switching an existing app costs one global logout. It also notes that the risk JSON removes only matters when the payload can be tampered with, which is why the cookie driver and a leaked `APP_KEY` are the canonical example. ## Checklist before flipping the switch 1. Search the codebase for `session()->put(`, `Session::put(` and `->with(` calls that pass objects. 2. Replace models with IDs, dates with strings, enums with their values. 3. Run the feature tests with `'serialization' => 'json'` so any array-instead-of-object bug fails in CI. 4. Announce or schedule the deploy, because every user will be signed out once.
- Why does switching an existing app from php to json session serialization log everyone out?Existing payloads were written with `serialize()`. After the switch, the store calls `json_decode()`, which cannot parse them, so it falls back to an empty attribute array. The authentication data and CSRF token in the old payload are ignored, and every user starts a fresh, anonymous session.
- Does json serialization matter if you use the database driver and APP_KEY has never leaked?It matters less, because an attacker would need write access to the sessions table to plant a payload. It is still defence in depth: it removes `unserialize()` from a path that runs on every request, and it pushes the code toward storing IDs and scalars, which are smaller and survive class changes between deploys.
saying these in an interview costs you the question
- JSON serialization keeps Eloquent models as models in the session
- Switching from php to json is transparent to logged-in users
- JSON session serialization encrypts the payload
- Every Laravel 13 app uses json even without the config key