Nothing exposes an API’s design debt faster than its response bodies. A user endpoint returns the password hash because nobody configured exclusion groups. A mobile client receives 1.2 MB of nested ORM entities because the serializer walked every lazy-loaded relation. A partner integration breaks because a field changed from "published_at" to "publishedAt" after a refactor. Serialization is where your domain model meets the outside world, and in PHP it is almost always an afterthought until the first incident.
Four libraries dominate PHP serialization in 2026, and they are not interchangeable: Symfony Serializer 8.1.7, JMS Serializer 3.32.9, League Fractal 0.21, and Spatie Laravel Data 4.23.0. Each is the right answer to a different question, and picking the wrong one means fighting your own tooling for a year.
TL;DR — Quick Verdict
Choose Symfony Serializer if you are on Symfony or want a framework-agnostic component with attribute-driven groups, normalizers you can compose, and the strongest metadata caching story. Choose JMS Serializer if you need response versioning as a first-class feature (@Since/@Until) across many API consumers, or you maintain a legacy bundle that already depends on it. Choose Fraktal-style transform pipelines (League Fractal) when your output shape is a product decision — transformers give you explicit, testable control over every field and native sparse-fieldset includes. Choose Spatie Laravel Data if you are on Laravel and want DTOs that validate, cast, and serialise without writing a transformer class per resource.
The mistake to avoid: adopting three of them in one codebase. Pick one serialization strategy, keep it at the edge of the application, and never let ORM entities escape a controller.
Side-by-Side Comparison: PHP Serialization Libraries in 2026
| Dimension | Symfony Serializer | JMS Serializer | League Fractal | Spatie Laravel Data |
|---|---|---|---|---|
| Version | 8.1.7 | 3.32.9 | 0.21 | 4.23.0 |
| Install | composer require symfony/serializer | composer require jms/serializer | composer require league/fractal | composer require spatie/laravel-data |
| Framework coupling | None (used by Symfony) | None | None | Laravel |
| GitHub repository | symfony/serializer | schmittjoh/serializer | thephpleague/fractal | spatie/laravel-data |
| Stars | 2,535 | 2,342 | 3,544 | 1,793 |
| Last commit | 2026-09-16 | 2026-09-04 | 2025-12-16 | 2026-09-01 |
| License | MIT | MIT | MIT | MIT |
| Configuration style | PHP attributes (#[Groups]) | Attributes / annotations | PHP transformer classes | PHP attributes + constructor |
| Response versioning | Manual (groups per version) | Built in (Since/Until) | Manual (transformer per version) | Manual (data classes per version) |
| Sparse fieldsets / includes | Partial (via groups) | Partial | Native (availableIncludes) | Partial (#[Lazy], partials) |
| Validation integration | Symfony Validator | Symfony Validator | None | Built in |
| Metadata caching | Tag-aware PSR-6 cache | File cache | Compiled transformers | Laravel cache |
| Best fit | Symfony apps, JSON API edges | Multi-version partner APIs | Public APIs with negotiated shapes | Laravel apps wanting DTOs |
One observation from the live data: Symfony Serializer had commits within two days of writing this, and Spatie Laravel Data within three weeks, while Fractal’s last commit was in December 2025. Fractal is stable rather than abandoned — 3,544 stars and a frozen, well-understood API — but if you are choosing for a greenfield project in 2026, factor that cadence difference into the decision.
Decision Matrix: Pick in Ten Seconds
| Your situation | Choose | Why |
|---|---|---|
| Symfony application, REST endpoints | Symfony Serializer | Attribute groups, autowiring, metadata cache built in |
| Partner API with v1/v2 consumers | JMS Serializer | @Since/@Until plus setVersion() handles it declaratively |
| Public API with client-selected fields | League Fractal | ?include=author,comments maps directly to includes |
| Laravel app with form-to-DTO flows | Spatie Laravel Data | Validation, casts, and serialisation in one class |
| Legacy codebase on Doctrine annotations | JMS Serializer | Mature, widely documented, minimal migration |
| You need full control of every response field | League Fractal | Transformers are explicit and trivially unit-testable |
| Plain-PHP microservice, no framework | Symfony Serializer | Standalone component, no container required |
Symfony Serializer 8.1.7 — The Composable Default
Symfony Serializer separates two concerns that other libraries blur: normalizers convert objects to arrays and back, encoders convert arrays to formats such as JSON, XML, CSV, and YAML. You compose them, which means the same object graph can become JSON for a mobile client and CSV for a finance export without a second library.
| |
| |
Serialising with groups is a single call, and the same object produces two different payloads without touching the model:
| |
The Groups attribute is the security boundary most teams learn about the hard way: any property without a group is invisible when groups are active, which is exactly what you want for internalNotes above. Deserialisation respects the same metadata, so $serializer->deserialize($json, Article::class, 'json') validates the shape while building the object.

JMS Serializer 3.32.9 — Versioning as a First-Class Concern
JMS Serializer predates attributes by a decade and is still the strongest choice when a single API must serve multiple contract versions simultaneously. Its Since and Until annotations let you express “this field appeared in 1.2 and disappeared in 2.0” directly on the property, and the serialization context selects the version at runtime.
| |
| |
| |
ExclusionPolicy('all') inverts the default so that nothing is serialised unless explicitly exposed — a small annotation that prevents an entire category of data-leak bugs. JMS also supports PostDeserialize and PreSerialize visitors, which are the right place to hide fields conditionally (for example, omitting an author’s email unless the requester owns the record).
League Fractal 0.21 — Transformers and Sparse Fieldsets
Fractal takes a different stance: your API’s shape is a product surface, so it deserves its own class. A transformer is pure PHP — a transform() method returning an array — which makes every response field explicit, greppable, and unit-testable without booting a framework.
| |
| |
| |
parseIncludes() is where Fractal earns its keep: ?include=author,comments becomes real eager-loading hints in a repository, so the client’s requested shape drives exactly one query instead of a cascade of lazy loads. Deriving computed fields like reading_time inside the transformer also keeps presentation logic out of the entity, which is the difference between a model you can refactor and one you cannot.
Spatie Laravel Data 4.23.0 — DTOs That Do Everything
If you work in Laravel, Spatie’s Laravel Data collapses three separate concerns — validation, casting, and serialisation — into a single data class. You construct it from a request or a model, and it knows how to render itself.
| |
| |
| |
Attribute mapping means your API’s snake_case contract and your DTO’s camelCase properties coexist without a transformer. Because the data class is also a validation target, the same rules serve both inbound requests and documentation generators, which removes the drift that makes hand-written API docs wrong within a month.
Real-World Patterns and Pitfalls
Never unserialize() untrusted input. PHP’s native serialize()/unserialize() pair can instantiate arbitrary classes and is a well-known object-injection vector. All four libraries above serialise to JSON (or XML) for a reason: the wire format carries data, not class identities.
Exclude by default, expose explicitly. Whether you use #[Groups], ExclusionPolicy('all'), or a transformer, the fail-safe direction is “nothing leaves unless declared.” The alternative — blacklist patterns that hide password and internalNotes — fails the moment someone adds a secretToken field on a Friday afternoon.
Handlers for circular references. Bidirectional ORM relations will recurse forever. Symfony Serializer accepts a circular_reference_handler callback, JMS has a CircularReferenceHandler service, and Fractal simply cannot recurse because transformers are explicit. Choose the mechanism before your first nested relation ships.
Serialize DTOs, not Doctrine proxies. Normalising an entity directly drags in proxy classes, triggers lazy loads you did not intend, and couples your JSON contract to your database schema. Build a DTO in the controller or a dedicated assembler, then serialise that.
Cache your metadata in production. Reading attributes on every request is real CPU. Symfony Serializer’s CacheClassMetadataFactory, JMS’s setCacheDir(), and Laravel’s config cache all remove that cost — but only if you configure them and, crucially, clear them on deploy.
Pin your date format. DateTimeImmutable serialised without an explicit format produces different strings across configurations. Pick ISO 8601 with an offset (or DATE_ATOM), document it, and assert it in a test.
Watch memory on large collections. Serialising 50,000 records into a single JSON array will exhaust memory_limit long before it finishes. Stream the response, paginate, or use a generator with a JSON streaming encoder.
Related Reading for PHP Teams
Serialisation rarely stands alone. If your API accepts user input, pair it with the PHP validation libraries comparison so inbound payloads are rejected before they reach your DTOs. When your service calls other services, the PHP HTTP client comparison covers the client side of the same contract, including how response deserialisation is wired. And if your serialization configuration lives in a container — as it does in every Symfony or Laravel application — the PHP dependency injection containers guide explains how those services are assembled and cached.
FAQ
Which PHP serialization library should I use in 2026? Use Symfony Serializer for Symfony applications and framework-agnostic components, JMS Serializer when you need built-in response versioning for multiple API consumers, League Fractal when explicit transformers and includes are the priority, and Spatie Laravel Data inside Laravel applications that want DTOs with validation included.
Does JMS Serializer still make sense when Symfony Serializer exists?
Yes, in two cases: APIs that must serve several contract versions at once, where @Since and @Until with setVersion() are cleaner than duplicating attribute groups, and legacy codebases already built on JMS annotations. For a fresh Symfony project, Symfony Serializer is the better default.
How do I stop sensitive fields from leaking into API responses?
Use exclusion-by-default. Set ExclusionPolicy('all') in JMS, rely on #[Groups] in Symfony (ungrouped properties are omitted when groups are active), or return explicit arrays from a Fractal transformer. Blacklists that list fields to hide will eventually miss one.
Is League Fractal abandoned? No. Version 0.21 is the current release and the API has been stable for years; the repository last received a commit in December 2025 and carries 3,544 stars. Treat it as a mature, frozen library rather than an abandoned one, and note that its transformer approach means less churn than annotation-based alternatives.
Can I use two serialization libraries in the same project? You can, and many mature applications do when they migrate gradually, but each additional library adds metadata caches, attribute dialects, and onboarding cost. If you must run two temporarily, isolate them behind a single assembler interface so the rest of the codebase never knows which one produced the payload.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com