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

jms/serializer

jms/serializer is a powerful PHP library for (de)serializing complex data to and from JSON and XML. It supports dates, intervals, Doctrine ORM integration, versioning for APIs, circular references, exclusion strategies, and configuration via XML, YAML, or annotations.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm you're on 3.x. composer require jms/serializer:^3.13 (3.x is the only supported line; 1.x had an authorization-bypass CVE). There is no first-party Laravel service provider — this is a framework-agnostic library, so plan to wire it yourself. symfony/expression-language is only needed if you want #[Exclude(if: ...)] / #[VirtualProperty(exp: ...)], and doctrine/orm only if you want the Doctrine object constructor.

Where to look first. Skip the README (it's a donation banner plus a link). Go straight to:

  • SerializerBuilder — the single entry point for configuring visitors, handlers, naming strategies, cache, and debug mode.
  • JMS\Serializer\Annotation\* — the metadata classes. In 3.x these same classes are PHP 8 attributes, and attributes are enabled by default on PHP 8+ (since 3.14), so you do not need doctrine/annotations or an AnnotationReader for new code.
  • JMS\Serializer\Context / SerializationContext / DeserializationContext — where groups, versions, formats, and null-handling live.
  • The docs at jmsyst.com/libs/serializer for the type grammar (array<string, Foo>, DateTimeImmutable<'Y-m-d'>, Iterator<Foo>), which is the part people get wrong most often.

Minimal wiring in Laravel. Build once, bind as a singleton, inject SerializerInterface:

// AppServiceProvider::register()
$this->app->singleton(SerializerInterface::class, function () {
    return SerializerBuilder::create()
        ->setCacheDir(storage_path('framework/serializer'))
        ->setDebug(config('app.debug'))
        ->addDefaultHandlers()
        ->addDefaultListeners()
        ->setPropertyNamingStrategy(new CamelCaseNamingStrategy())
        ->build();
});

Make sure storage/framework/serializer exists and is writable — the builder will not create it for you.

First real use case. Don't start by pointing it at Eloquent models. Write one response DTO with attributes and round-trip it:

final class OrderDto
{
    #[Groups(['list', 'detail'])]
    public int $id;

    #[Groups(['detail'])]
    #[Type('array<App\Dto\OrderLineDto>')]
    public array $lines = [];

    #[Groups(['detail'])]
    #[Type("DateTimeImmutable<'Y-m-d\TH:i:sP'>")]
    public \DateTimeImmutable $placedAt;
}

$json = $serializer->serialize($dto, 'json', SerializationContext::create()->setGroups(['detail']));
$dto  = $serializer->deserialize($json, OrderDto::class, 'json');

That gives you the whole mental model: metadata drives output, Context scopes it, the type grammar drives casting.

Implementation Patterns

DTO boundary, not entity serialization. The highest-value pattern is mapping entities → explicit DTOs and letting JMS own the DTO layer. Pointing it at Eloquent models works until you hit lazy-load N+1s, belongsTo cycles, and hidden/visible mismatches with Laravel's own $hidden/$casts/toArray() conventions — which JMS ignores entirely. DTOs also make #[Groups], #[Since]/#[Until], and #[SerializedName] meaningful.

Groups for endpoint shapes. Treat groups as your API surfaces: list vs detail vs admin. Pass via SerializationContext::create()->setGroups([...]). Remember nested objects only serialize if their properties belong to a group in the active set — a common "field silently disappeared" cause.

Versioning. Combine #[Since('3.0')] / #[Until('4.0')] with ->setVersion('3.2') on the context. This is the cleanest way to evolve a versioned API without duplicating DTO classes, and it's a feature Laravel's API Resources has no equivalent for.

Custom handlers for value objects. Anything that isn't scalar/date/array (money, UUIDs, enums with metadata, geo points) belongs in a SubscribingHandlerInterface:

public static function getSubscribingMethods(): array
{
    return [[
        'direction' => GraphNavigatorInterface::DIRECTION_BOTH,
        'format'    => 'json',
        'type'      => Money::class,
        'method'    => 'serializeMoney', // or serialize/deserialize pair
    ]];
}

Register with ->addDefaultHandlers() (auto-discovers) or ->configureHandlers(fn ($h) => $h->register(...)). Handlers receive the Context, so you can propagate groups downward.

Object construction on deserialize. By default JMS tries new + setters. For constructor-promoted readonly DTOs, either register #[PostDeserialize]/#[PreSerialize] callbacks or implement a custom ObjectConstructor and ->setObjectConstructor(...). Doctrine users get DoctrineObjectConstructor for free, which will fetch entities by identifier instead of building new ones — powerful, but it means a deserialization can silently hit the database.

XML when you need it. Same metadata, different format string. Add #[XmlRoot('order')], #[XmlAttribute], #[XmlList(inline: true, entry: 'line')], #[XmlKeyValuePairs]. Attributes must be scalar — arrays there will throw. XML is where JMS genuinely beats most PHP serializers, so if you have SOAP/legacy feeds this is the reason to pick it.

Caching in production. Metadata parsing is the expensive part. Use ->setCacheDir(), ->setDebug(false), and — since 3.13 dropped doctrine/cache — a PSR-6 PsrCachedReader backed by a Laravel/Symfony cache adapter if you want a shared pool. In dev keep setDebug(true) so metadata changes are picked up without a manual cache purge.

Testing. Assert on serialized JSON snapshots rather than object identity; it catches accidental metadata changes (a renamed property, a lost group) that unit tests on DTOs won't. Keep a fixture per API version.

Gotchas and Tips

The cache directory is the #1 "it works locally" failure. setCacheDir() needs an existing, writable path, and production metadata caches go stale after a deploy that renames properties. Clear it in your deploy script, or key the cache by a build/release identifier. Debug mode disables the cache entirely — never leave it on in prod.

JSON object keys are always strings. array<int, Foo> serializes to {"0": ...} and type information on array keys is ignored during JSON serialization (a long-standing documented limitation). If key types matter to your consumers, reshape into a list of {key, value} DTOs.

Nulls and empties are three separate switches. setSerializeNull(true) on the context emits nulls; ->setSerializeNull()/skip_null_values at the builder changes defaults; #[SkipWhenEmpty] is different again — and its interaction with #[Exclude] has produced surprising output (see issue #1240). Decide the policy once, encode it in one place.

Expression-based exclusion is expensive and needs the extra package. #[Exclude(if: '...')] and #[VirtualProperty(exp: '...')] evaluate Symfony ExpressionLanguage per property. Without symfony/expression-language installed you get a confusing runtime failure rather than a composer error. Class-level conditional excludes only started merging correctly across hierarchies in 3.7 — pin >=3.7 if you rely on them.

Deserialization type selection is code-controlled, but polymorphic fields aren't. #[DiscriminatorMap] lets an incoming payload pick the concrete class. Restrict the map to a known set and validate the result before persisting — never derive the target Type from user input. Use 3.x; the 1.x authorization bypass is why.

Exceptions inside handlers were swallowed before 3.6. If you're debugging "my handler returns null instead of throwing," you're on an old version. Similarly, strict_types casting failures were only properly handled from 3.9 — bump before you spend a day on TypeErrors.

DateTimeInterface typehints deserialize to DateTime, not DateTimeImmutable (behavior tightened in 3.7). If you typehint the interface but need immutability, declare the concrete class in #[Type]. Also set an explicit format in the type string — the default will surprise you across timezones.

Iterables and Laravel collections. Illuminate\Support\Collection implements Traversable, so it serializes, but generics inside it are invisible to reflection — you must annotate #[Type('Iterator<App\Dto\Item>')] on the property or items come out as raw arrays.

Property access quirks. By default JMS uses reflection plus getter/setter detection; private properties without accessors still serialize. #[Accessor] overrides this, and #[ReadOnly] blocks hydration. If a property "won't deserialize," check for a #[ReadOnly], a missing no-arg constructor path, or a naming-strategy mismatch — the CamelCaseNamingStrategy changes the wire name, so #[SerializedName] must match what clients actually send.

Under Octane, keep the serializer as a singleton but never mutate it. The Serializer is reusable; Context objects are the per-call mutable state. Rebuilding the serializer per request throws away the metadata cache each time and will dominate your profile.

Extension points worth knowing about, in order of usefulness: custom Handlers → custom ObjectConstructor → custom NamingStrategy → custom SerializationVisitor (only if you need a new output format; JSON and XML are the only shipped ones).

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.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr