From a2dbc6835663f5a59e6642b93ac3a68d15fa1c33 Mon Sep 17 00:00:00 2001 From: Agustin Gomes Date: Tue, 21 Apr 2026 23:34:09 +0200 Subject: [PATCH 1/4] Validate presence of the unexpected behavior The following minimal reproduction case currently ends in failure: ```php deserialize( serialized: $nullableArray, from: 'json', to: MyList::class, ); ``` This test addition aims to confirm the bug, and serves as automated test to fix the implementation to achieve the desired behavior. --- tests/Records/NullablePointList.php | 19 +++++++++++++++ .../NullablePointListWithoutConstructor.php | 24 +++++++++++++++++++ tests/SerdeTestCases.php | 13 ++++++++++ 3 files changed, 56 insertions(+) create mode 100644 tests/Records/NullablePointList.php create mode 100644 tests/Records/NullablePointListWithoutConstructor.php diff --git a/tests/Records/NullablePointList.php b/tests/Records/NullablePointList.php new file mode 100644 index 0000000..431f32d --- /dev/null +++ b/tests/Records/NullablePointList.php @@ -0,0 +1,19 @@ +|null $points + */ + public function __construct( + #[SequenceField(arrayType: Point::class)] + public array|null $points = null, + ) { + } +} diff --git a/tests/Records/NullablePointListWithoutConstructor.php b/tests/Records/NullablePointListWithoutConstructor.php new file mode 100644 index 0000000..7ea36e3 --- /dev/null +++ b/tests/Records/NullablePointListWithoutConstructor.php @@ -0,0 +1,24 @@ + [ + 'data' => new NullablePointList(), + ]; + + $reflected = new ReflectionClass(NullablePointListWithoutConstructor::class); + $instance = $reflected->newInstanceWithoutConstructor(); + $reflected->getProperty('points')->setValue($instance, null); + yield 'nullable_list_without_constructor' => [ + 'data' => $instance, + ]; } public static function value_object_flatten_examples(): \Generator From 0fcbae2393ae9fb523f5118a0e34db06fe48f492 Mon Sep 17 00:00:00 2001 From: Agustin Gomes Date: Tue, 21 Apr 2026 23:56:40 +0200 Subject: [PATCH 2/4] Allow deserialization of nullable arrays This was a case encountered when dealing with an external system as part of a project I was working on. With this fix, the following minimal reproduction case can work: ```php deserialize( serialized: $nullableArray, from: 'json', to: MyList::class, ); ``` --- src/Attributes/Field.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Attributes/Field.php b/src/Attributes/Field.php index e4e764e..11e0d93 100644 --- a/src/Attributes/Field.php +++ b/src/Attributes/Field.php @@ -375,7 +375,9 @@ public function validate(mixed $value): bool { $valueType = \get_debug_type($value); - if ($this->phpType === $valueType) { + if ($this->phpType === 'array' && $this->nullable && $value === null) { + return true; + } elseif ($this->phpType === $valueType) { $valid = true; } elseif ($this->phpType === 'mixed') { // From a type perspective, mixed accepts anything. From 39c43178121405ee180d12cf5c6134c97beffd16 Mon Sep 17 00:00:00 2001 From: Agustin Gomes Date: Wed, 29 Apr 2026 18:13:56 +0200 Subject: [PATCH 3/4] Move nullable array check towards the end of method We're returning early under this conditions due to the fact that a SequenceField instance call to `validate` with a null value would throw an exception. Unfortunately at this moment, only the value can be passed to the `validate`, meaning we cannot check `$this->nullable`, which would allow us to have this check inside the `validate` method of the SequenceField instance. --- src/Attributes/Field.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Attributes/Field.php b/src/Attributes/Field.php index 11e0d93..b949a11 100644 --- a/src/Attributes/Field.php +++ b/src/Attributes/Field.php @@ -375,9 +375,7 @@ public function validate(mixed $value): bool { $valueType = \get_debug_type($value); - if ($this->phpType === 'array' && $this->nullable && $value === null) { - return true; - } elseif ($this->phpType === $valueType) { + if ($this->phpType === $valueType) { $valid = true; } elseif ($this->phpType === 'mixed') { // From a type perspective, mixed accepts anything. @@ -416,6 +414,18 @@ public function validate(mixed $value): bool }; } + /** + * We're returning early under this conditions due to the fact that a SequenceField + * instance call to `validate` with a null value would throw an exception. + * + * Unfortunately at this moment, only the value can be passed to the `validate`, + * meaning we cannot check `$this->nullable`, which would allow us to have this check inside the `validate` + * method of the SequenceField instance. + */ + if ($this->phpType === 'array' && $this->nullable && $value === null && $valid) { + return true; + } + // The value validates if it passes the simple check above, // plus the typeField check, if any. return $valid && ($this->typeField?->validate($value) ?? true); From 2fcc98cd7abfb31f9c52e59646e7e4638a738de0 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 7 Jun 2026 16:38:02 +0200 Subject: [PATCH 4/4] Any nullable field permits null, without further validation. --- src/Attributes/Field.php | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/Attributes/Field.php b/src/Attributes/Field.php index b949a11..f29e8d6 100644 --- a/src/Attributes/Field.php +++ b/src/Attributes/Field.php @@ -373,6 +373,12 @@ public function exclude(): bool */ public function validate(mixed $value): bool { + if ($this->nullable && $value === null) { + // If the field is nullable and the value is null, we know it's valid. + // No need to call the validate() method further down. + return true; + } + $valueType = \get_debug_type($value); if ($this->phpType === $valueType) { @@ -380,8 +386,6 @@ public function validate(mixed $value): bool } elseif ($this->phpType === 'mixed') { // From a type perspective, mixed accepts anything. $valid = true; - } elseif ($this->nullable && $valueType === 'null') { - $valid = true; } elseif (is_object($value) || class_exists($this->phpType) || interface_exists($this->phpType)) { // For objects, do a type check and we're done. $valid = $value instanceof $this->phpType; @@ -414,18 +418,6 @@ public function validate(mixed $value): bool }; } - /** - * We're returning early under this conditions due to the fact that a SequenceField - * instance call to `validate` with a null value would throw an exception. - * - * Unfortunately at this moment, only the value can be passed to the `validate`, - * meaning we cannot check `$this->nullable`, which would allow us to have this check inside the `validate` - * method of the SequenceField instance. - */ - if ($this->phpType === 'array' && $this->nullable && $value === null && $valid) { - return true; - } - // The value validates if it passes the simple check above, // plus the typeField check, if any. return $valid && ($this->typeField?->validate($value) ?? true);