In example below, we're validating json request against models and map serialised fields to model properties. In the case of invalid request, json error response with "400 Bad request" is sent back. @Serializer\Type("....") will convert some values automatically to match the type however, if it cannot then "500 Internal Server Error - Could not decode JSON, syntax error - malformed JSON." response will be issued. Example also uses a custom built validator constraint for datetime.


Composer.json


Install "jms/serializer-bundle": "0.13.0" with composer.


Controllers.yml


services:
application_backend.controller.abstract:
class: Application\BackendBundle\Controller\AbstractController
abstract: true
arguments:
- @serializer
- @validator
- @doctrine_common_inflector

application_backend.controller.api:
class: Application\BackendBundle\Controller\ApiController
parent: application_backend.controller.abstract

doctrine_common_inflector:
class: Doctrine\Common\Inflector\Inflector

Controllers


AbstractController


namespace Application\BackendBundle\Controller;

use Doctrine\Common\Inflector\Inflector;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

abstract class AbstractController
{
private $validContentTypes = ['json' => 'application/json', 'xml' => 'application/xml'];

protected $serializer;
protected $validator;
protected $inflector;

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

/**
* @param string $contentType
*
* @return string|Response
*/
protected function validateContentType($contentType)
{
if (!in_array($contentType, $this->validContentTypes)) {
return $this->createFailureResponse(
['content_type' => sprintf('Invalid content type [%s].', $contentType)],
'json',
415
);
}

return array_search($contentType, $this->validContentTypes);
}

/**
* @param string $payload
* @param string $model
* @param string $format
*
* @return object|Response
*/
protected function validatePayload($payload, $model, $format)
{
$payload = $this->serializer->deserialize($payload, $model, $format);

$errors = $this->validator->validate($payload);
if (count($errors)) {
return $this->createFailureResponse($errors, $format);
}

return $payload;
}

/**
* @param array|object $content
* @param string $format
* @param int $status
*
* @return Response
*/
protected function createSuccessResponse($content, $format = 'json', $status = 200)
{
return $this->getResponse($content, $format, $status);
}

/**
* @param array|ConstraintViolationListInterface $content
* @param string $format
* @param int $status
*
* @return Response
*/
protected function createFailureResponse($content, $format = 'json', $status = 400)
{
$errorList = null;

if ($content instanceof ConstraintViolationList) {
foreach ($content as $error) {
$error = $this->getErrorFromValidation($error);
$errorList[$error['key']] = $error['value'];
}
} else {
$errorList = $content;
}

return $this->getResponse(['errors' => $errorList], $format, $status);
}

/**
* @param array|object $content
* @param string $format
* @param int $status
*
* @return Response
*/
private function getResponse($content, $format, $status)
{
$context = new SerializationContext();
$context->setSerializeNull(false);

$response = $this->serializer->serialize($content, $format, $context);

return new Response($response, $status, ['Content-Type' => $this->validContentTypes[$format]]);
}

/**
* @param ConstraintViolationInterface $error
*
* @return array
*/
private function getErrorFromValidation($error)
{
$properties = $this->inflector->tableize($error->getPropertyPath());

return ['key' => $properties, 'value' => $error->getMessage()];
}
}

ApiController


namespace Application\BackendBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use JMS\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Doctrine\Common\Inflector\Inflector;

/**
* @Route("api", service="application_backend.controller.api")
*/
class ApiController extends AbstractController
{
public function __construct(
SerializerInterface $serializer,
ValidatorInterface $validator,
Inflector $inflector
) {
parent::__construct($serializer, $validator, $inflector);
}

/**
* @param Request $request
*
* @Method({"POST"})
* @Route("/json")
*
* @return JsonResponse|Response
*/
public function jsonAction(Request $request)
{
$format = $this->validateContentType($request->headers->get('content_type'));
if ($format instanceof Response) {
return $format;
}

$device = $this->validatePayload(
$request->getContent(),
'Application\BackendBundle\Model\Api\Json\Device',
$format
);
if ($device instanceof Response) {
return $device;
}

//print_r($device); exit;

return $this->createSuccessResponse($device, $format);
}
}

