Description
Symfony version(s) affected
7.0
Description
This problem exists only if you're using MetadataAwareNameConverter
(with attributes #[SerializedName]
or #[SerializedPath]
). The current implementation of ConstraintViolationListNormalizer
does not respect such attributes and way of name converting.
Why it happens?
Let's take a look at ConstraintViolationListNormalizer::normalize()
method. On the line 63
(https://github.com/symfony/symfony/blob/7.0/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php#L63), we try to normalize the property path according to current name converter. If we're using CamelCaseToSnakeCaseNameConverter
everything is working fine, but with MetadataAwareNameConverter
it does not.
Here we pass the following parameters to MetadataAwareNameConverter::normalize()
method:
$violation->getPropertyPath()
as reported property pathnull
as class$format
as format$context
as normalization context
The most important parameter for us here is class. Since we don't know the class MetadataAwareNameConverter::normalize()
fallback to another strategy of normalization (line https://github.com/symfony/symfony/blob/7.0/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php#L47). If there is no fallback name converter available, we will return passed property name itself. This is what happens in our case (we did not configure any fallback).
How to reproduce
For example, we have the following request object
// imports
readonly class UserRequest
{
#[Assert\NotBlank]
#[Assert\Length(max: 10)]
#[SerializedName('first_name')]
public string $firstName;
public public function __construct(string $firstName)
{
$this->firstName = $firstName;
}
}
Also, let's imagine we have the following controller
// imports
#[AsController]
class PinCodeController
{
#[Route('/user', name: 'update', methods: ['POST'])]
public function updateUser(
#[CurrentUser] UserInterface $user,
#[MapRequestPayload(acceptFormat: 'json')] UserRequest $request
): JsonResponse {
// handle request and perform logic
}
}
And we have the following configuration of the framework
framework:
serializer:
default_context:
!php/const Symfony\Component\Serializer\Normalizer\DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS: true
services:
get_set_method_normalizer:
class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer
tags:
- { name: 'serializer.normalizer', priority: -999 }
arguments:
$classMetadataFactory: '@serializer.mapping.class_metadata_factory'
$nameConverter: '@serializer.name_converter.metadata_aware'
Finally, we want to send the following request
{
"first_name": "supercalifragilisticexpialidocious"
}
As you can see, we will exceed the maximum length of the field.
What do we expect to receive as a response:
{
"type": "https://symfony.com/errors/validation",
"title": "Validation Failed",
"status": 422,
"detail": "first_name: This value is too long. It should have 10 characters or less.",
"violations": [
{
"propertyPath": "first_name",
"title": "This value is too long. It should have 10 characters or less.",
"template": "This value is too long. It should have {{ limit }} characters or less.",
"parameters": {
"{{ value }}": "\"supercalifragilisticexpialidocious\"",
"{{ limit }}": "10"
},
"type": "urn:uuid:d94b19cc-114f-4f44-9cc4-4138e80a87b9"
}
]
}
But we will receive the following one:
{
"type": "https://symfony.com/errors/validation",
"title": "Validation Failed",
"status": 422,
"detail": "firstName: This value is too long. It should have 10 characters or less.",
"violations": [
{
"propertyPath": "firstName",
"title": "This value is too long. It should have 10 characters or less.",
"template": "This value is too long. It should have {{ limit }} characters or less.",
"parameters": {
"{{ value }}": "\"supercalifragilisticexpialidocious\"",
"{{ limit }}": "10"
},
"type": "urn:uuid:d94b19cc-114f-4f44-9cc4-4138e80a87b9"
}
]
}
As you can see, the current implementation of ConstraintViolationListNormalizer
does not respect MetadataAwareNameConverter
correctly and fallback to the property name.
Possible Solution
The first idea that I got is to try to pass the class name of the original object to which the property belongs (root). But I'm not sure that it will cover all possible cases.
Additional Context
No response