Skip to content

[Serializer] Add support for denormalizing invalid datetime without throwing an exception #42236

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

Closed
wants to merge 1 commit into from

Conversation

lyrixx
Copy link
Member

@lyrixx lyrixx commented Jul 23, 2021

Q A
Branch? 5.4
Bug fix? no
New feature? yes
Deprecations? yes
Tickets Fix #27824
License MIT
Doc PR

@lyrixx lyrixx force-pushed the serializer-date branch from 32a5233 to 364bd7f Compare July 23, 2021 12:36
@carsonbot

This comment has been minimized.

@OskarStark OskarStark added this to the 5.4 milestone Aug 1, 2021
@@ -39,6 +43,10 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface,
public function __construct(array $defaultContext = [])
{
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);

if (null === $this->defaultContext[self::THROW_EXCEPTION_ON_INVALID_KEY]) {
trigger_deprecation('symfony/serializer', '5.4', 'The key context "%s" of "%s" must be defined. The value will be "false" in Symfony 6.0.', self::THROW_EXCEPTION_ON_INVALID_KEY, __CLASS__);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
trigger_deprecation('symfony/serializer', '5.4', 'The key context "%s" of "%s" must be defined. The value will be "false" in Symfony 6.0.', self::THROW_EXCEPTION_ON_INVALID_KEY, __CLASS__);
trigger_deprecation('symfony/serializer', '5.4', 'The key context "%s" of "%s" must be provided. The value will default to "false" in Symfony 6.0.', self::THROW_EXCEPTION_ON_INVALID_KEY, __CLASS__);

@lyrixx
Copy link
Member Author

lyrixx commented Aug 12, 2021

Warning Don't merge it for now, I think I found a better and more global way to fix this issue

@fabpot
Copy link
Member

fabpot commented Aug 26, 2021

@lyrixx Can we close this one?

@lyrixx
Copy link
Member Author

lyrixx commented Sep 3, 2021

Yes, Sorry for the late reply. I wanted to be sure #42502 is gonna be merged, but there are already 2 👍🏼 So I think it's safe to close this one.

@lyrixx lyrixx closed this Sep 3, 2021
@lyrixx lyrixx deleted the serializer-date branch September 3, 2021 12:54
fabpot added a commit that referenced this pull request Sep 10, 2021
…ing denormalization (lyrixx)

This PR was merged into the 5.4 branch.

Discussion
----------

[Serializer] Add support for collecting type error during denormalization

| Q             | A
| ------------- | ---
| Branch?       | 5.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #27824, Fix #42236, Fix #38472, Fix #37419 Fix #38968
| License       | MIT
| Doc PR        |

---

There is something that I don't like about the (de)Serializer. It's about the way it deals with typed properties.
As soon as you add a type to a property, the API can return 500.

Let's consider the following code:
```php
class MyDto
{
    public string $string;
    public int $int;
    public float $float;
    public bool $bool;
    public \DateTime $dateTime;
    public \DateTimeImmutable $dateTimeImmutable;
    public \DateTimeZone $dateTimeZone;
    public \SplFileInfo $splFileInfo;
    public Uuid $uuid;
    public array $array;
    /** `@var` MyDto[] */
    public array $collection;
}
```

and the following JSON:

```json
{
	"string": null,
	"int": null,
	"float": null,
	"bool": null,
	"dateTime": null,
	"dateTimeImmutable": null,
	"dateTimeZone": null,
	"splFileInfo": null,
	"uuid": null,
	"array": null,
	"collection": [
		{
			"string": "string"
		},
		{
			"string": null
		}
	]
}
```

**By default**, I got a 500:
![image](https://user-images.githubusercontent.com/408368/129211588-0ce9064e-171d-42f2-89ac-b126fc3f9eab.png)

It's the same with the prod environment. This is far from perfect when you try to make a public API :/
ATM, the only solution, is to remove all typehints and add assertions (validator component). With that, the public API is nice, but the internal PHP is not so good (PHP 7.4+ FTW!)

In APIP, they have support for transforming to [something](https://github.com/api-platform/core/blob/53837eee3ebdea861ffc1c9c7f052eecca114757/src/Core/Serializer/AbstractItemNormalizer.php#L233-L237) they can handle gracefully. But the deserialization stop on the first error (so the end user must fix the error, try again, fix the second error, try again etc.). And the raw exception message is leaked to the end user. So the API can return something like  `The type of the "string" attribute for class "App\Dto\MyDto" must be one of "string" ("null" given).`. Really not cool :/

So ATM, building a nice public API is not cool.

That's why I propose this PR that address all issues reported
* be able to collect all error
* with their property path associated
* don't leak anymore internal

In order to not break the BC, I had to use some fancy code to make it work 🐒

With the following code, I'm able to collect all errors, transform them in `ConstraintViolationList` and render them properly, as expected.

![image](https://user-images.githubusercontent.com/408368/129215560-b0254a4e-fec7-4422-bee0-95cf9f9eda6c.png)

```php
    #[Route('/api', methods:['POST'])]
    public function apiPost(SerializerInterface $serializer, Request $request): Response
    {
        $context = ['not_normalizable_value_exceptions' => []];
        $exceptions = &$context['not_normalizable_value_exceptions'];

        $dto = $serializer->deserialize($request->getContent(), MyDto::class, 'json', $context);

        if ($exceptions) {
            $violations = new ConstraintViolationList();
            /** `@var` NotNormalizableValueException */
            foreach ($exceptions as $exception) {
                $message = sprintf('The type must be one of "%s" ("%s" given).', implode(', ', $exception->getExpectedTypes()), $exception->getCurrentType());
                $parameters = [];
                if ($exception->canUseMessageForUser()) {
                    $parameters['hint'] = $exception->getMessage();
                }
                $violations->add(new ConstraintViolation($message, '', $parameters, null, $exception->getPath(), null));
            };

            return $this->json($violations, 400);
        }

        return $this->json($dto);
    }
```

If this PR got accepted, the above code could be transferred to APIP to handle correctly the deserialization

Commits
-------

ebe6551 [Serializer] Add support for collecting type error during denormalization
hultberg pushed a commit to hultberg/symfony that referenced this pull request Sep 17, 2021
…ror during denormalization (lyrixx)

This PR was merged into the 5.4 branch.

Discussion
----------

[Serializer] Add support for collecting type error during denormalization

| Q             | A
| ------------- | ---
| Branch?       | 5.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix symfony#27824, Fix symfony#42236, Fix symfony#38472, Fix symfony#37419 Fix symfony#38968
| License       | MIT
| Doc PR        |

---

There is something that I don't like about the (de)Serializer. It's about the way it deals with typed properties.
As soon as you add a type to a property, the API can return 500.

Let's consider the following code:
```php
class MyDto
{
    public string $string;
    public int $int;
    public float $float;
    public bool $bool;
    public \DateTime $dateTime;
    public \DateTimeImmutable $dateTimeImmutable;
    public \DateTimeZone $dateTimeZone;
    public \SplFileInfo $splFileInfo;
    public Uuid $uuid;
    public array $array;
    /** `@var` MyDto[] */
    public array $collection;
}
```

and the following JSON:

```json
{
	"string": null,
	"int": null,
	"float": null,
	"bool": null,
	"dateTime": null,
	"dateTimeImmutable": null,
	"dateTimeZone": null,
	"splFileInfo": null,
	"uuid": null,
	"array": null,
	"collection": [
		{
			"string": "string"
		},
		{
			"string": null
		}
	]
}
```

**By default**, I got a 500:
![image](https://user-images.githubusercontent.com/408368/129211588-0ce9064e-171d-42f2-89ac-b126fc3f9eab.png)

It's the same with the prod environment. This is far from perfect when you try to make a public API :/
ATM, the only solution, is to remove all typehints and add assertions (validator component). With that, the public API is nice, but the internal PHP is not so good (PHP 7.4+ FTW!)

In APIP, they have support for transforming to [something](https://github.com/api-platform/core/blob/53837eee3ebdea861ffc1c9c7f052eecca114757/src/Core/Serializer/AbstractItemNormalizer.php#L233-L237) they can handle gracefully. But the deserialization stop on the first error (so the end user must fix the error, try again, fix the second error, try again etc.). And the raw exception message is leaked to the end user. So the API can return something like  `The type of the "string" attribute for class "App\Dto\MyDto" must be one of "string" ("null" given).`. Really not cool :/

So ATM, building a nice public API is not cool.

That's why I propose this PR that address all issues reported
* be able to collect all error
* with their property path associated
* don't leak anymore internal

In order to not break the BC, I had to use some fancy code to make it work 🐒

With the following code, I'm able to collect all errors, transform them in `ConstraintViolationList` and render them properly, as expected.

![image](https://user-images.githubusercontent.com/408368/129215560-b0254a4e-fec7-4422-bee0-95cf9f9eda6c.png)

```php
    #[Route('/api', methods:['POST'])]
    public function apiPost(SerializerInterface $serializer, Request $request): Response
    {
        $context = ['not_normalizable_value_exceptions' => []];
        $exceptions = &$context['not_normalizable_value_exceptions'];

        $dto = $serializer->deserialize($request->getContent(), MyDto::class, 'json', $context);

        if ($exceptions) {
            $violations = new ConstraintViolationList();
            /** `@var` NotNormalizableValueException */
            foreach ($exceptions as $exception) {
                $message = sprintf('The type must be one of "%s" ("%s" given).', implode(', ', $exception->getExpectedTypes()), $exception->getCurrentType());
                $parameters = [];
                if ($exception->canUseMessageForUser()) {
                    $parameters['hint'] = $exception->getMessage();
                }
                $violations->add(new ConstraintViolation($message, '', $parameters, null, $exception->getPath(), null));
            };

            return $this->json($violations, 400);
        }

        return $this->json($dto);
    }
```

If this PR got accepted, the above code could be transferred to APIP to handle correctly the deserialization

Commits
-------

ebe6551 [Serializer] Add support for collecting type error during denormalization
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Serializer] Do not throw exception in the DateTimeNormalizer if it's not a date
5 participants