Models


Device


namespace Application\BackendBundle\Model\Api\Json;

use Symfony\Component\Validator\Constraints as Assert;
use Application\BackendBundle\Validator\Constraints as BackendBundleAssert;
use JMS\Serializer\Annotation as Serializer;

class Device
{
/**
* @var int
*
* @Assert\NotBlank(message="The id field is required.")
* @Assert\Regex(
* pattern="/^[0-9]+$/",
* message="The id field must contain only numeric character(s)."
* )
*
* @Serializer\Type("integer")
*/
public $id;

/**
* @var string
*
* @Assert\NotBlank(message="The name field is required.")
* @Assert\Length(
* max=255,
* maxMessage="The name field cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
*/
public $name;

/**
* @var float
*
* @Assert\NotBlank(message="The price field is required.")
* @Assert\Regex(
* pattern="/^(?!0\d)\d{1,8}(\.[0-9]{1,2})?$/",
* message="The price field must have a valid format.")
*
* @Serializer\Type("float")
*/
public $price;

/**
* @var bool
*
* @Assert\NotBlank(message="The is_available field is required.")
* @Assert\Type(
* type="bool",
* message="The is_available field must contain only boolean value."
* )
*
* @Serializer\Type("boolean")
*/
public $isAvailable;

/**
* @var string
*
* @Assert\NotBlank(message="The datetime field is required.")
* @BackendBundleAssert\DateTime
*
* @Serializer\Type("string")
*/
public $datetime;

/**
* @var array
*
* @Assert\Count(
* min="1",
* max="2",
* minMessage="You must provide at least {{ limit }} image(s).",
* maxMessage="You can provide maximum {{ limit }} image(s)."
* )
*
* @Serializer\Type("array")
*/
public $images;

/**
* @var Property
*
* @Assert\Valid(traverse="true")
*
* @Serializer\Type("Application\BackendBundle\Model\Api\Json\Property")
*/
public $properties;

/**
* @var Repair[]
*
* @Assert\Valid(traverse="true")
*
* @Serializer\Type("array<Application\BackendBundle\Model\Api\Json\Repair>")
*/
public $repairs = [];

/**
* @var array
*
* @Assert\Count(
* max="10",
* maxMessage="You can provide maximum {{ limit }} random info."
* )
*
* @Serializer\Type("array")
*/
public $randoms = [];
}

Property


namespace Application\BackendBundle\Model\Api\Json;

use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as Serializer;

class Property
{
/**
* @var string
*
* @Assert\NotBlank(message="The made_in field is required.")
* @Assert\Length(
* max=100,
* maxMessage="The made_in field cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
*/
public $madeIn;

/**
* @var array
*
* @Assert\Count(
* max="5",
* maxMessage="You can provide maximum {{ limit }} colour(s)."
* )
*
* @Serializer\Type("array")
*/
public $colours;

/**
* @var Manufacturer
*
* @Assert\Valid(traverse="true")
*
* @Serializer\Type("Application\BackendBundle\Model\Api\Json\Manufacturer")
*/
public $manufacturer;
}

Manufacturer


namespace Application\BackendBundle\Model\Api\Json;

use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as Serializer;

class Manufacturer
{
/**
* @var string
*
* @Assert\NotBlank(message="The name field is required.")
* @Assert\Length(
* max=100,
* maxMessage="The name field cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
*/
public $name;

/**
* @var string
*
* @Assert\NotBlank(message="The phone field is required.")
* @Assert\Length(
* max=20,
* maxMessage="The phone field cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
*/
public $phone;

/**
* @var string
*
* @Assert\NotBlank(message="The email_address field is required.")
* @Assert\Email(checkMX=true, message="The email_address must have a valid format.")
*
* @Serializer\Type("string")
*/
public $emailAddress;
}

Repair


namespace Application\BackendBundle\Model\Api\Json;

use Symfony\Component\Validator\Constraints as Assert;
use Application\BackendBundle\Validator\Constraints as BackendBundleAssert;
use JMS\Serializer\Annotation as Serializer;

