I'm developing a Symfony 7 REST API. I'm following the official docs to implement a custom argument resolver to handle mapping of the request payload format that my endpoints are receiving to DTOs (https://symfony.com/doc/current/controller/value_resolver.html#adding-a-custom-value-resolver).
Here's an example of a request payload to an endpoint
{"article": {"title": "How to train your dragon","description": "Ever wonder how?","body": "You have to believe" }}
Here's the custom ValueResolver
class NestedJsonValueResolver implements ValueResolverInterface{ public function __construct( private readonly SerializerInterface $serializer, private readonly ValidatorInterface $validator, ) { } public function resolve(Request $request, ArgumentMetadata $argument): iterable { $argumentType = $argument->getType(); if (!in_array('NestedJsonDtoInterface', class_implements($argumentType))) { return []; } return [$this->mapRequestPayload($request, $argumentType)]; } /** * @param Request $request * @param class-string<NestedJsonDtoInterface> $type */ private function mapRequestPayload(Request $request, string $type): ?object { if (null === $format = $request->getContentTypeFormat()) { throw new HttpException(Response::HTTP_UNSUPPORTED_MEDIA_TYPE, 'Unsupported format.'); } if ('' === $data = $request->getContent()) { return null; } try { $nestedJsonObjectKey = $type::getNestedJsonObjectKey(); $decodedData = json_decode($data, true); $nestedJsonObject = $decodedData[$nestedJsonObjectKey]; $payload = $this->serializer->deserialize($nestedJsonObject, $type, 'json'); $violations = $this->validator->validate($payload); if ($violations->count() > 0) { throw new HttpException(Response::HTTP_UNPROCESSABLE_ENTITY, implode("\n", array_map(static fn ($e) => $e->getMessage(), iterator_to_array($violations))), new ValidationFailedException($payload, $violations)); } return $payload; } catch (UnsupportedFormatException $e) { throw new HttpException(Response::HTTP_UNSUPPORTED_MEDIA_TYPE, sprintf('Unsupported format: "%s".', $format), $e); } catch (NotEncodableValueException $e) { throw new HttpException(Response::HTTP_BAD_REQUEST, sprintf('Request payload contains invalid "%s" data.', $format), $e); } }}
Here's my services.yaml
services: # default configuration for services in *this* file _defaults: autowire: true # Automatically injects dependencies in your services. autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name App\: resource: '../src/' exclude: - '../src/DependencyInjection/' - '../src/Entity/' - '../src/Kernel.php' # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones App\ValueResolver\NestedJsonValueResolver: tags: - controller.argument_value_resolver: name: nested_json priority: 150
Here's the controller action
#[Route('', methods: ['POST'])] public function store( #[ValueResolver('nested_json')] StoreArticleDto $dto ): JsonResponse { $article = $this->articleService->store($dto); return $this->json(['article' => $article, ], 201); }
And here's the target DTO
final readonly class StoreArticleDto implements NestedJsonDtoInterface{ private const NESTED_JSON_OBJECT_KEY = 'article'; public function __construct( #[Assert\NotNull] #[Assert\Type('string')] private ?string $title, #[Assert\NotNull] #[Assert\Type('string')] private ?string $body, #[Assert\NotNull] #[Assert\Type('string')] private ?string $description, ) { } public static function getNestedJsonObjectKey(): string { return self::NESTED_JSON_OBJECT_KEY; } public function getTitle(): ?string { return $this->title; } public function getBody(): ?string { return $this->body; } public function getDescription(): ?string { return $this->description; }}
Upon hitting an endpoint the following Exception is being thrown.
How should I go about making the custom ValueResolver work?