Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

CodeWhen to return it
200Success
201A record was created
204Success with no content, for example after deleting
404The record was not found
422The 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.

Thanks for reading!

Donate to the site — we work hard for you.