If your API is for only JSON requests then you can use native Symfony serializer package instead of more powerful JMS Serilizer package. See example below for usage.


Installation


$ composer require symfony/serializer
$ composer require symfony/property-info
$ composer require symfony/validator

Configuration


# services.yaml

services:
...

Symfony\Component\Serializer\Normalizer\PropertyNormalizer:
arguments:
$nameConverter: '@serializer.name_converter.camel_case_to_snake_case'
$propertyTypeExtractor: '@property_info.php_doc_extractor'
tags: [serializer.normalizer]

RequestUtil


namespace App\Util;

use Exception;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class RequestUtil
{
private $serializer;
private $validator;
private $violator;

public function __construct(
SerializerInterface $serializer,
ValidatorInterface $validator,
ViolationUtil $violator
) {
$this->serializer = $serializer;
$this->validator = $validator;
$this->violator = $violator;
}

public function validate(string $data, string $model): object
{
if (!$data) {
throw new BadRequestHttpException('Empty body.');
}

try {
$object = $this->serializer->deserialize($data, $model, 'json');
} catch (Exception $e) {
throw new BadRequestHttpException('Invalid body.');
}

$errors = $this->validator->validate($object);

if ($errors->count()) {
throw new BadRequestHttpException(json_encode($this->violator->build($errors)));
}

return $object;
}
}

ViolationUtil


declare(strict_types=1);

namespace Inanzzz\RequestResponseHandler\Util;

use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationListInterface;

class ViolationUtil implements ViolationUtilInterface
{
private $textUtil;

public function __construct(TextUtilInterface $textUtil)
{
$this->textUtil = $textUtil;
}

public function build(ConstraintViolationListInterface $violations): array
{
$errors = [];

/** @var ConstraintViolation $violation */
foreach ($violations as $violation) {
$errors[
$this->textUtil->makeSnakeCase($violation->getPropertyPath())
] = $violation->getMessage();
}

return $this->buildMessages($errors);
}

private function buildMessages(array $errors): array
{
$result = [];

foreach ($errors as $path => $message) {
$temp = &$result;

foreach (explode('.', $path) as $key) {
preg_match('/(.*)(\[.*?\])/', $key, $matches);
if ($matches) {
$index = str_replace(['[', ']'], '', $matches[2]);
$temp = &$temp[$matches[1]][$index];
} else {
$temp = &$temp[$key];
}
}

$temp = $message;
}

return $result;
}
}

TextUtil


declare(strict_types=1);

namespace Inanzzz\RequestResponseHandler\Util;

class TextUtil implements TextUtilInterface
{
public function makeSnakeCase(string $text): string
{
if (!trim($text)) {
return $text;
}

return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '_$1', $text));
}
}

ResponseUtil


namespace App\Util;

use Symfony\Component\Serializer\SerializerInterface;

class ResponseUtil implements ResponseUtilInterface
{
private $serializer;

public function __construct(
SerializerInterface $serializer
) {
$this->serializer = $serializer;
}

public function serialize(object $model): string
{
return $this->serializer->serialize($model, 'json');
}
}

Models


User


namespace App\Model\User\Update;

use Symfony\Component\Validator\Constraints as Assert;

class User
{
/**
* @var string
*
* @Assert\Type("string")
* @Assert\NotBlank
*/
public $fullName;

/**
* @var iterable
*
* @Assert\Type("iterable")
*/
public $favouriteTeams;

/**
* @var iterable
*
* @Assert\Type("iterable")
*/
public $randomData;

/**
* @var Address
*
* @Assert\NotBlank
* @Assert\Type("App\Model\User\Update\Address")
* @Assert\Valid
*/
public $address;
}

Address


namespace App\Model\User\Update;

use Symfony\Component\Validator\Constraints as Assert;

class Address
{
/**
* @var int
*
* @Assert\Type("int")
* @Assert\NotBlank
*/
public $houseNumber;

/**
* @var string
*
* @Assert\Type("string")
* @Assert\NotBlank
*/
public $line1;

/**
* @var string
*
* @Assert\Type("string")
*/
public $line2;

/**
* @var iterable
*
* @Assert\Type("iterable")
* @Assert\Count(max=2)
*/
public $flats;

/**
* @var Occupier[]
*
* @Assert\NotBlank
* @Assert\Type("iterable")
* @Assert\Valid
*/
public $occupiers;
}

Occupier


namespace App\Model\User\Update;

use Symfony\Component\Validator\Constraints as Assert;

class Occupier
{
/**
* @var string
*
* @Assert\Type("string")
* @Assert\NotBlank
*/
public $fullName;

/**
* @var string
*
* @Assert\Type("string")
* @Assert\NotBlank
*/
public $sex;
}

Usage


# Deserialize request as User model and validate it
$userModel = $this->requestUtil->validate($request->getContent(), User::class);

# Serilize User object as JSON string
$userJsonString = $this->responseUtil->serialize($userModel);

# Return response
return new Response($result, 200, ['Content-Type' => 'application/json']);

Request


{
"full_name": "John Travolta",
"favourite_teams": [
"Fenerbahce",
"Arsenal"
],
"random_data": {
"hair_style": "Curly",
"eye_colour": "Brown",
"allergies": [
"Nuts",
"Shellfish"
]
},
"address": {
"house_number": 184,
"line1": "House 101",
"line2": "",
"flats": [
"A",
"B"
],
"occupiers": [
{
"full_name": "Robert DeNiro",
"sex": "Male"
},
{
"full_name": "Sharon Stone",
"sex": "Female"
}
]
}
}