opensoft/simple-serializer
opensoft/simple-serializer is a lightweight PHP serializer for turning objects and Eloquent models into arrays or JSON. Define attributes once and produce consistent, predictable API payloads with minimal boilerplate and configuration.
Install and confirm the wiring
composer require opensoft/simple-serializer
Laravel package discovery should register the service provider automatically. If you're on an older Laravel or discovery is disabled, add it to config/app.php:
Opensoft\SimpleSerializer\SimpleSerializerServiceProvider::class,
Check whether the package ships a config file and publish it:
php artisan vendor:publish --provider="Opensoft\SimpleSerializer\SimpleSerializerServiceProvider"
Where to look first
With 19 stars and sparse docs, treat vendor/opensoft/simple-serializer/src/ as the specification. In order:
Serializer class/contract — the actual public methods (serialize, deserialize, and any normalizer/handler interfaces).composer.json — confirm the illuminate/support constraint matches your Laravel version.First use case: a DTO boundary for an API endpoint
use Opensoft\SimpleSerializer\Serializer;
class UserDto
{
public function __construct(
public int $id,
public string $name,
public string $email,
) {}
}
// In a controller or action
public function show(int $id, Serializer $serializer)
{
$dto = new UserDto($id, 'Ada', '[email protected]');
return response()->json($serializer->serialize($dto));
}
That's the whole value proposition: replace hand-rolled toArray() methods and ad-hoc json_decode mapping with one symmetric pair of calls. Confirm the exact binding name in the provider before injecting — it may be an interface rather than the concrete class.
Keep DTOs as the serialization boundary. The biggest day-to-day win is not serializing Eloquent models directly. Map models into plain DTOs, then let the serializer handle the array/JSON shape. This keeps lazy-loading, hidden attributes, and relation recursion out of the serialization layer.
final class OrderDto
{
/** @param LineItemDto[] $items */
public function __construct(
public string $number,
public \DateTimeImmutable $placedAt,
public array $items,
) {}
}
Use it symmetrically for input. The same DTO can hydrate from a validated request payload:
$data = $request->validate([
'number' => ['required', 'string'],
'placed_at' => ['required', 'date'],
'items' => ['required', 'array'],
]);
$dto = $serializer->deserialize($data, OrderDto::class);
Validate before deserializing, not after — a serializer is a mapper, not a validator.
Nested objects and collections. If the serializer is reflection/type-driven, typed properties (LineItemDto[] via docblock or a typed collection) usually drive recursion. Test one level of nesting early; if nested hydration isn't automatic, map collections explicitly:
$items = array_map(
fn (array $row) => $serializer->deserialize($row, LineItemDto::class),
$data['items']
);
Normalize naming once. If the package supports a naming strategy (snake_case out, camelCase properties), set it centrally in config rather than sprinkling toArray() transforms in controllers. Same for date formats — pick ISO-8601 and enforce it in one place.
Wrap it in a thin application service. Don't call the serializer from twenty controllers. A single ApiPresenter or Serializer wrapper gives you one place to swap strategies, add logging, or migrate off the package later:
final class ApiPresenter
{
public function __construct(private Serializer $serializer) {}
public function present(object $dto): array
{
return $this->serializer->serialize($dto);
}
}
Test round-trips, not shapes. A one-line test per DTO (deserialize(serialize($dto)) == $dto) catches property-name drift, date-format mismatches, and null handling far more cheaply than asserting full JSON fixtures.
Feature tests for the endpoint contract. Keep one HTTP-level test per endpoint asserting the JSON structure, so a serializer upgrade that changes null/omitted-field behavior fails loudly.
This is a small package — pin and read it. 19 stars means limited community coverage. Pin an exact version in composer.json, and read the source before relying on a behavior. Budget an hour; the class count is likely small.
Untyped properties often don't hydrate. Reflection-based serializers typically need typed properties or constructor promotion to know what to build. A public $items; property may silently stay null. Convert DTOs to typed properties first when debugging "why is this field empty."
Constructors may be bypassed. Many simple serializers instantiate without calling __construct (via ReflectionClass::newInstanceWithoutConstructor). That means constructor invariants and default-value logic won't run on deserialize. Validate and normalize explicitly after hydration.
Circular references will blow the stack. If you serialize an Eloquent model with loaded relations, expect infinite recursion or a memory error. Serialize DTOs, or ensure relations are converted to arrays before they reach the serializer.
null vs. missing key. Test both directions. Some implementations omit null properties, others emit "field": null. Pick one and lock it with a test — clients will depend on it. Watch isset() vs property_exists() in any custom handler you write.
Dates and enums are the usual failure point. A DateTimeImmutable property round-tripping through a string needs a matching parse format on the way back. Backed enums generally need their ->value, not the case object. Handle both explicitly in a normalizer rather than hoping the serializer guesses.
Metadata/reflection caching. If the package caches class metadata, adding a property during development may not take effect until the cache clears. Check for a cache driver in config and clear it (or disable caching locally) when a field "mysteriously" doesn't appear. Also re-run php artisan config:cache after changing serializer config in production.
Extension points to look for. The typical seams in a package like this are a normalizer/handler interface registered in config or the container, and possibly attributes for renaming fields or excluding properties. Use those before forking — a custom normalizer for Money or Uuid value objects is usually a 20-line class.
Debugging workflow. When output looks wrong, isolate the layer: dump the DTO (dd($dto)), then dd($serializer->serialize($dto)). If serialization is fine but JSON is wrong, the problem is in your response layer, not the package. Catch and log the package's serialization exception type explicitly rather than letting a generic \Exception bubble up as a 500.
Have an exit plan. For a low-traffic package, keep serialization calls behind your own wrapper (see above) so swapping to Symfony Serializer or Laravel API Resources later is a one-file change, not a codebase-wide find-and-replace.
How can I help you explore Laravel packages today?