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

[Serializer] Fix denormalizing empty string into object|null parameter #52172

Merged
merged 1 commit into from
Nov 24, 2023

Conversation

Jeroeny
Copy link
Contributor

@Jeroeny Jeroeny commented Oct 19, 2023

Q A
Branch? 5.4
Bug fix? yes
New feature? no
Deprecations? no
License MIT

In XML and CSV all basic datatypes are represented as strings

The AbstractObjectNormalizer handles this for bool, int and float types. But if a parameter is typed Object|null, a serialization and then deserialization will fail.

This will throw:

final class Xml
{
    public function __construct(public Uuid|null $element = null)
    {
    }
}

$test = new Xml(null);
$serialized = $this->serializer->serialize($test, XmlEncoder::FORMAT);
$deserialized = $this->serializer->deserialize($serialized, Xml::class, XmlEncoder::FORMAT);
  [Symfony\Component\Serializer\Exception\NotNormalizableValueException]       
  The data is not a valid "Symfony\Component\Uid\Uuid" string representation.  

Reproducer: https://github.com/Jeroeny/reproduce/blob/xmlnull/src/Test.php

@Jeroeny Jeroeny requested a review from dunglas as a code owner October 19, 2023 14:23
@carsonbot carsonbot added this to the 6.4 milestone Oct 19, 2023
@OskarStark OskarStark changed the title [Serializer] Fix denormalizing empty string into object|null parameter [Serializer] Fix denormalizing empty string into object|null parameter Oct 20, 2023
@OskarStark
Copy link
Contributor

Looks like a bugfix to me, in this case 🎯 6.3, but let's wait for advice of @nicolas-grekas

@nicolas-grekas
Copy link
Member

This would need tests for sure. But I'm no expert in the Serializer component so I'd appreciate if someone more familiar with it could review.

@Jeroeny
Copy link
Contributor Author

Jeroeny commented Oct 23, 2023

I've added tests. I also discovered that $builtinType isn't reset in the for-loop in some cases. Which means a Object|bool type wouldn't hit this switch-case: https://github.com/symfony/symfony/blob/6.4/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php#L471 (The added tests also cover this).

@Jeroeny
Copy link
Contributor Author

Jeroeny commented Oct 23, 2023

Another interesting failure scenario:

// this fails
$this->serializer->denormalize(['element' => '0'], Example::class, context: [AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true]);

class Example {
    public function __construct(public Args|int $element) {}
}

class Args {
    public function __construct(public Uuid $nested, private string $test)
}

//  [Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException]
//  Cannot create an instance of "App\Args" from serialized data because its constructor requires the following parameters to be present : "$nested", "$test".

// whereas this returns an instance succesfully
$this->serializer->denormalize(['element' => '0'], Test::class, context: [AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true]);

class Test {
    public function __construct(public Uuid|int $element)
}

Because the first denormalization throws MissingConstructorArgumentsException, which goes before the return on DISABLE_TYPE_ENFORCEMENT.
The second throws NotNormalizableValueException, but is caught and will return 0 because of DISABLE_TYPE_ENFORCEMENT.

I'm not sure what the reasoning was here or if this is actually not intended.

It could be fixed by adding $hasTypeMismatch = $builtinType !== Type::BUILTIN_TYPE_OBJECT here:
https://github.com/symfony/symfony/blob/6.4/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php#L565. To establish that it did not throw for at least one of the $types.
And the later using that to return before MissingConstructorArgumentsException. (Or always return before that exception when DISABLE_TYPE_ENFORCEMENT is true?)

@nicolas-grekas nicolas-grekas modified the milestones: 6.4, 7.1 Nov 15, 2023
@yceruto yceruto modified the milestones: 6.4, 7.1 Nov 15, 2023
@nicolas-grekas
Copy link
Member

Let's resume this one? Is this a bugfix as it's documented in the PR description? If yes can you please target the lowest affected branch?

@Jeroeny
Copy link
Contributor Author

Jeroeny commented Nov 21, 2023

Applied feedback and targeting 6.3

@nicolas-grekas nicolas-grekas modified the milestones: 7.1, 6.3 Nov 21, 2023
@nicolas-grekas
Copy link
Member

Isn't 5.4 affected also?

@Jeroeny
Copy link
Contributor Author

Jeroeny commented Nov 21, 2023

Eh yes, I was looking into the wrong part when checking that branch. Now based on 5.4.

@nicolas-grekas
Copy link
Member

Parse error on PHP 7.2 ;)

@Jeroeny Jeroeny force-pushed the fixnullunion branch 3 times, most recently from 1a725d3 to 1df027a Compare November 23, 2023 10:27
@Jeroeny
Copy link
Contributor Author

Jeroeny commented Nov 23, 2023

Serializer tests are ok now

@nicolas-grekas
Copy link
Member

(rebase needed ;) )

@nicolas-grekas
Copy link
Member

Thank you @Jeroeny.

@nicolas-grekas nicolas-grekas merged commit 1bc8d26 into symfony:5.4 Nov 24, 2023
5 of 11 checks passed
nicolas-grekas added a commit that referenced this pull request Dec 4, 2023
This PR was merged into the 5.4 branch.

Discussion
----------

[Serializer] fix nullable int cannot be serialized

| Q             | A
| ------------- | ---
| Branch?       | 5.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| License       | MIT

Hello,

previous to [this PR](#52172) such XML could be deserialized correctly, setting null in the value:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<DummyNullableInt>
	<value/>
</DummyNullableInt>
```
```php
class DummyNullableInt
{
    public function __construct(
        public int|null $value = null
    )
    {}
}
```

but now it creates the following error:
```
Uninitialized string offset 0

/home/nikophil/works/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php:495
/home/nikophil/works/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php:630
/home/nikophil/works/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php:377
/home/nikophil/works/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php:246
/home/nikophil/works/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php:346
```

I looked for any issue or PR mentioning this problem, but couldn't find it. So here is a fix

ping `@Jeroeny`

Commits
-------

5d62dea [Serializer] fix regression where nullable int cannot be serialized
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.

None yet

6 participants