Hello internals,
I’d like to open a discussion about a new proposal introducing the
#[NoSerialize] attribute, which allows developers to explicitly
exclude properties — or even entire classes — from native PHP
serialization.
RFC: https://wiki.php.net/rfc/no_serialize_attribute
Implementation: https://github.com/php/php-src/pull/20074
The primary goal of my series of proposals is to gradually reduce the
responsibilities of __sleep(), which today is used both to filter
which properties should be serialized, and to perform pre-serialization
logic.
Before proposing significant behavioral changes to __sleep(), we first
need to introduce clear, attribute-based alternatives for its common
use cases.
This RFC represents one of these preparatory steps.
It introduces an explicit, engine-level mechanism (#[NoSerialize]) to
declare which properties or classes should be skipped during
serialization — without requiring __sleep() to handle that
responsibility.
Please also consider the related RFCs (to be discussed separately):
- https://wiki.php.net/rfc/sleep_without_return_array
- https://wiki.php.net/rfc/not_serializable_attribute
Together, these proposals aim to clarify and simplify PHP’s object
serialization model by moving away from overloaded magic methods
toward more explicit, declarative mechanisms.
Best regards,
Dmytro Kulyk
Hi Dmytro
I’d like to open a discussion about a new proposal introducing the
#[NoSerialize] attribute, which allows developers to explicitly
exclude properties — or even entire classes — from native PHP
serialization.RFC: https://wiki.php.net/rfc/no_serialize_attribute
Implementation: https://github.com/php/php-src/pull/20074
Thank you for your proposal. I have a few comments.
When applied to a class, instances will be serialized as NULL.
I don't understand the rationale for diverging from the existing
@not-serializable behavior of internal classes, which throw when
attempted to be serialized (e.g. Random\Engine\Secure). I see there's
also a separate RFC to introduce that behavior:
https://wiki.php.net/rfc/not_serializable_attribute I don't think
there's a need for both of these attributes. To demonstrate, how would
we even decide whether an internal class like PDO should get the
#[NoSerialize] or #[NotSerializable] attribute? Informing the user of
potentially incorrect serialization is the prudent option, and if they
would like to skip the serialization of a property containing a PDO
object they can simply mark it as #[NoSerialize].
The RFC says:
This approach ensures that data structures containing such objects (for example, arrays, collections, or parent objects) remain valid and can be safely unserialized without errors, while clearly indicating that the value was intentionally omitted.
But is replacing unserializable objects with NULL in nested arrays
really the safe choice? In these cases, I feel like a custom
serializer that consciously replaces the object is warranted. This
might also cause problems for typed properties:
#[NoSerialize]
class PDO {}
class Foo {
/* Will be happily serialized as NULL, but can't be restored because the
* property is not nullable. */
public PDO $connection;
}
To summarize, I'd prefer if #[NoSerialize] on classes would cause
serialize() to throw, like we already do for @not-serializable.
Class-level #[NoSerialize] is inherited by child classes unless explicitly overridden.
Can you clarify how this would work? How can you override this
attribute by omission?
#[NoSerialize]
class Foo {}
/* What do I add here to remove #[NoSerialize]? */
class Bar extends Foo {}
Out of scope: JSON (json_encode(), JsonSerializable) and
var_export()remain unaffected.
Any future attempt to exclude properties marked as #[NoSerialize] from
json_encode() would be backwards incompatible after this RFC has been
implemented. To avoid this BC break, json_encode() would need a
separate attribute (e.g. #[NoJsonEncode]). That sounds reasonable, but
should be spelled out in the RFC.
Invalid Targets & Compile-Time Diagnostics
What's the rationale for warning for some, but erroring for others?
Ilija
Thanks for the feedback!
Hi Dmytro
I’d like to open a discussion about a new proposal introducing the
#[NoSerialize] attribute, which allows developers to explicitly
exclude properties — or even entire classes — from native PHP
serialization.RFC: https://wiki.php.net/rfc/no_serialize_attribute
Implementation: https://github.com/php/php-src/pull/20074Thank you for your proposal. I have a few comments.
When applied to a class, instances will be serialized as NULL.
I don't understand the rationale for diverging from the existing
@not-serializable behavior of internal classes, which throw when
attempted to be serialized (e.g. Random\Engine\Secure). I see there's
also a separate RFC to introduce that behavior:
https://wiki.php.net/rfc/not_serializable_attribute I don't think
there's a need for both of these attributes. To demonstrate, how would
we even decide whether an internal class like PDO should get the
#[NoSerialize] or #[NotSerializable] attribute? Informing the user of
potentially incorrect serialization is the prudent option, and if they
would like to skip the serialization of a property containing a PDO
object they can simply mark it as #[NoSerialize].The RFC says:
This approach ensures that data structures containing such objects (for example, arrays, collections, or parent objects) remain valid and can be safely unserialized without errors, while clearly indicating that the value was intentionally omitted.
But is replacing unserializable objects with
NULLin nested arrays
really the safe choice? In these cases, I feel like a custom
serializer that consciously replaces the object is warranted. This
might also cause problems for typed properties:#[NoSerialize] class PDO {} class Foo { /* Will be happily serialized as NULL, but can't be restored because the * property is not nullable. */ public PDO $connection; }To summarize, I'd prefer if #[NoSerialize] on classes would cause
serialize()to throw, like we already do for @not-serializable.
You’re absolutely right that the current internal behavior (e.g.
Random\Engine\Secure, CurlHandle, etc.) throws when serialization is
attempted, and that the proposed #[NotSerializable] RFC formalizes
this for userland classes.
The intent behind #[NoSerialize] is not to diverge from that model,
but to provide a softer, complementary mechanism targeting a different
use case — non-critical, transient classes that can safely degrade to
NULL without breaking surrounding data structures.
In this example, PDO (and other resource-backed classes) should indeed
use #[NotSerializable], because serialization is semantically invalid
and must throw.
#[NoSerialize], on the other hand, would apply to wrappers,
containers, or domain objects where serialization of the rest of the
structure should continue even if one field or nested object cannot be
meaningfully serialized.
Class-level #[NoSerialize] is inherited by child classes unless explicitly overridden.
Can you clarify how this would work? How can you override this
attribute by omission?#[NoSerialize] class Foo {} /* What do I add here to remove #[NoSerialize]? */ class Bar extends Foo {}
Class-level #[NoSerialize] is transparently inherited by child classes.
There’s currently no way to override or cancel this behavior in
descendants; the attribute remains effective throughout the
inheritance chain.
As an alternative, #[NotSerializable] could be extended to
#[NotSerializable(bool $soft = false)], which would provide the same
behavior while keeping it in a separate, more consistent attribute —
avoiding the double semantics currently implied by #[NoSerialize].
Out of scope: JSON (json_encode(), JsonSerializable) and
var_export()remain unaffected.Any future attempt to exclude properties marked as #[NoSerialize] from
json_encode()would be backwards incompatible after this RFC has been
implemented. To avoid this BC break,json_encode()would need a
separate attribute (e.g. #[NoJsonEncode]). That sounds reasonable, but
should be spelled out in the RFC.
It has been added to section 2.5
Invalid Targets & Compile-Time Diagnostics
What's the rationale for warning for some, but erroring for others?
Initially, the idea was to make all of these cases emit warnings, but
I didn’t figure out how to implement that consistently within the
attribute validators.
At this point, I don’t see any practical reason to allow the attribute
on unsupported targets, so all such cases will be changed to
compile-time errors instead of warnings.
Dmytro
Hi
Thank you for your RFC. It is really well-written and nicely explains
the various (edge) cases. I've only read through the #[\NoSerialize]
one, since it is the topic of this RFC discussion. I agree with Ilija on
all points.
Am 2025-10-29 15:35, schrieb Dmytro Kulyk:
#[NoSerialize], on the other hand, would apply to wrappers,
containers, or domain objects where serialization of the rest of the
structure should continue even if one field or nested object cannot be
meaningfully serialized.
But even for wrappers you can't guarantee that the wrapper is stored in
a nullable property. When applying the attribute to a field, omitting
the field makes sense, since the author of the property is the same
person who is capable of adding the attribute and thus is in full
control. This is different for classes where the author of the class
doesn't know how and where the class is being used. I thus agree with
Ilija that this should throw to clearly indicate that the user of the
class is doing something incorrect and to not silently corrupt data. If
an author of a class needs more fine-grained control, they can either
add the attribute to individual properties themselves - or implement
__serialize().
Class-level #[NoSerialize] is inherited by child classes unless explicitly overridden.
Can you clarify how this would work? How can you override this
attribute by omission?#[NoSerialize] class Foo {} /* What do I add here to remove #[NoSerialize]? */ class Bar extends Foo {}Class-level #[NoSerialize] is transparently inherited by child classes.
There’s currently no way to override or cancel this behavior in
descendants; the attribute remains effective throughout the
inheritance chain.
Indeed, however the “unless explicitly overridden” bit in the RFC text
implies that this is possible.
As an alternative, #[NotSerializable] could be extended to
#[NotSerializable(bool $soft = false)], which would provide the same
behavior while keeping it in a separate, more consistent attribute —
avoiding the double semantics currently implied by #[NoSerialize].
I don't think that sub classes should be able to make a non-serializable
parent class serializable, since they clearly won't be able to correctly
restore the state of the parent class. In fact I would consider this an
improvement over the existing serialization hooks where the child class
can simply skip calling the parent's serializer and thus create a
situation that the parent class does not expect to occur.
Initially, the idea was to make all of these cases emit warnings, but
I didn’t figure out how to implement that consistently within the
attribute validators.
At this point, I don’t see any practical reason to allow the attribute
on unsupported targets, so all such cases will be changed to
compile-time errors instead of warnings.
Yes, every clear error that is detectable at compile-time should be a
hard error to make it easy for users to detect the mistake.
Best regards
Tim Düsterhus
Thanks for the feedback.
Thank you for your RFC. It is really well-written and nicely explains
the various (edge) cases. I've only read through the#[\NoSerialize]
one, since it is the topic of this RFC discussion. I agree with Ilija on
all points.Am 2025-10-29 15:35, schrieb Dmytro Kulyk:
#[NoSerialize], on the other hand, would apply to wrappers,
containers, or domain objects where serialization of the rest of the
structure should continue even if one field or nested object cannot be
meaningfully serialized.But even for wrappers you can't guarantee that the wrapper is stored in
a nullable property. When applying the attribute to a field, omitting
the field makes sense, since the author of the property is the same
person who is capable of adding the attribute and thus is in full
control. This is different for classes where the author of the class
doesn't know how and where the class is being used. I thus agree with
Ilija that this should throw to clearly indicate that the user of the
class is doing something incorrect and to not silently corrupt data. If
an author of a class needs more fine-grained control, they can either
add the attribute to individual properties themselves - or implement
__serialize().
You are right, this behavior can be non-obvious and lead to unexpected results.
Class-level #[NoSerialize] is inherited by child classes unless explicitly overridden.
Can you clarify how this would work? How can you override this
attribute by omission?#[NoSerialize] class Foo {} /* What do I add here to remove #[NoSerialize]? */ class Bar extends Foo {}Class-level #[NoSerialize] is transparently inherited by child classes.
There’s currently no way to override or cancel this behavior in
descendants; the attribute remains effective throughout the
inheritance chain.Indeed, however the “unless explicitly overridden” bit in the RFC text
implies that this is possible.As an alternative, #[NotSerializable] could be extended to
#[NotSerializable(bool $soft = false)], which would provide the same
behavior while keeping it in a separate, more consistent attribute —
avoiding the double semantics currently implied by #[NoSerialize].I don't think that sub classes should be able to make a non-serializable
parent class serializable, since they clearly won't be able to correctly
restore the state of the parent class. In fact I would consider this an
improvement over the existing serialization hooks where the child class
can simply skip calling the parent's serializer and thus create a
situation that the parent class does not expect to occur.
It already makes no sense because the class-level attribute will be
excluded from the RFC, and I have no plan to add something like this
to #[NotSerializable]. It may be added in a future RFC.
Initially, the idea was to make all of these cases emit warnings, but
I didn’t figure out how to implement that consistently within the
attribute validators.
At this point, I don’t see any practical reason to allow the attribute
on unsupported targets, so all such cases will be changed to
compile-time errors instead of warnings.Yes, every clear error that is detectable at compile-time should be a
hard error to make it easy for users to detect the mistake.
Best regards
Dmytro Kulyk
Hi
I am just noticing that your RFC still is marked as Draft, both within
the RFC text and within the overview at https://wiki.php.net/rfc. Don't
forget to adjust the status to “Under Discussion” for proper visibility.
Am 2025-10-29 23:23, schrieb Dmytro Kulyk:
It already makes no sense because the class-level attribute will be
excluded from the RFC, and I have no plan to add something like this
to #[NotSerializable]. It may be added in a future RFC.
I am seeing that you adjusted the RFC to remove the support for applying
#[\NoSerialize] to classes. Given that we determined that serializing
classes should either throw or work (i.e. that serializing them to
null is never right), I don't think that two separate attributes are
necessary. While a combined attribute would behave somewhat differently
for classes and properties, the name of #[\NoSerialize] and
#[\NotSerializable] is very similar and thus confusing in itself. I
would suggest to merge the RFCs of #[\NoSerialize] and
#[\NotSerializable] into a single attribute, since in both cases you
are indicating “I don't want this serialized” and I think it is
reasonable to expect from users to learn that there is some difference
in behavior for properties compared to classes.
Best regards
Tim Düsterhus
I am just noticing that your RFC still is marked as Draft, both within
the RFC text and within the overview at https://wiki.php.net/rfc. Don't
forget to adjust the status to “Under Discussion” for proper visibility.Am 2025-10-29 23:23, schrieb Dmytro Kulyk:
It already makes no sense because the class-level attribute will be
excluded from the RFC, and I have no plan to add something like this
to #[NotSerializable]. It may be added in a future RFC.I am seeing that you adjusted the RFC to remove the support for applying
#[\NoSerialize]to classes. Given that we determined that serializing
classes should either throw or work (i.e. that serializing them to
nullis never right), I don't think that two separate attributes are
necessary. While a combined attribute would behave somewhat differently
for classes and properties, the name of#[\NoSerialize]and
#[\NotSerializable]is very similar and thus confusing in itself. I
would suggest to merge the RFCs of#[\NoSerialize]and
#[\NotSerializable]into a single attribute, since in both cases you
are indicating “I don't want this serialized” and I think it is
reasonable to expect from users to learn that there is some difference
in behavior for properties compared to classes.
Thanks, that’s a fair point — combining both proposals would
definitely simplify the user-facing story and avoid naming confusion.
If we go in that direction, I’d suggest keeping the name
#[NoSerialize], because it’s more general and clearly describes the
action rather than the failure mode.
#[NotSerializable] reads more like an error state, while
#[NoSerialize] works naturally for both contexts — skipping properties
silently and preventing class serialization (by throwing).
This way, the attribute name remains short and declarative, while the
actual behavior (throwing vs skipping) can be determined by where it’s
applied — on a class or on a property — without introducing another
attribute.
Best,
Dmytro
Hi
Am 2025-10-30 23:24, schrieb Dmytro Kulyk:
If we go in that direction, I’d suggest keeping the name
#[NoSerialize], because it’s more general and clearly describes the
action rather than the failure mode.
#[NotSerializable] reads more like an error state, while
#[NoSerialize] works naturally for both contexts — skipping properties
silently and preventing class serialization (by throwing).This way, the attribute name remains short and declarative, while the
actual behavior (throwing vs skipping) can be determined by where it’s
applied — on a class or on a property — without introducing another
attribute.
That makes sense to me and I don't have a suggestion for a better name.
Best regards
Tim Düsterhus
Hi
Am 2025-10-30 23:24, schrieb Dmytro Kulyk:
If we go in that direction, I’d suggest keeping the name
#[NoSerialize], because it’s more general and clearly describes the
action rather than the failure mode.
#[NotSerializable] reads more like an error state, while
#[NoSerialize] works naturally for both contexts — skipping properties
silently and preventing class serialization (by throwing).This way, the attribute name remains short and declarative, while the
actual behavior (throwing vs skipping) can be determined by where it’s
applied — on a class or on a property — without introducing another
attribute.That makes sense to me and I don't have a suggestion for a better name.
I hope I'm not starting a bikeshed discussion, but I've been following
along - can I suggest "DoNotSerialize"? "NoSerialize" sounds odd to me,
whereas "DoNotSerialize" can be seen as both an instruction to PHP - "don't
serialize this property when serializing the class" - and an instruction to
developers - "do not attempt to serialize this class".
Hi
Am 2025-10-31 13:16, schrieb Eric Norris:
I hope I'm not starting a bikeshed discussion, but I've been following
along - can I suggest "DoNotSerialize"? "NoSerialize" sounds odd to me,
whereas "DoNotSerialize" can be seen as both an instruction to PHP -
"don't
serialize this property when serializing the class" - and an
instruction to
developers - "do not attempt to serialize this class".
I don't have a particularly strong opinion on naming, but I'd like to
note that #[\NoSerialize] is in line with #[\NoDiscard] (added in PHP
8.5, https://wiki.php.net/rfc/marking_return_value_as_important).
Best regards
Tim Düsterhus
Hi
Am 2025-10-31 13:16, schrieb Eric Norris:
I hope I'm not starting a bikeshed discussion, but I've been following
along - can I suggest "DoNotSerialize"? "NoSerialize" sounds odd to me,
whereas "DoNotSerialize" can be seen as both an instruction to PHP -
"don't
serialize this property when serializing the class" - and an
instruction to
developers - "do not attempt to serialize this class".I don't have a particularly strong opinion on naming, but I'd like to
note that #[\NoSerialize] is in line with #[\NoDiscard] (added in PHP
8.5, https://wiki.php.net/rfc/marking_return_value_as_important).
Fair. I had considered that this might already be inconsistent with other
attributes, but I was responding by phone and didn't look it up - thanks.
I'm still partial to something that sounds more "natural" from a language
perspective to me, but I can appreciate the argument to prefer consistency.
(This may come up in the future, so I wonder if we have / should have a
naming policy?)