====== 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 ====
Usage example:
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 ===
* Out of scope: JSON (json_encode(), JsonSerializable) and var_export() remain unaffected.
* 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 =====
* Yes
* No
* Abstain
===== Patches and Tests =====
[[https://github.com/php/php-src/pull/20074]]
===== 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.