rfc:no_serialize_attribute

PHP RFC: Add #[NoSerialize] attribute for excluding properties or classes from serialization

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

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

Implement #[NoSerialize] attribute as outlined in the RFC?
Real name Yes No Abstain
Final result: 0 0 0
This poll has been closed.

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

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.
rfc/no_serialize_attribute.txt · Last modified: by dkulyk