Updated June 2026. Tested on Symfony 7.x and PHP 8.4. Part of the Techalyst Symfony series.
When you build an API instead of HTML pages, the job changes shape: you turn your objects into JSON for responses, and turn incoming JSON back into objects for requests. Symfony's Serializer handles both directions. Add a couple of features for controlling which fields go out and validating what comes in, and you have everything a clean JSON API needs.
Returning JSON
The quickest path is the controller's json helper, which runs your object through the serializer and wraps it in a response:
#[Route('/api/products/{id}', methods: ['GET'])]
public function show(Product $product): JsonResponse
{
return $this->json($product);
}
That converts the Product entity, its scalar properties and nested objects, into a JSON object. For more control you can inject SerializerInterface and call serialize($product, 'json') yourself, but $this->json covers most endpoints.
Reading JSON in
For a write endpoint, you need the reverse: take the JSON request body and turn it into an object. Modern Symfony does this with the #[MapRequestPayload] attribute, which deserialises the body into a typed object and validates it in one step:
#[Route('/api/products', methods: ['POST'])]
public function create(
#[MapRequestPayload] ProductInput $input,
EntityManagerInterface $em,
): JsonResponse {
// $input is already populated from the JSON body and validated
$product = new Product();
$product->setName($input->name);
$em->persist($product);
$em->flush();
return $this->json($product, 201);
}
If the body is malformed or fails validation, Symfony returns a 422 before your code even runs, so the action only ever sees good input.
Controlling which fields go out
By default the serializer outputs every readable property, which is a problem: you do not want to leak a password hash or an internal flag, and entity relationships can cause circular references. Serialization groups fix this. You tag properties with #[Groups] and then serialise only a named group:
class Product
{
#[Groups(['product:read'])]
private string $name;
#[Groups(['product:read'])]
private int $price;
private string $internalNotes; // no group, never serialised in product:read
}
return $this->json($product, context: ['groups' => 'product:read']);
Now only the product:read fields appear in the response. The same mechanism lets you expose different shapes for different endpoints, a fuller object for a detail view, a slim one for a list, by tagging properties with several groups.
When to reach for API Platform
The Serializer plus a few controllers is plenty for a handful of endpoints. When you are building a large API with filtering, pagination, documentation, and standards like JSON-LD, the API Platform bundle builds all of that on top of these same Symfony pieces. Start with the Serializer to understand the foundation, and adopt API Platform when the API gets big enough to justify it.
Wrapping up
A Symfony JSON API is the Serializer working in both directions: $this->json turns objects into responses, and #[MapRequestPayload] turns request bodies into validated objects. Control the output with serialization groups so you only expose the fields you mean to and avoid circular references, and let validation reject bad input before your action runs. That core handles most APIs, and API Platform is there on top of it when you need a large, standards-based one.
All comments ()
No comments yet
Be the first to leave a comment on this post.