Skip to content
LaravelBlog

livewire/livewire: Preserve structured strings in URL properties

LaravelBlogBot
LaravelBlogBot

Contributed by calebporzio

Part of the livewire v4.4.7 release

<!-- categories: 3 (fix), 11 (upgrade notes) -->

If you are upgrading from Livewire v3.8.10 to v4, pull request #10617 is one of those fixes you will likely never see working — because it prevents a crash. URL-bound properties no longer decode strings that merely look like JSON. Visiting a page with ?search=[123] used to hydrate a string property as the array [123], breaking pages — most visibly in Filament — with an Array to string conversion error. In v4, structured-looking text stays text, while genuine bracket-notation arrays and ordinary scalar coercion behave exactly as before.

Why ?search=[123] broke pages

PHP's query-string parser already distinguishes a scalar value from an array: ?search=[123] is a scalar, ?search[0]=123 is an array. Livewire's hydration layer, however, ran every scalar value through json_decode() before assigning it to the component, so the text [123] was decoded into a real PHP array on the way in.

Consider a typical URL-bound property:

class ProductTable extends Component
{
    #[Url]
    public string $search = '';

    public function render()
    {
        return view('products.table', ['results' => Product::search($this->search)->get()]);
    }
}

Visiting /products?search=[123] — for example, because a user pasted that into a search box that syncs to the URL:

  • v3.8.10: $search hydrates as the array [123]. Rendering it — or Filament reading it as a search term — fails with Array to string conversion, and the page fails to load.
  • v4 with this PR: $search hydrates as the literal string '[123]', and the page loads normally.

The bug was surfaced through Filament (#20372) and first addressed by an alternative fix (#10602); #10617 is the reworked implementation that landed.

The fix: check the property type before decoding

Instead of trusting json_decode() unconditionally, BaseUrl now only intervenes where it matters: when a raw scalar decodes into an array, it asks whether the target property actually wants that array. It resolves the property — including nested form-object properties — and compares both candidates against the declared type. If the type rejects the array and accepts the string (a string property, or an uninitialized ?string), the original text is preserved. If the property is untyped, the code falls back to the type of the value it already holds: a default of '' keeps strings, a default of [] still accepts the decoded array.

Side by side, the behavior you get after upgrading:

Query string Property v3.8.10 v4
?search=[123] public string $search = '' [123] (array, crash) '[123]'
?search[0]=123 public array $search = [] [123] [123]
?page=2 public int $page = 1 2 2

A reusable type-matching utility

The decision is not buried inside the query-string feature. It lives in a new documented utility, Utils::propertyTypeMatchesValue(), which performs a strict, non-coercing comparison between a value's native PHP type and a property's declared type:

Utils::propertyTypeMatchesValue($component, 'search', '[123]');  // true  (string property)
Utils::propertyTypeMatchesValue($component, 'search', [123]);    // false
Utils::propertyTypeMatchesValue($component, 'filters', '[123]'); // null  (untyped property)

It returns true when the value satisfies the type, false when it doesn't, and null when there is no type contract — a missing or untyped property — so callers decide the fallback instead of the utility guessing. It understands named scalars, mixed, nullable types, unions such as string|array, intersections such as Countable&Stringable, classes, interfaces, and self/static/parent. The policy ships in the internal Livewire\Drawer namespace with a dedicated test matrix covering each combination.

What still decodes exactly as before

Only structured-looking strings on string-capable properties are preserved; everything else keeps its existing behavior:

  • Bracket-notation arrays: ?search[0]=123 and ?filters[status]=active still hydrate as arrays, and partially present arrays still merge into existing values.
  • Opt-in structured decoding: declare a union type and JSON-shaped values still decode:
#[Url]
public string|array $filters;

With ?filters={"status":"active"}, this hydrates as ['status' => 'active'], just like before.

  • Scalar coercion: ?page=2 still lands as int on an int property; number and boolean coercion, plus the preservation of oversized integers as strings, are untouched.

Upgrade impact from v3.8.10

  • Crashes disappear. Pages that failed on Array to string conversion when a visitor's query value looked like JSON now load, and the value stays the string your property declared.
  • Audit accidental array hydration. If any code relied on ?x=[1,2] arriving as an array on a string-typed or string-initialized property, that no longer happens. Declare the property string|array to keep decoding, or json_decode() the string yourself.
  • Untyped properties follow their current value. public $search = '' keeps strings; public $search = [] keeps accepting arrays.
  • No migration needed. There are no API changes; #[Url], legacy queryString() methods, and all existing attributes keep working. The change is covered by the unit suite (1,404 tests / 3,985 assertions) and the query-string browser suite (36 tests / 288 assertions).

Takeaways

  • Structured-looking strings such as [123] or {"id":1} now stay strings on string-capable #[Url] properties, fixing Array to string conversion crashes, including Filament's.
  • Bracket-notation arrays, scalar number/boolean coercion, partial array merging, and big-integer handling are unchanged.
  • Union types like string|array let a property opt back into structured decoding.
  • The strict matcher Utils::propertyTypeMatchesValue() centralizes the policy and returns null for untyped or missing properties instead of guessing.
  • Upgrading from v3.8.10 requires no code changes, but audit anything that depended on JSON-looking query values becoming arrays on string properties.

Sources

More from this release

Related Articles