Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Serializer Laravel Package

ivory/serializer

ivory/serializer is a lightweight Laravel and PHP package for transforming models and data structures into consistent arrays or JSON. Define serialization rules to control output, simplify API responses, and keep data formatting predictable across your application.

View on GitHub
Deep Wiki
Context7

Getting Started

ivory/serializer is a standalone wrapper around the Symfony Serializer's encoder/decoder + normalizer stack, with one encoder class per format (JSON, XML, YAML, CSV) and a Format constants class. There is no Laravel package, service provider, facade, or config file — it is a library you wire yourself. Also note the docs are ambiguous about direction: the description advertises serialization (object → array/JSON), while most ecosystem discussion frames it as a deserializer. It is actually both — the Serializer exposes serialize() and deserialize() and delegates to the registered encoders/normalizers. Verify what your pinned tag exposes before designing around it.

Order of reading, before writing any code:

  1. composer.json — confirm the PHP constraint (7.0+) and the exact Symfony components pulled in (serializer, property-access, options-resolver, yaml). You pay for all of them even if you only use JSON.
  2. src/Serializer.php — the entire public surface. Expect serialize($data, $format, array $context = []) and deserialize($data, $type, $format, array $context = []).
  3. src/Format.php — the format constants you'll pass around.
  4. src/Encoder/ and src/Decoder/ — one class per format; this is where every format-specific quirk lives.
  5. src/Normalizer/ — how objects become arrays and back (property access, name conversion, date handling).

Freeze it before you depend on it. The repo is archived, has 3 stars and 0 dependents. Nothing will ever be patched, no PHP 8.2/8.3/8.4 fixes will land, and a transitive dependency break will be yours to fix. Use an exact constraint in composer.json, commit composer.lock, and record the decision in an ADR with an explicit exit criterion. If you are greenfield, stop and bind symfony/serializer directly — this library is convenience, not capability.

Minimal working setup:

use Ivory\Serializer\Serializer;
use Ivory\Serializer\Format;

$serializer = new Serializer(); // registers the default encoders/decoders

// object -> array/JSON
$payload = $serializer->serialize($user, Format::JSON);

// raw string -> hydrated DTO
$user = $serializer->deserialize($json, App\Dto\User::class, Format::JSON);

Bind it once in the container so you never new it in a controller:

// app/Providers/AppServiceProvider.php
$this->app->singleton(Serializer::class, fn () => new Serializer());

Better still, bind your own interface and let the package live behind it:

interface PayloadSerializer
{
    public function toArray(object $value, string $format): array|string;
    public function fromString(string $raw, string $type, string $format): object;
}

Your first use case should be the boring one: a webhook/API payload landing in a flat DTO, or an admin "export to CSV/JSON" button. If either works end-to-end on your target PHP version with representative production payloads, the rest of the library behaves predictably. If the spike fails on PHP 8.x, you have lost hours, not a sprint.

Implementation Patterns

Let the DTO shape do the work. Hydration goes through public properties and setters via Symfony's property access. A class whose constructor requires arguments with no no-arg path fails deep inside the normalizer with an unhelpful message. Keep import DTOs plain and mutable:

final class ContactRow
{
    public ?string $email = null;
    public ?string $name = null;
    public ?string $company = null;
    public ?string $createdAt = null;  // string; cast downstream
}

Register only the formats you actually consume. new Serializer() wires everything, which means a malformed CSV can only fail after the YAML parser has been autoloaded. If your pinned version allows explicit encoder construction, register JSON only, and the failure surface shrinks accordingly.

Normalize before hydration, not during. CSV is the acute case: every cell is a string, headers are the only key source, nesting is unrepresentable. Keep CSV DTOs flat and scalar, then run a single explicit post-hydration cast step ((int), filter_var(..., FILTER_VALIDATE_BOOLEAN), Carbon::parse). Doing type coercion inside setters scatters the rules across every DTO.

Use it as an export tool where it earns its keep. Laravel's API Resources are the right answer for JSON API responses — they're explicit and testable. The serializer earns its place when one model needs the same shape in four formats (a report download offered as CSV/JSON/XML, a data-room export). Define the projection once, emit it per format:

