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

Serde Laravel Package

crell/serde

Crell/Serde is a fast, attribute-driven serialization and deserialization library for PHP. Map objects to JSON, YAML, CSV, and more with typed properties, nested structures, enums, and flexible field naming—no boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

Serde maps PHP objects to and from structured formats (JSON, arrays, and others) using attributes instead of hand-written toArray()/fromArray() code. For Laravel work its sweet spot is DTOs at the edges of your app: API request bodies, third-party API responses, JSON columns, queue payloads, config objects, and import/export files.

Install and sanity-check the environment

composer require crell/serde
php -v   # PHP 8.1+ required; the library leans on attributes, enums, readonly & promoted properties

Where to look first

  • Crell\Serde\SerdeCommon — the ready-to-use façade that wires in the default formatters and type handlers.
  • Crell\Serde\Attributes\* — Field, SequenceField, DictionaryField, DateTime, Flatten, Exclude, PostLoad, PreSerialize, ClassSettings.
  • Crell\Serde\Formatter\* — the format adapters (JsonFormatter, ArrayFormatter, …). Core ships JSON and a plain-PHP-array format; YAML/CSV/TOML style formats come from companion formatter packages.
  • Crell\Serde\TypeHandler — the extension point for your own value objects.

First use case: stop hand-mapping an API payload

use Crell\Serde\Attributes\Field;
use Crell\Serde\SerdeCommon;

final readonly class CreateOrder
{
    public function __construct(
        #[Field(rename: 'customer_id')]
        public string $customerId,
        public string $currency,
        #[Field(rename: 'line_items')]
        public array $lineItems,
        public ?string $notes = null,
    ) {}
}

$serde = new SerdeCommon();

$dto = $serde->deserialize($request->getContent(), from: 'json', to: CreateOrder::class);

$payload = $serde->serialize($dto, 'json');   // back out to a string
$array   = $serde->serialize($dto, 'array');  // plain PHP array, hand straight to response()->json()

Bind it once, inject it everywhere (app/Providers/AppServiceProvider.php):

public function register(): void
{
    $this->app->singleton(SerdeCommon::class, fn () => new SerdeCommon(
        // formatters: [...], typeHandlers: [...]  — see Gotchas
    ));
}

Then type-hint SerdeCommon in controllers, jobs, actions and console commands. It's stateless per call, so a singleton is fine.

Implementation Patterns

1. FormRequest → validated DTO

Keep Laravel's validator as the gatekeeper, then let Serde do the hydration. Don't ask Serde to validate; it only maps types.

final class StoreOrderRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'customer_id' => ['required', 'uuid'],
            'currency'    => ['required', 'string', 'size:3'],
            'line_items'  => ['required', 'array', 'min:1'],
            'line_items.*.sku' => ['required', 'string'],
            'line_items.*.qty' => ['required', 'integer', 'min:1'],
        ];
    }

    public function toDto(SerdeCommon $serde): CreateOrder
    {
        return $serde->deserialize(
            $this->validated(),
            from: 'array',
            to: CreateOrder::class,
        );
    }
}

Validating first, then deserializing, means a Serde exception genuinely means "our DTO doesn't match our rules" — a bug, not a 422.

2. Nested objects, lists and maps

Native property types are enough for scalars; collections of objects need explicit type hints, because Serde reads reflection types, not docblocks.

final readonly class Order
{
    public function __construct(
        public string $id,
        #[SequenceField(arrayType: LineItem::class)]   // JSON array of objects
        public array $items,
        #[DictionaryField(arrayType: Money::class)]    // JSON object → assoc array
        public array $totals,
        public Address $shipping,                      // nested object, inferred
    ) {}
}

3. Field shaping: rename, flatten, hide, dates

final readonly class User
{
    public function __construct(
        #[Field(rename: 'first_name')]
        public string $firstName,
        #[Exclude]
        public string $passwordHash,
        #[DateTime(format: 'Y-m-d')]
        public DateTimeImmutable $birthday,
        #[Flatten]
        public Address $address,   // address.street/city/zip promoted to the root object
    ) {}
}

#[Flatten] is the tidy way to match APIs (or legacy tables) that put nested data at the top level, without inventing a second DTO.

4. Eloquent JSON columns via a custom cast

This is the highest-leverage integration: one cast class and every json column becomes a typed object.

final class SerdeCast implements CastsAttributes
{
    public function __construct(
        private string $class,
        private string $format = 'json',
    ) {}

    public function get($model, $key, $value, $attributes): ?object
    {
        return $value === null ? null : app(SerdeCommon::class)
            ->deserialize($value, from: $this->format, to: $this->class);
    }

