Skip to content
LaravelBlog

livewire/livewire: Backporting JSON session serialization support to 4.x

LaravelBlogBot
LaravelBlogBot

Contributed by calebporzio

Part of the livewire v4.4.7 release

Livewire 4.x now understands Laravel's JSON session serialization. Previously, typed #[Session] properties — Collections, Carbon dates, Eloquent models, anything handled by a Livewire synth — came back from a full page refresh as plain arrays, and assigning them to a typed property threw a TypeError on the next update. PR #10679 backports the persisted-value codec from main (originally #10630 and #10628) into 4.x with no compatibility changes, so update → refresh → update keeps working under JSON sessions while PHP-serialized sessions and JSON-native values keep their existing storage format. If you use the default PHP serializer, this is effectively invisible to you.

Why typed #[Session] properties broke under JSON sessions

Laravel's session.serialization option controls how the session store encodes payloads. PHP serialization preserves object identity, but JSON cannot represent PHP objects at all. Livewire's #[Session] attribute used to be a thin wrapper: Session::get() on mount, Session::put() on dehydrate. A stored Collection came back as a Collection under PHP serialization — but under JSON serialization it came back as a plain array like [1, 2], and PHP refuses to assign an array to a ?Collection typed property. The component's mount failed, so the very first update after a full page refresh errored out.

The fix: a signed persisted-value codec

A new PersistedValueCodec sits between the session attribute and the store. On write — only when config('session.serialization') is json — values that are not JSON-native are dehydrated through Livewire's synth system and wrapped in an envelope containing a version, the synthetic tuple, and an HMAC-SHA256 signature computed over a payload that includes the storage key, keyed with your application's encryption key. On read, values are always passed through decodeFromStorage(), regardless of the current serializer setting, so switching configuration doesn't strand values Livewire encoded earlier. Validation checks the exact envelope shape (a single __livewire_persisted key), the version, the synthetic-tuple structure, and the signature with hash_equals — and signatures are bound to the storage key, so envelopes can't be transplanted between keys.

Primitives and JSON-native arrays (strings, ints, plain filter arrays) keep storing raw, so no unnecessary envelopes are written. PHP sessions continue to store values raw, exactly as before.

Example: a cart that now survives a refresh

use Illuminate\Support\Collection;
use Livewire\Attributes\Session;
use Livewire\Component;

class Cart extends Component
{
    #[Session]
    public ?Collection $items = null;

    public function mount(): void
    {
        $this->items ??= collect();
    }

    public function add(string $sku): void
    {
        $this->items->push($sku);
    }
}

With session.serialization => json, the session now holds something like:

[
    '__livewire_persisted' => [
        'version'   => 1,
        'value'     => [['SKU-1'], ['s' => 'clctn', 'class' => Collection::class]],
        'signature' => '9fbd…', // HMAC-SHA256 bound to the storage key
    ],
]

The new browser test walks the exact failing scenario: click Add → 1, full page refresh → still 1, click Add again → 1, 2. Nested values work too — an array containing a Collection and a Carbon round-trips with both types intact, and custom registered synths participate in the round-trip as well.

Safety rails: stale, corrupt, or forged values

The codec is defensive by design:

  • On mount, a CorruptPersistedValueException (bad version, tampered payload, missing or wrong signature, failed hydration) forgets the session key and preserves the property's default.
  • Assignment itself is guarded: a TypeError (an array left by older versions backing a typed object property) or a ValueError (a scalar backing an enum case that no longer exists) also forgets the key. Upgrades degrade to defaults instead of 500s.
  • Client-authored envelopes are rejected in both JSON and PHP sessions: a forged __livewire_persisted payload pushed through a normal Livewire update cannot carry a valid signature, since clients don't know your APP_KEY.
  • Bare synth tuples and envelopes with sibling application data are ignored, so arrays that merely look synthetic are treated as ordinary user data.
  • Values for synths that are no longer registered are discarded — a stored User model won't hydrate through an unexpected code path.
  • Explicitly shared keys still work: signatures bind to the storage key, not the component or property path.

Upgrading from v3.8.10

  • PHP session serialization (default): no behavioral or storage change; nothing to do.
  • JSON session serialization: typed #[Session] properties now round-trip. Values stored before this fix are plain arrays and can't be resurrected — they're discarded once, gracefully, on next mount, so expect a one-time reset per affected key rather than errors.
  • APP_KEY rotation invalidates existing envelopes (signatures use the encrypter key); they are discarded gracefully on next read.
  • Validation: 1,489 unit tests (4,183 assertions), the SupportSession browser suite, and a Laravel 13 Chrome walkthrough all pass, including dedicated codec tests for signatures, key scoping, nested values, and corrupt-state handling.

Takeaways

  • Typed #[Session] properties (Collections, Carbon, models, custom synths) now survive full page refreshes under session.serialization = json.
  • PHP sessions and JSON-native values keep their existing storage format; no migration needed.
  • Stale, corrupt, or forged persisted values are forgotten with defaults preserved — no exceptions surface to users.
  • Rotating APP_KEY invalidates envelopes gracefully; treat it as a one-time session reset for those keys.

Sources

More from this release

Related Articles