zumba/json-serializer
A PHP library that serializes variables and objects to JSON and restores them, including binary data, nested objects, recursion, and closures. Requires PHP 7.2+; never unserialize untrusted input.
Installation
composer require zumba/json-serializer:^3.2.5
The package is auto-discoverable in Laravel 5.5+ (no manual provider registration needed).
Upgrade note (3.2.5): If you pinned
^3.2.4and run PHP 7.2 or 7.3, upgrade immediately — 3.2.4 shipped aReflectionProperty::isInitialized()call that is a fatal error below PHP 7.4. On PHP 8.5, 3.2.4 also emits deprecations fromSplObjectStorage::contains()/attach()andReflectionProperty::setAccessible(). Both are resolved in 3.2.5, which is now CI-tested on PHP 7.2 through 8.5.
Verify your runtime matrix
php -v
composer why-not zumba/json-serializer 3.2.5
Check that your composer.json require.php constraint and your Docker/CI images actually line up with what the package supports. The 3.2.4 → 3.2.5 regression is exactly the class of bug that only shows up when your production PHP differs from your dev PHP.
Basic Usage
use Zumba\JsonSerializer\JsonSerializer;
$serializer = app(JsonSerializer::class);
$data = ['name' => 'John', 'age' => 30, 'active' => true];
// Serialize
$json = $serializer->serialize($data);
// '{"name":"John","age":30,"active":true}'
// Unserialize
$decoded = $serializer->unserialize($json);
// ['name' => 'John', 'age' => 30, 'active' => true]
First Use Case
public string $name) serialize to null instead of fataling — on PHP 7.4+ only, which 3.2.4 failed to check for.SECURITY.md before you ever point unserialize() at request data.Object Serialization (Including Parent/Child Class Properties)
class ParentClass {
private $secret = 'hidden';
}
class ChildClass extends ParentClass {
public $name = 'Alice';
private $privateData = 'child_secret';
}
$child = new ChildClass();
$json = $serializer->serialize($child);
// Now includes private properties from parent AND child classes
Typed Properties in DTOs and Events
class User {
public string $name; // uninitialized → null in JSON
public int $age;
public ?DateTime $createdAt;
}
$json = $serializer->serialize(new User());
// '{"name":null,"age":null,"createdAt":null}'
This is the pattern that broke on PHP 7.2/7.3 in 3.2.4 and is fixed in 3.2.5. If any of your services still run 7.2/7.3, treat 3.2.4 as a broken release, not just an older one.
Queue Job Payloads
class RebuildReport implements ShouldQueue
{
use SerializesModels;
public function __construct(public ReportDefinition $definition) {}
public function handle(JsonSerializer $serializer): void
{
$cached = $serializer->serialize($this->definition);
Cache::put("report_def:{$this->definition->id}", $cached, now()->addDay());
}
}
Serializing once at dispatch/queue time is far cheaper than re-serializing on every read.
Custom Handlers for Special Types
$serializer->setDateTimeHandler(fn ($date) => $date->format('Y-m-d H:i:s'));
$serializer->setObjectHandler('Carbon\Carbon', fn ($carbon) => $carbon->toIso8601String());
$serializer->setCircularReferenceHandler(
fn ($object, $path) => "[Circular Reference: {$path}]"
);
Register handlers in a service provider binding so every consumer gets identical formatting — date format drift between services is the most common source of "it deserialized but the data is wrong".
Integration with Laravel
public function toArray($request)
{
return $this->serializer->serialize($this->resource->toArray(), [
'exclude' => ['password', 'api_token'],
]);
}
public function handle($request, Closure $next)
{
$validator = Validator::make($request->all(), ['data' => ['required', 'json']]);
$validator->validate();
$request->merge($this->serializer->unserialize($request->input('data')));
return $next($request);
}
Batch Processing
$jsonArray = User::query()
->cursor()
->map(fn ($user) => $serializer->serialize($user))
->all();
cursor() over all() keeps memory flat; the serializer itself has no streaming mode, so chunking is your responsibility.
Closures (opis/closure v4)
$serializer->setObjectHandler('Closure', fn ($closure) => 'Closure detected');
PHP version drift between 3.2.4 and 3.2.5 The 3.2.5 changelog is essentially "3.2.4 was only safe on PHP 7.4–8.4". If you deploy a single lockfile across a mixed fleet (e.g. an old queue worker container on PHP 7.3 alongside PHP 8.3 web nodes), standardize on 3.2.5 everywhere before merging the upgrade PR.
PHP 8.5 deprecation noise
3.2.5 silences SplObjectStorage::contains()/attach() and ReflectionProperty::setAccessible() deprecations. If your error tracker pages on deprecations, upgrade before turning on 8.5 in staging — otherwise you will get alerts that look like new bugs.
Circular references
Default behavior returns a placeholder ([Circular Reference: path]) rather than throwing. Customize via setCircularReferenceHandler — and log the path in non-production so you can find the offending relation.
Private/protected properties Serialized by default, including inherited ones. Never assume "private" means "not in the payload" when handing a JSON string to a client or a log sink:
$json = $serializer->serialize($object, ['exclude' => ['secret', 'password']]);
Build an allowlist-based exclude in a base DTO rather than relying on per-call discipline.
Uninitialized typed properties
Now null instead of a fatal error. If null in a non-nullable property is worse for you than an exception, enforce it at your own boundary (validation rule or a strict mode flag if your version exposes one) rather than trusting the serializer to catch it.
Performance with large data
$cached = cache()->remember("user_{$user->id}_serialized", now()->addHour(), function () use ($user, $serializer) {
return $serializer->serialize($user);
});
Never serialize inside a Blade loop or an N+1 accessor — it re-reflects the whole object graph each call.
Security
unserialize() on request input is a deserialization gadget surface. Treat it like eval(): only decode payloads your own application produced, and prefer HMAC-signed payloads or a whitelist of allowed classes if the API supports one.
CI coverage, not feature coverage The 3.2.5 headline is that CI now runs on every supported PHP version, 7.2 → 8.5. That is a real improvement, but it also means 3.2.4's bugs were matrix bugs. Mirror the same matrix in your own pipeline (or at least in your dependency-bump job) so the next reflection-based change gets caught by your CI instead of your users.
docker run --rm -v $(pwd):/app -w /app php:7.3-cli composer test. A ReflectionProperty fatal in 3.2.4 looks nothing like the deprecation in 8.5.$json = $serializer->serialize($data);
if (json_validate($json)) { /* safe to store/return */ }
$serializer->setCircularReferenceHandler(function ($object, $path) {
Log::debug("Circular reference detected at: {$path}");
return '[Circular]';
});
assertEquals($original, $serializer->unserialize($serializer->serialize($original))) catches private-state and type loss that eyeballing JSON never will.Custom serializers
Implement the package's serializer contract for domain-specific logic (money, enums, IDs) and bind it in a service provider so the rest of the app never calls serialize() directly.
Object handlers
setObjectHandler() per class is the cheapest place to normalize value objects before they hit the wire:
$serializer->setObjectHandler(Money::class, fn (Money $m) => [
'amount' => $m->getAmount(),
'currency' => $m->getCurrency()->getCode(),
]);
Closure handlers
Register once at boot for Closure if your graph can contain them; otherwise the reflective walk will surprise you at runtime.
Container binding
$this->app->singleton(JsonSerializer::class, function () {
$serializer = new JsonSerializer();
$serializer->setDateTimeHandler(fn ($d) => $d->format(DATE_ATOM));
return $serializer;
});
A singleton with handlers pre-registered beats scattering setObjectHandler across controllers.
^3.2.5 in composer.json, and treat any 3.2.x bump as a release that needs the matrix test to pass.Response:
return response()->json($serializer->unserialize($cached), 200, [], JSON_UNESCAPED_UNICODE);
Note response()->json() encodes arrays natively — don't double-encode by passing an already-serialized string unless a consumer demands a string body.null, false, NAN, INF, resources, and uninitialized typed properties. Run the suite on your lowest supported PHP minor, not just the newest.How can I help you explore Laravel packages today?