Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce requireAllRequiredProperties context #1554

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
17 changes: 17 additions & 0 deletions src/DeserializationContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ class DeserializationContext extends Context
*/
private $depth = 0;

/**
* @var bool
*/
private $requireAllRequiredProperties = false;

public static function create(): self
{
return new self();
Expand Down Expand Up @@ -41,4 +46,16 @@ 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;
}
}
9 changes: 9 additions & 0 deletions src/Exception/PropertyMissingException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

final class PropertyMissingException extends RuntimeException
{
}
20 changes: 20 additions & 0 deletions src/GraphNavigator/DeserializationGraphNavigator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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');
}
}

Expand Down Expand Up @@ -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): bool
{
$allowsNull = false;
if ('union' === $type['name'] && isset($type['params'][0])) {
foreach ($type['params'] as $param) {
if ('NULL' === $param['name']) {
$allowsNull = true;
}
}
} elseif ('NULL' === $type['name']) {
$allowsNull = true;
}

return $allowsNull;
}
}
35 changes: 35 additions & 0 deletions tests/Serializer/JsonSerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
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\PropertyMissingException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver;
Expand All @@ -23,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;
Expand Down Expand Up @@ -134,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"}}';
Expand Down Expand Up @@ -438,6 +442,37 @@ public static function getTypeHintedArraysAndStdClass()
];
}

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) {
Expand Down
Loading