Handling imporper data during deserialization when using Symfony Serializer Component

Viewed 377

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.

1 Answers

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)

Related