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

php.internals

Дмитрий Кулик

308 days ago
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

Ilija Tovilo

308 days ago
Hi Dmytro On Tue, Oct 28, 2025 at 12:00 AM Dmytro Kulyk <lnkvisitor.ts@gmail.com> wrote:
> > 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

Дмитрий Кулик

307 days ago
Thanks for the feedback! On Tue, Oct 28, 2025 at 17:47 Ilija Tovilo <tovilo.ilija@gmail.com> wrote:
> > Hi Dmytro > > On Tue, Oct 28, 2025 at 12:00 AM Dmytro Kulyk <lnkvisitor.ts@gmail.com> wrote: > > > > 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.
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

Tim Düsterhus

307 days ago
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

Дмитрий Кулик

306 days ago
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

Tim Düsterhus

306 days ago
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

Дмитрий Кулик

305 days ago
> 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. >
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

Tim Düsterhus

305 days ago
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

Eric Norris

305 days ago
On Fri, Oct 31, 2025, 4:47 AM Tim Düsterhus <tim@bastelstu.be> wrote:
> 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".

Tim Düsterhus

305 days ago
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

Дмитрий Кулик

298 days ago
Hi all, I’ve updated the RFC to include the new class-level behavior — applying #[NoSerialize] to a class now forbids its serialization by throwing, using the same internal mechanism as built-in non-serializable classes (e.g., CurlHandle, Random\Engine\Secure). This update aligns the proposal with existing engine semantics and unifies both property-level skipping and class-level prohibition under a single attribute. The inheritance model has also been clarified: class-level #[NoSerialize] is inherited (“sticky”) and cannot be overridden in subclasses. Updated RFC: https://wiki.php.net/rfc/no_serialize_attribute Best regards, Dmytro Kulyk

Tim Düsterhus

298 days ago
Hi Am 2025-11-07 01:13, schrieb Dmytro Kulyk:
> I’ve updated the RFC to include the new class-level behavior — > applying #[NoSerialize] to a class now forbids its serialization by > throwing, using the same internal mechanism as built-in > non-serializable classes (e.g., CurlHandle, Random\Engine\Secure). > > This update aligns the proposal with existing engine semantics and > unifies both property-level skipping and class-level prohibition under > a single attribute. > The inheritance model has also been clarified: class-level > #[NoSerialize] is inherited (“sticky”) and cannot be overridden in > subclasses. > > Updated RFC: > https://wiki.php.net/rfc/no_serialize_attribute
Thank you. The updated RFC is looking good to me. I also appreciate that you kept a changelog within the RFC. One thing you could mention and that should be done as part of the RFC is migrating all existing classes with `@not-serializable` to make use of the attribute instead. That's also what has been done when the `#[\Deprecated]` attribute was introduced. Other than that, I don't have any further comments. Best regards Tim Düsterhus