From a2dbc6835663f5a59e6642b93ac3a68d15fa1c33 Mon Sep 17 00:00:00 2001 From: Agustin Gomes Date: Tue, 21 Apr 2026 23:34:09 +0200 Subject: [PATCH 1/3] 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/3] 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/3] 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);