Kostiantyn Miakshyn
Contributed by Kostiantyn Miakshyn in #49518

Building API endpoints in Symfony often involves the same repetitive boilerplate: inject the serializer service, call serialize(), create a response with the proper status code, and configure the Content-Type header:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Controller/GetUserController.php
namespace App\Controller;

use App\Model\User;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Serializer\SerializerInterface;

final readonly class GetUserController
{
    public function __construct(
        private SerializerInterface $serializer
    ) {
    }

    public function __invoke(): JsonResponse
    {
        $data = new User(1, 'Jane Smith', '...');
        $serialized = $this->serializer->serialize($data, 'json');

        return JsonResponse::fromJsonString($serialized, JsonResponse::HTTP_CREATED);
    }
}

Symfony 8.1 streamlines this workflow with the new #[Serialize] attribute. After installing and configuring the Serializer component, apply the #[Serialize] attribute to a controller method and return an object or array instead of a full Response object.

Symfony serializes the result using the format derived from the request (JSON by default), sets the Content-Type header, and wraps everything in a Response object automatically:

1
2
3
4
5
6
7
8
9
10
11
// ...
use Symfony\Component\HttpKernel\Attribute\Serialize;

final readonly class GetUserController
{
    #[Serialize]
    public function __invoke(): User
    {
        return new User(1, 'Jane Smith', '...');
    }
}

You can also customize the HTTP status code, response headers, and serialization context passed to the Serializer component:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

final readonly class CreateProductController
{
    #[Serialize(
        code: 201,
        headers: ['X-Custom-Header' => 'abc'],
        context: [DateTimeNormalizer::FORMAT_KEY => 'd.m.Y H:i:s'],
    )]
    public function __invoke(): ProductCreated
    {
        // ... create the product

        return new ProductCreated(101);
    }
}

The response format is determined from the current request format, so the same controller can return JSON or XML depending on the route configuration. For example, a route defined as /products/{id}.{_format} produces JSON for /products/42.json and XML for /products/42.xml.

If the request asks for a format that is not supported by the Serializer component, Symfony automatically returns a 415 Unsupported Media Type response.

Published in #Living on the edge