I am new to the Symfony serializer component. I am trying to properly deserialize a JSON body to the following DTO:
class PostDTO
{
/** @var string */
private $name;
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @param string $name
*/
public function setName(string $name): void
{
$this->name = $name;
}
}
The controller method as follows:
/**
* @Route (path="", methods={"POST"}, name="new_post")
* @param Request $request
* @return Response
*/
public function create(Request $request): Response
{
$model = $this->serializer->deserialize($request->getContent(), PostDTO::class, 'json');
// call the service with the model
return new JsonResponse();
}
My problem is that I wanted to handle business-validation after the body was deserialized. However, if i specify an invalid value for the name, such as false
or []
, the deserialization will fail with an exception: Symfony\Component\Serializer\Exception\NotNormalizableValueException: "The type of the "name" attribute for class "App\Service\PostDTO" must be one of "string" ("array" given).
.
I do understand that it is because I intentionally set "name": []
. However, I was looking for a way to set the fields to a default value or even perform some validation pre-deserialization.
I have found the proper way to handle this. That exception was thrown because the serializer was not able to create the PostDTO
class using the invalid payload I have provided.
To handle this, I have created my custom denormalizer which kicks in only for this particular class. To do this, I have implemented the DenormalizerInterface
like so:
use App\Service\PostDTO;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class PostDTODeserializer implements DenormalizerInterface
{
/** @var ObjectNormalizer */
private $normalizer;
/**
* PostDTODeserializer constructor.
* @param ObjectNormalizer $normalizer
*/
public function __construct(ObjectNormalizer $normalizer)
{
$this->normalizer = $normalizer;
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
return $type === PostDTO::class;
}
/**
* @param mixed $data
* @param string $type
* @param string|null $format
* @return array|bool|object
* @throws ExceptionInterface
*/
public function supportsDenormalization($data, string $type, string $format = null)
{
// validate the array which will be normalized (you should write your validator and inject it through the constructor)
if (!is_string($data['name'])) {
// normally you would throw an exception and leverage the `ErrorController` functionality
// do something
}
// convert the array to the object
return $this->normalizer->denormalize($data, $type, $format);
}
}
If you want to access the context
array, you can implement the DenormalizerAwareInterface
. Normally, you would create your custom validation and inject it into this denormalizer and validate the $data
array.
Please not that I have injected the ObjectNormalizer
here so that when the data successfully passed the validation, I can still construct the PostDTO
using the $data
.
PS: in my case, the autowiring has automatically registered my custom denormalizer. If yours is not autowired automatically, go to services.yaml
and add the following lines:
App\Serializer\PostDTODeserializer:
tags: ['serializer.normalizer']
(I have tagged the implementation with serializer.normalizer
so as it is recognized during the deserialization pipeline)