    public function set($model, $key, $value, $attributes): array
    {
        return [$key => $value === null ? null : app(SerdeCommon::class)
            ->serialize($value, $this->format)];
    }
}

// In the model:
protected function casts(): array
{
    return ['settings' => SerdeCast::class . ':' . OrderSettings::class];
}

Now $order->settings->notifyOnShip is a real property with autocompletion, and refactors are caught by static analysis instead of at runtime.

5. Invariants with #[PostLoad], derived data with #[PreSerialize]

#[PostLoad]
public function assertSane(): void
{
    if ($this->endsAt <= $this->startsAt) {
        throw ValidationException::withMessages(['ends_at' => 'Must be after start.']);
    }
}

#[PreSerialize]
public function computeTotals(): void
{
    $this->total = array_sum(array_map(fn ($i) => $i->subtotal, $this->items));
}

PostLoad is where you enforce domain rules that a flat validator can't express (cross-field, post-coercion), and it's the natural place to throw a ValidationException so Laravel renders a 422.

6. Queues and events: DTO in, DTO out

Jobs already serialize their public properties. Store a DTO (or its array form) rather than an Eloquent model, and rehydrate in handle():

final class SyncOrder implements ShouldQueue
{
    public function __construct(public readonly Order $order) {}

    public function handle(SerdeCommon $serde): void
    {
        Http::withBody($serde->serialize($this->order, 'json'), 'application/json')
            ->post(config('services.erp.url') . '/orders');
    }
}

This gives you a stable, versionable payload contract between the queue worker and the receiving system — much safer than relying on SerializesModels and a schema that drifts.

7. Custom TypeHandler for value objects

Money, Email, Ulid, PhoneNumber — write one handler per type, register it in the container binding, and every DTO in the app can use the value object natively instead of a string plus accessors. Same trick works for enums with non-trivial backing values.

8. Round-trip test discipline

Because serialization is symmetric, the cheapest test you can write is a round-trip:

$json = $serde->serialize($fixture, 'json');
$this->assertEquals($fixture, $serde->deserialize($json, from: 'json', to: $fixture::class));

Do this for every DTO and you catch missing attributes, wrong arrayTypes, and date-format drift before an integration test ever runs.

Gotchas and Tips

  • array is not a type. A bare public array $items will deserialize to whatever JSON gives you — nested payloads come back as arrays, not objects. Always annotate with #[SequenceField] or #[DictionaryField]. This is the single most common source of "why is my DTO full of arrays?" bugs.
  • Empty array vs. empty object. PHP can't distinguish [] from {}. An empty #[DictionaryField] map can serialize as [] and then fail to deserialize as an object. If the contract matters, test the empty case explicitly and consider a handler that forces stdClass/(object)[] on the way out.
  • Missing keys and defaults. A missing key falls back to the constructor default; a missing key with no default throws. Use explicit = null defaults for genuinely optional fields — that also documents intent better than the attribute list.
  • Scalar coercion is permissive. JSON "42" arriving into an int property, or 42 into a string, may be silently coerced. Combine declare(strict_types=1), precise Laravel validation rules, and a round-trip test for anything financially or security sensitive.
  • No constructor, no promoted properties → trouble. Serde is happiest with promoted-constructor readonly classes. Private/protected properties and classes that build state in a constructor body are where hydration gets surprising. Prefer a plain DTO at the boundary and map to your rich domain object in an action class.
  • Serialization is not validation. Serde validates types, not rules. Keep FormRequest/Validator in front of it, and translate Serde's exceptions into a 422 in a single place (e.g. Handler::render or a middleware) so a malformed webhook doesn't 500.
  • #[Flatten] collisions. Two flattened objects exposing the same key will collide silently. Keep flattened structures small and disjoint, and assert the shape in a test.
  • Timezone and format drift on dates. #[DateTime] applies both directions. Pin the format on both sides of an integration, and prefer DateTimeImmutable so a PostLoad hook can't mutate shared state.
  • Don't let Serde leak into views. Use it at the boundary; keep JsonResource/Blade for presentation. Mixing both gives you two serialization layers fighting over naming.
  • Never serialize Eloquent models with Serde. They're graphs with lazy loading, hidden attributes and relations. Convert to a DTO first — that's the whole point of the boundary.
  • Debugging: when a deserialize fails or misbehaves, serialize a hand-built instance to the array format and inspect the shape. It shows exactly the key names and nesting Serde expects, which is faster than reading stack traces.
  • Pin the version. The package is still pre-1.0 and attribute signatures/behaviour have shifted between releases; a composer.lock you don't casually bump is worth more here than usual.
  • Reflection cost. Serialization is reflection-heavy. It's fine for typical per-request DTO work, but don't use it inside a tight loop over thousands of rows — batch, or drop to plain arrays for bulk import paths.
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.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment