From 4cef60716b32ec0ac13a5387ad01cc02d011b08d Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Fri, 26 Jul 2024 12:54:03 -0600 Subject: [PATCH 1/8] introduce requireAllRequiredProperties context --- src/DeserializationContext.php | 16 +++++++++++++++ .../DeserializationGraphNavigator.php | 20 +++++++++++++++++++ tests/Serializer/JsonSerializationTest.php | 7 +++++++ 3 files changed, 43 insertions(+) diff --git a/src/DeserializationContext.php b/src/DeserializationContext.php index 5c16a24db..ac5f9b285 100644 --- a/src/DeserializationContext.php +++ b/src/DeserializationContext.php @@ -13,6 +13,11 @@ class DeserializationContext extends Context */ private $depth = 0; + /** + * @var bool + */ + private $requireAllRequiredProperties = false; + public static function create(): self { return new self(); @@ -41,4 +46,15 @@ public function decreaseDepth(): void $this->depth -= 1; } + public function setRequireAllRequiredProperties(bool $require): self + { + $this->requireAllRequiredProperties = $require; + + return $this; + } + + public function getRequireAllRequiredProperties(): bool + { + return $this->requireAllRequiredProperties; + } } diff --git a/src/GraphNavigator/DeserializationGraphNavigator.php b/src/GraphNavigator/DeserializationGraphNavigator.php index a29e43c12..0b72720b2 100644 --- a/src/GraphNavigator/DeserializationGraphNavigator.php +++ b/src/GraphNavigator/DeserializationGraphNavigator.php @@ -14,6 +14,7 @@ use JMS\Serializer\Exception\ExpressionLanguageRequiredException; use JMS\Serializer\Exception\LogicException; use JMS\Serializer\Exception\NotAcceptableException; +use JMS\Serializer\Exception\PropertyMissingException; use JMS\Serializer\Exception\RuntimeException; use JMS\Serializer\Exception\SkipHandlerException; use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy; @@ -197,6 +198,7 @@ public function accept($data, ?array $type = null) $this->visitor->startVisitingObject($metadata, $object, $type); foreach ($metadata->propertyMetadata as $propertyMetadata) { + $allowsNull = null === $propertyMetadata->type ? true : $this->allowsNull($propertyMetadata->type); if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) { continue; } @@ -218,6 +220,8 @@ public function accept($data, ?array $type = null) $cloned = clone $propertyMetadata; $cloned->setter = null; $this->accessor->setValue($object, $cloned->defaultValue, $cloned, $this->context); + } elseif (!$allowsNull && $this->context->getRequireAllRequiredProperties()) { + throw new PropertyMissingException('Property ' . $propertyMetadata->name . ' is missing from data'); } } @@ -263,4 +267,20 @@ private function afterVisitingObject(ClassMetadata $metadata, object $object, ar $this->dispatcher->dispatch('serializer.post_deserialize', $metadata->name, $this->format, new ObjectEvent($this->context, $object, $type)); } } + + private function allowsNull(array $type) + { + $allowsNull = false; + if ('union' === $type['name']) { + foreach ($type['params'] as $param) { + if ('NULL' === $param['name']) { + $allowsNull = true; + } + } + } elseif ('NULL' === $type['name']) { + $allowsNull = true; + } + + return $allowsNull; + } } diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index 44c22ff5f..fbe34e1ea 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -13,6 +13,7 @@ use JMS\Serializer\GraphNavigatorInterface; use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver; use JMS\Serializer\SerializationContext; +use JMS\Serializer\DeserializationContext; use JMS\Serializer\Tests\Fixtures\Author; use JMS\Serializer\Tests\Fixtures\AuthorList; use JMS\Serializer\Tests\Fixtures\DiscriminatedAuthor; @@ -438,6 +439,12 @@ public static function getTypeHintedArraysAndStdClass() ]; } + public function testDeserializationFailureOnPropertyMissing() + { + self::expectException(\JMS\Serializer\Exception\PropertyMissingException::class); + $this->deserialize(static::getContent('empty_object'), Author::class, DeserializationContext::create()->setRequireAllRequiredProperties(true)); + } + public function testDeserializingUnionProperties() { if (PHP_VERSION_ID < 80000) { From 9dcfe60027cb0a0858af4996b1e62f50c5c19d95 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Fri, 26 Jul 2024 13:07:12 -0600 Subject: [PATCH 2/8] style and missing file --- src/DeserializationContext.php | 1 + src/Exception/PropertyMissingException.php | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 src/Exception/PropertyMissingException.php diff --git a/src/DeserializationContext.php b/src/DeserializationContext.php index ac5f9b285..c5ee6a301 100644 --- a/src/DeserializationContext.php +++ b/src/DeserializationContext.php @@ -46,6 +46,7 @@ public function decreaseDepth(): void $this->depth -= 1; } + public function setRequireAllRequiredProperties(bool $require): self { $this->requireAllRequiredProperties = $require; diff --git a/src/Exception/PropertyMissingException.php b/src/Exception/PropertyMissingException.php new file mode 100644 index 000000000..1454c83f5 --- /dev/null +++ b/src/Exception/PropertyMissingException.php @@ -0,0 +1,9 @@ + Date: Fri, 26 Jul 2024 13:10:21 -0600 Subject: [PATCH 3/8] style --- tests/Serializer/JsonSerializationTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index fbe34e1ea..7ab6793c8 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -10,10 +10,11 @@ use JMS\Serializer\EventDispatcher\ObjectEvent; use JMS\Serializer\Exception\NonVisitableTypeException; use JMS\Serializer\Exception\RuntimeException; +use \JMS\Serializer\Exception\PropertyMissingException; use JMS\Serializer\GraphNavigatorInterface; use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver; -use JMS\Serializer\SerializationContext; use JMS\Serializer\DeserializationContext; +use JMS\Serializer\SerializationContext; use JMS\Serializer\Tests\Fixtures\Author; use JMS\Serializer\Tests\Fixtures\AuthorList; use JMS\Serializer\Tests\Fixtures\DiscriminatedAuthor; @@ -441,7 +442,7 @@ public static function getTypeHintedArraysAndStdClass() public function testDeserializationFailureOnPropertyMissing() { - self::expectException(\JMS\Serializer\Exception\PropertyMissingException::class); + self::expectException(PropertyMissingException::class); $this->deserialize(static::getContent('empty_object'), Author::class, DeserializationContext::create()->setRequireAllRequiredProperties(true)); } From 91bfcb1982d4026493596ee02f3bcc8d19867feb Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Mon, 26 Aug 2024 09:12:45 -0400 Subject: [PATCH 4/8] update to support unions --- src/GraphNavigator/DeserializationGraphNavigator.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GraphNavigator/DeserializationGraphNavigator.php b/src/GraphNavigator/DeserializationGraphNavigator.php index 0b72720b2..6743de713 100644 --- a/src/GraphNavigator/DeserializationGraphNavigator.php +++ b/src/GraphNavigator/DeserializationGraphNavigator.php @@ -268,10 +268,10 @@ private function afterVisitingObject(ClassMetadata $metadata, object $object, ar } } - private function allowsNull(array $type) + private function allowsNull(array $type): bool { $allowsNull = false; - if ('union' === $type['name']) { + if ('union' === $type['name'] && isset($type['params'][0])) { foreach ($type['params'] as $param) { if ('NULL' === $param['name']) { $allowsNull = true; From d8c5ee5df69d70f46ec28c3352dbeca53ca4c8f5 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Mon, 26 Aug 2024 09:47:12 -0400 Subject: [PATCH 5/8] add more tests --- tests/Serializer/JsonSerializationTest.php | 29 +++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index 7ab6793c8..a0cff5116 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -25,6 +25,7 @@ use JMS\Serializer\Tests\Fixtures\ObjectWithObjectProperty; use JMS\Serializer\Tests\Fixtures\Tag; use JMS\Serializer\Tests\Fixtures\TypedProperties\ComplexDiscriminatedUnion; +use JMS\Serializer\Tests\Fixtures\TypedProperties\ConstructorPromotion\Vase; use JMS\Serializer\Tests\Fixtures\TypedProperties\UnionTypedProperties; use JMS\Serializer\Visitor\Factory\JsonSerializationVisitorFactory; use JMS\Serializer\Visitor\SerializationVisitorInterface; @@ -136,6 +137,7 @@ protected static function getContent($key) $outputs['inline_map'] = '{"a":"1","b":"2","c":"3"}'; $outputs['inline_empty_map'] = '{}'; $outputs['empty_object'] = '{}'; + $outputs['vase_with_plant'] = '{"color":"blue","size":"big","plant":"flower","typeOfSoil":"potting mix","daysSincePotting":-1,"weight":10}'; $outputs['inline_deserialization_map'] = '{"a":"b","c":"d","e":"5"}'; $outputs['iterable'] = '{"iterable":{"foo":"bar","bar":"foo"}}'; $outputs['iterator'] = '{"iterator":{"foo":"bar","bar":"foo"}}'; @@ -440,12 +442,37 @@ public static function getTypeHintedArraysAndStdClass() ]; } - public function testDeserializationFailureOnPropertyMissing() + public function testDeserializationFailureOnPropertyMissingRequiredMissing() { self::expectException(PropertyMissingException::class); $this->deserialize(static::getContent('empty_object'), Author::class, DeserializationContext::create()->setRequireAllRequiredProperties(true)); } + public function testDeserializationFailureOnPropertyMissingUnionRequiredMissing() + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); + + return; + } + + self::expectException(PropertyMissingException::class); + $this->deserialize(static::getContent('empty_object'), UnionTypedProperties::class, DeserializationContext::create()->setRequireAllRequiredProperties(true)); + } + + public function testDeserializationSuccessPropertyMissingNullablePresent() + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); + + return; + } + + // Vase setters on size and weight modify the inputs from big -> huge and 10 -> 40. + $object = new Vase('blue', 'huge', 'flower', 'potting mix', -1, 40); + self::assertEquals($object, $this->deserialize(static::getContent('vase_with_plant'), Vase::class, DeserializationContext::create()->setRequireAllRequiredProperties(true))); + } + public function testDeserializingUnionProperties() { if (PHP_VERSION_ID < 80000) { From 5a83189dd43d3234aed56ffc6d9aaa6a44ac7336 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Mon, 26 Aug 2024 10:04:04 -0400 Subject: [PATCH 6/8] add documentation. --- doc/configuration.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/configuration.rst b/doc/configuration.rst index aabe95662..5019dd43c 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -104,3 +104,16 @@ a serialization context from your callable and use it. You can also set a default DeserializationContextFactory with ``->setDeserializationContextFactory(function () { /* ... */ })`` to be used with methods ``deserialize()`` and ``fromArray()``. + +Fail Deserialization When Required Properties Are Missing +--------------------------------------------------------- +By default, the deserializer will ignore missing required properties - +deserialization will succeed and the properties will be left unset. + +You may want, instead, for deserialization to fail in this case. You can +configure the deserializer to fail in this way by using the `DeserializationContext` + +For example: + $object = $serializer->deserialize($json, 'MyObject`, 'json', DeserializationContext::create()->setRequireAllRequiredProperties(true)); + +If you would like this behaviour to be the default, you can set the `DeserializationContextFactory` as described above. \ No newline at end of file From b5b851b401f689235a6582d63306752edc2b956e Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Mon, 26 Aug 2024 10:04:59 -0400 Subject: [PATCH 7/8] fix weird import --- tests/Serializer/JsonSerializationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index a0cff5116..a88bebe0b 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -10,7 +10,7 @@ use JMS\Serializer\EventDispatcher\ObjectEvent; use JMS\Serializer\Exception\NonVisitableTypeException; use JMS\Serializer\Exception\RuntimeException; -use \JMS\Serializer\Exception\PropertyMissingException; +use JMS\Serializer\Exception\PropertyMissingException; use JMS\Serializer\GraphNavigatorInterface; use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver; use JMS\Serializer\DeserializationContext; From 4bad485ca1530dcff7e9ec85547c55455cd50102 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Mon, 26 Aug 2024 10:09:10 -0400 Subject: [PATCH 8/8] import order --- tests/Serializer/JsonSerializationTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index a88bebe0b..8abcec389 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -5,15 +5,15 @@ namespace JMS\Serializer\Tests\Serializer; use JMS\Serializer\Context; +use JMS\Serializer\DeserializationContext; use JMS\Serializer\EventDispatcher\Event; use JMS\Serializer\EventDispatcher\EventSubscriberInterface; use JMS\Serializer\EventDispatcher\ObjectEvent; use JMS\Serializer\Exception\NonVisitableTypeException; -use JMS\Serializer\Exception\RuntimeException; use JMS\Serializer\Exception\PropertyMissingException; +use JMS\Serializer\Exception\RuntimeException; use JMS\Serializer\GraphNavigatorInterface; use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver; -use JMS\Serializer\DeserializationContext; use JMS\Serializer\SerializationContext; use JMS\Serializer\Tests\Fixtures\Author; use JMS\Serializer\Tests\Fixtures\AuthorList;