Fixing 'Property Type Not Supported' in Livewire: A Guide to Custom Object Support with Synthesizers

I've been building an app that handles money, and if you've ever built an app that even remotely touches monetary values, you know you probably shouldn't roll your own implementation. Instead, use a battle-tested package.

For me, I’m a fan of Brick Money. It’s stable, well-documented, and a joy to use. So, naturally, I wrote a custom cast class to handle translating the object into and out of the database. That way, anywhere in the app, I can be sure the amount attribute is always a Money object.

Everything was going great — until I hit the admin panel.

I’m using Filament, which is powered by Livewire. And if you’ve worked with Livewire before, you might have run into this gem of an error: "Property type not supported in Livewire for property: X"

Yeah — Livewire doesn’t like custom objects (beyond a few blessed ones). The standard advice here is: “Use primitives.” So I’d need to create an accessor for the UI and a setter to handle input. But that approach has downsides:

  • I now have to maintain both a getter and a setter.
  • More importantly, in the context of this app, a Money object is a primitive. It represents a raw value, just wrapped safely.

Then I came across an answer that finally clicked for me. I don’t remember where, but someone said:

“Just use a synth.”

A what now?

Turns out, Livewire Synthesizers let you take full control of how Livewire hydrates (unserializes) and dehydrates (serializes) your custom objects.

In other words: Livewire says, “Look, I don’t know how to handle this object — but if you tell me how, I’ll gladly do it.”

So how do we write a Synth?

A Livewire synth has four parts:

  1. $key — A unique string that identifies this synth.
  2. match() — Livewire uses this to decide whether your synth should handle the given object.
  3. dehydrate() — Turns the object into a serializable array.
  4. hydrate() — Reverses the dehydration, turning the array back into your object.

Here’s an example synth for the Money object:

1<?php
2 
3namespace App\Livewire\Synthesizers;
4 
5use Brick\Money\Money;
6use Livewire\Mechanisms\HandleComponents\Synthesizers\Synth;
7 
8class MoneySynth extends Synth
9{
10 public static $key = 'money';
11 
12 public static function match($target)
13 {
14 return $target instanceof Money;
15 }
16 
17 public function dehydrate($target): array
18 {
19 // returns [value, meta]
20 return [
21 $target->getAmount()->__toString(),
22 [],
23 ];
24 }
25 
26 public function hydrate($value, $meta)
27 {
28 return money($value); // assumes you have a `money()` helper
29 }
30}

Then, register it in a service provider so Livewire knows about it:

1use Livewire\Livewire;
2use App\Livewire\Synthesizers\MoneySynth;
3 
4public function boot()
5{
6 Livewire::propertySynthesizer(MoneySynth::class);
7}

Now, whenever Livewire sees a Money object on a component, it knows exactly how to handle it — no more accessors, no more brittle workarounds.

Until the next one — keep it squeaky clean 🤙