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
AppValueResolverNestedJsonValueResolver:
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(
#[AssertNotNull]
#[AssertType('string')]
private ?string $title,
#[AssertNotNull]
#[AssertType('string')]
private ?string $body,
#[AssertNotNull]
#[AssertType('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?