Route::get('/reports/{report}/export.{format}', function (Report $report, string $format) {
    $rows = $report->rows()->get()->map->toExportArray()->all();

    return response(
        app(Serializer::class)->serialize($rows, $format),
        200,
        ['Content-Type' => match ($format) {
            Format::CSV  => 'text/csv',
            Format::XML  => 'application/xml',
            default      => 'application/json',
        }]
    );
});

Snake_case vs camelCase. Wire a NameConverter (camelCase ↔ snake_case) into the normalizer once, in the container binding, and never again per-DTO. Doing it per-call via context arrays is how you end up with three different JSON key conventions in one app.

Validation belongs in a Form Request, not the DTO. Deserialize inside a try/catch, translate parser exceptions into a 422 with row/column context, and never let a raw Symfony exception reach the client:

try {
    $rows = $serializer->deserialize($file->getContent(), ContactRow::class, Format::CSV);
} catch (\Throwable $e) {
    throw ValidationException::withMessages(['file' => 'Unreadable CSV: '.$e->getMessage()]);
}

Push bulk work to a job. Request-time deserialization is fine to a few hundred KB. For real imports: store the upload, dispatch a job, stream the file, deserialize in chunks, and report per-row errors into a table the UI can poll. Do not pass the raw payload into the job — pass a storage path.

Contract-test each format you enable. One fixture file per format, in tests/Fixtures, asserting round-trip equality for a known DTO. When you eventually swap the package for symfony/serializer, those tests are the migration plan.

Gotchas and Tips

  • Archived means you own it. Expect no PHP 8.1+ deprecation fixes. Vendor the code into app/Support/Ivory (MIT permits it) or fork it internally rather than depending on an unreachable upstream. Either way, keep the package behind your own interface so the exit is a one-file change.
  • CSV is not a data format, it's a negotiation. Delimiter, enclosure, escape character, line endings, BOM, and encoding all vary per source. Detect or configure explicitly; never assume UTF-8 without a BOM check. Excel will mangle ;-delimited files on some locales.
  • CSV formula injection is a real security issue. Any export of user-supplied cells that starts with =, +, -, @, tab, or CR is executable in Excel/Sheets. Prefix with ' or wrap in quotes on export. The library will not do this for you.
  • XML: disable external entities. XXE and billion-laughs are the default risk with naive parsing. Pass libxml_disable_entity_loader/LIBXML_NONET at the decoder, or you have a file-disclosure and DoS vector on any upload endpoint. This is exactly the class of hardening an archived package will never receive.
  • YAML: turn off object instantiation. !php/object and unmarshalled custom tags can construct arbitrary classes from user input. Use Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE | Yaml::PARSE_CUSTOM_TAGS off-by-default, and never parse YAML from an untrusted source with object support on.
  • Empty string vs null vs missing key. Deserializers differ on whether "" becomes null, false, or stays "". Make the intent explicit in setters or the post-cast step; otherwise CSV and JSON paths silently diverge on the same field.
  • Dates are strings. There is no automatic DateTimeImmutable conversion out of the box. Either keep them as strings in the DTO and parse downstream, or register a custom normalizer. Mixed approaches produce Carbon vs string inconsistencies across importers.
  • Circular references throw. Eloquent models with bidirectional relations will blow the circular reference limit. Never pass a model straight in — pass an explicit DTO or array projection. Use the context circular_reference_limit only as a debugging aid, not a fix.
  • Eloquent models are a trap as serializer input. $hidden/$visible are not honored by the serializer; you will leak password, remember_token, or internal IDs. Always project through an explicit array/DTO first.
  • Debug decode and normalize separately. When hydration fails mysteriously, first get the raw decoded array (call the decoder alone or json_decode the input), then attempt normalization on that array. This tells you instantly whether the problem is the format or the class shape — the exception messages usually don't.
  • Extension points, if you must stay: custom Normalizer implementations for value objects, a NameConverter for key casing, and the context array for per-call behaviour. These are Symfony contracts, so anything you write is portable to symfony/serializer later — write them against Symfony's interfaces, not Ivory's, wherever possible.
  • Exit criterion up front: "Replace with symfony/serializer + league/csv once the import feature stabilises, or on the first PHP upgrade that breaks the package." Without that written down, an archived dependency becomes permanent by default.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource