APIs and JSON
An API returns data in JSON rather than pages — for a mobile app or a JavaScript interface. Symfony has the json() method for that and a serializer that turns objects into JSON and back.
A JSON response
#[Route('/api/posts', name: 'api_posts', methods: ['GET'])]
public function index(PostRepository $posts): JsonResponse
{
return $this->json($posts->findAll());
}json() hands objects to the serializer: it reads properties through get methods and builds the JSON.
What goes into the response
Without limits every property goes into the response, including unneeded and secret ones. Groups define the set of fields:
use Symfony\Component\Serializer\Attribute\Groups;
class Post
{
#[Groups(['post:list', 'post:read'])]
private ?int $id = null;
#[Groups(['post:list', 'post:read'])]
private ?string $title = null;
#[Groups(['post:read'])]
private ?string $body = null;
}return $this->json($posts->findAll(), context: ['groups' => ['post:list']]);The list contains only id and title, while a single post with the post:read group also includes the text.
A single record
#[Route('/api/posts/{id}', name: 'api_post', methods: ['GET'])]
public function show(Post $post): JsonResponse
{
return $this->json($post, context: ['groups' => ['post:read']]);
}Receiving data
It is convenient to turn submitted JSON into a separate object with validation rules:
// src/Dto/PostInput.php
namespace App\Dto;
use Symfony\Component\Validator\Constraints as Assert;
class PostInput
{
public function __construct(
#[Assert\NotBlank]
#[Assert\Length(min: 5)]
public string $title,
#[Assert\NotBlank]
public string $body,
) {}
}The #[MapRequestPayload] attribute fills and validates it:
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
#[Route('/api/posts', name: 'api_post_create', methods: ['POST'])]
public function create(
#[MapRequestPayload] PostInput $input,
EntityManagerInterface $em,
): JsonResponse {
$post = new Post();
$post->setTitle($input->title);
$post->setBody($input->body);
$em->persist($post);
$em->flush();
return $this->json($post, 201, context: ['groups' => ['post:read']]);
}If the data fails validation, Symfony itself returns a response with code 422 and a list of errors.
Status codes
| Code | When to return it |
|---|---|
200 | Success |
201 | A record was created |
204 | Success with no content, for example after deleting |
404 | The record was not found |
422 | The data failed validation |
return new JsonResponse(null, 204);API Platform
For a large API there is API Platform — a layer on top of Symfony. Mark an entity with the #[ApiResource] attribute, and all the addresses, pagination, filters and documentation appear.
Common mistake
Related entities refer to each other: a post to its author, the author to their posts. Without groups the serializer goes round in circles and ends with the error A circular reference has been detected. Groups solve this at once: only the marked properties go into the response.