PHP RFC: Add #[NoSerialize] attribute for excluding properties or classes from serialization
- Version: 0.7
- Date: 2025-10-06
- Author: Dmytro Kulyk, lnkvisitor.ts@gmail.com
- Status: Under Discussion
- Implementation: https://github.com/php/php-src/pull/20074
Introduction
Serialization is a fundamental PHP mechanism that allows objects to be converted into a storable or transferable representation.
However, not every property of an object should necessarily be serialized. Frameworks and libraries often contain transient or resource-based properties—such as database connections, file handles, or caches—that should not be persisted.
Currently, developers must manually handle this by overriding __sleep() or __serialize(), which can lead to repetitive boilerplate and maintenance overhead.
Proposal
This proposal introduces a new #[NoSerialize] attribute that can be applied to properties to exclude them from native PHP serialization or to classes to forbid serialization entirely.
It provides a declarative alternative to manually filtering properties within __sleep() or __serialize(), making serialization rules easier to maintain and more self-documenting.
1. Syntax and Definition
<?php use Attribute; /** * Marks a property or class as excluded from native serialization. * * When applied to a property, it will be skipped during serialize(). * When applied to a class, serialization will be forbidden. */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS)] final class NoSerialize {} ?>
Usage example:
<?php class Example { public string $name; #[NoSerialize] public PDO $connection; } $object = new Example(); $object->name = "User"; $object->connection = new PDO('sqlite::memory:'); echo serialize($object); // Serialized output will be `O:7:"Example":1:{s:4:"name";s:4:"User";}`. ?>
2. Semantics
2.1 Property-level Behavior
When the #[NoSerialize] attribute is applied to a property, it instructs the engine to skip that property during native serialization.
- When
serialize()is invoked and the class does not define its own__serialize()or__sleep(), properties marked with#[NoSerialize]are skipped automatically. - The resulting serialized data omits these properties entirely.
- Upon
unserialize(), all properties present in the serialized payload are restored normally. - The
#[NoSerialize]attribute does not affect deserialization — if a property exists in the serialized data (for example, from an older class version or custom serializer), it will still be deserialized. - Skipped properties (those not serialized) remain uninitialized if they have no default, or are restored to their declared default value.
class SessionWrapper { public string $id; #[NoSerialize] public $resource; // transient field public function __construct() { $this->resource = fopen('php://memory', 'r+'); } } $s = new SessionWrapper(); $s->id = 'abc'; var_dump(unserialize(serialize($s))); /* object(SessionWrapper)#2 (1) { ["id"]=> string(3) "abc" } */
Interaction with __serialize() and __sleep()
If a class defines its own serialization logic via __serialize() or __sleep(), the #[NoSerialize] attribute on properties has no effect.
These methods are entirely user-defined, and PHP does not automatically filter out properties or classes marked with #[NoSerialize].
This design maintains explicit and consistent behavior with existing PHP semantics — developer-defined serialization always takes precedence.
Example:
class Custom { public string $a = 'A'; #[NoSerialize] public string $b = 'B'; public function __serialize(): array { return ['a' => $this->a, 'b' => $this->b]; } } echo serialize(new Custom()); // Output still contains both 'a' and 'b'
Developers who wish to respect #[NoSerialize] inside __serialize() can do so manually via reflection:
class Custom { public string $a = 'A'; #[NoSerialize] public string $b = 'B'; public function __serialize(): array { $result = []; foreach ((new ReflectionObject($this))->getProperties() as $prop) { if (!$prop->getAttributes(NoSerialize::class)) { $result[$prop->getName()] = $prop->getValue($this); } } return $result; } }
Inheritance and Traits:
#[NoSerialize]applied to a property affects only that declaration and is not inherited if a subclass redeclares the property.- Properties introduced via traits preserve the attribute when composed into a class.
- Promoted constructor properties can include the attribute as usual.
class Example { public function __construct( public string $name, #[NoSerialize] public ?PDO $db = null ) {} }
2.2 Class-level Behavior
When the #[NoSerialize] attribute is applied to a class, any attempt to serialize an instance of that class will throw an exception, explicitly forbidding its serialization.
This behavior uses the same internal mechanism as for built-in non-serializable classes (such as Random\Engine\Secure or CurlHandle).
This ensures that invalid or unintended serialization attempts are immediately visible to developers and cannot result in partial or lossy data structures.
#[NoSerialize] class Connection { public PDO $pdo; public function __construct() { $this->pdo = new PDO('sqlite::memory:'); } } class Wrapper { public string $name = 'foo'; public Connection $conn; public function __construct() { $this->conn = new Connection(); } } $w = new Wrapper(); echo serialize($w); /* Fatal error: Cannot serialize instance of class Connection marked with #[NoSerialize] */
Notes:
- Class-level
#[NoSerialize]forbids serialization entirely by throwing a fatal error. - It can be used to mark classes that represent resources, handles, or runtime-only objects.
- This provides a clear and consistent failure mode, identical to the mechanism used for internal non-serializable classes.
Inheritance:
- Class-level
#[NoSerialize]is inherited by all child classes and cannot be “overridden”.
The prohibition is permanent (“sticky”) and automatically propagated throughout the inheritance chain.
- Applying
#[NoSerialize]again in a subclass when the parent already has it is a no-op (but allowed).
2.3 Interaction with other serialization forms
- The attribute affects only native PHP serialization (
serialize(),unserialize()).
Future changes extending the behavior of #[NoSerialize] to json_encode() or other formats would be backward-incompatible once this RFC is implemented.
For that reason, any future proposal in this direction would need to introduce a separate attribute, such as #[NoJsonEncode], to avoid ambiguity and preserve expected behavior for existing code.
2.4 Reflection API
The attribute is visible and queryable via reflection:
$rp = new ReflectionProperty(Example::class, 'connection'); var_dump($rp->getAttributes(NoSerialize::class)); // array(1) { ... }
2.5 Invalid Targets & Compile-Time Diagnostics
Applying #[NoSerialize] to unsupported targets results in compile-time diagnostics.
The engine validates the attribute’s target during class compilation and emits appropriate compile-time errors.
| Target | Severity | Message | Behavior |
|---|---|---|---|
| Static property | E_COMPILE_ERROR | `Cannot apply #[\NoSerialize] to static property %s::$%s` | Compilation aborted |
| Virtual property | E_COMPILE_ERROR | `Cannot apply #[\NoSerialize] to virtual property %s::$%s` | Compilation aborted |
| Interface | E_COMPILE_ERROR | `Cannot apply #[\NoSerialize] to interface %s` | Compilation aborted |
| Trait | E_COMPILE_ERROR | `Cannot apply #[\NoSerialize] to trait %s` | Compilation aborted |
Rationale:
- Static properties are class-level and not part of instance serialization.
- Virtual properties are engine-managed and not serialized by userland mechanisms.
- Interfaces and traits cannot be serialized or instantiated, so the attribute is invalid in those contexts.
3. Alternative names
| Proposed name | Notes |
|---|---|
| NoSerialize | Chosen for its clarity and consistency. Short, imperative, and self-explanatory. |
| SkipSerialize | Grammatically clear and intuitive; “skip” emphasizes runtime behavior rather than prohibition. Could be a valid alternative if “NoSerialize” is considered stylistically inconsistent. |
| SerializeIgnore | Mirrors conventions used in other languages and frameworks (e.g., @JsonIgnore in Java). However, it feels less idiomatic in PHP, which favors simple verb prefixes (No*, Allow*, etc.) over noun-based ones. |
| DoNotSerialize | Verbose but explicit. Deemed unnecessarily long for PHP attribute syntax. |
Backward Incompatible Changes
Defining a userland class named NoSerialize in the global namespace will no longer be possible, as this name becomes reserved for the new attribute.
A GitHub search for “class NoSerialize ” language:php returned 11 results, all defined within namespaces. Therefore, this change would not affect any known public codebases in practice, and the impact on backward compatibility is expected to be negligible.
Proposed PHP Version(s)
Next version of PHP (PHP 8.6 or PHP 9.0)
RFC Impact
To the Ecosystem
This RFC has no negative impact on existing code and benefits frameworks, static analyzers, and serializers that rely on native PHP serialization.
To Existing Extensions
None
To SAPIs
None
Open Issues
None currently.
Future Scope
- Allow
__sleep()to return null or no value, signaling the engine to fall back to the default serialization logic, which would then automatically respect#[NoSerialize].
Voting Choices
Patches and Tests
Implementation
After the RFC is implemented, this section should contain: - the version(s) it was merged into - a link to the git commit(s) - a link to the PHP manual entry for the feature
References
- Discussion: https://news-web.php.net/php.internals/128988
Rejected Features
- Class-level #[NoSerialize] with replacing value by NULL
Changelog
- v0.7 — Class-level behavior aligned with @not-serializable (now throws instead of serializing as NULL)
- v0.6 — Class-level #[NoSerialize] excluded from RFC
- v0.5 — Clarified JSON scope
- v0.4 — Restructured semantics into property/class sections, added proper compile-time diagnostics, and clarified deserialization behavior.