class Repair
{
/**
* @var string
*
* @Assert\NotBlank(message="The reason field is required.")
* @Assert\Length(
* max=255,
* maxMessage="The reason field cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
*/
public $reason;

/**
* @var string
*
* @Assert\NotBlank(message="The datetime field is required.")
* @BackendBundleAssert\DateTime
*
* @Serializer\Type("string")
*/
public $datetime;
}

Custom DateTimeValidator


# DateTime.php
namespace Application\BackendBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

/**
* @Annotation
*/
class DateTime extends Constraint
{
const MESSAGE = 'The datetime must be in yyyy-mm-dd H:i:s format.';

public function validatedBy()
{
return get_class($this).'Validator';
}
}

# DateTimeValidator.php
namespace Application\BackendBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use DateTime;

class DateTimeValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (trim($value) == '') {
return;
}

$datetime = DateTime::createFromFormat('Y-m-d H:i:s', $value);

if (!$datetime || $datetime->format('Y-m-d H:i:s') != $value) {
$this->context
->buildViolation($constraint::MESSAGE)
->addViolation();
}
}
}

Example json request


{
"id": 123,
"name": "Samsung Galaxy S4",
"price": 243.11,
"is_available": true,
"datetime": "2015-07-17 20:36:13",
"images": [
"1.jpg",
"2.jpg"
],
"properties": {
"made_in": "South Korea",
"colours": [
"black",
"white",
"gold"
],
"manufacturer": {
"name": "Samsung Group",
"phone": "123567890",
"email_address": "samsung@samsung.com"
}
},
"repairs": [
{
"reason": "Cracked screen",
"datetime": "2014-07-17 12:00:00"
},
{
"reason": "Bubbles in screen",
"datetime": "2014-02-21 17:30:50"
}
],
"randoms": {
"review": "9/10",
"out_of_date": "false",
"stock": 987
}
}

Test


After POSTing the json request payload above to http://football.local/app_dev.php/backend/api/json and if we dump $device in controller just before returning the response, valid output would be like below.


Application\BackendBundle\Model\Api\Json\Device Object
(
[id] => 123
[name] => Samsung Galaxy S4
[price] => 243.11
[isAvailable] => 1
[datetime] => 2015-07-17 20:36:13
[images] => Array
(
[0] => 1.jpg
[1] => 2.jpg
)
[properties] => Application\BackendBundle\Model\Api\Json\Property Object
(
[madeIn] => South Korea
[colours] => Array
(
[0] => black
[1] => white
[2] => gold
)
[manufacturer] => Application\BackendBundle\Model\Api\Json\Manufacturer Object
(
[name] => Samsung Group
[phone] => 123567890
[emailAddress] => samsung@samsung.com
)
)
[repairs] => Array
(
[0] => Application\BackendBundle\Model\Api\Json\Repair Object
(
[reason] => Cracked screen
[datetime] => 2014-07-17 12:00:00
)
[1] => Application\BackendBundle\Model\Api\Json\Repair Object
(
[reason] => Bubbles in screen
[datetime] => 2014-02-21 17:30:50
)
)
[randoms] => Array
(
[review] => 9/10
[out_of_date] => false
[stock] => 987
)
)

Example invalid request result


# 400 Bad request
{
"errors": {
"datetime": "The datetime must be in yyyy-mm-dd H:i:s format.",
"properties.made_in": "The made_in field cannot be longer than 100 character(s).",
"properties.colours": "You can provide maximum 3 colour(s).",
"properties.manufacturer.email_address": "The email_address must have a valid format."
}
}

Another simple json payload example


This is a simple json request payload and its validator class.


Payload


{
"items": [
"ABC",
"DEF",
"GHI"
]
}

Model


class Item
{
/**
* @var array[]
*
* @Assert\Type(type="array", message="You must provide an array of items.")
* @Assert\Count(
* max="5",
* maxMessage="You can provide maximum {{ limit }} item(s)."
* )
*
* @Serializer\Type("array")
*/
public $items = [];
}