When you ever develop an API in the Symfony framework1, you probably heard of the serializationGroup option. It allows you to define different groups of attributes to serialize and deserialize. However, defining these groups without proper consideration may make the whole development hard, e.g., if you need to deal with different scopes of the object’s attributes in many other endpoints.

I want to show you our approach to serialization groups in one of our projects. Together with my team, we use them to control the serialization of related entities on various endpoints in our API.

Context

It’ll be more comfortable if I provide some context. Instead of talking about entities from the actual project, I’ll use a generic example: Order and Product.

Our case is a RESTish2 API for a relatively small application. Some endpoints respond with entities with a relation or collection of other related entities to avoid unnecessary calls. To generate responses, we use the Serializer component. For handling requests, we rely on Symfony Form component3.

The main challenge is to return mandatory data for each scenario.

  • If the user calls for the Products collection, he gets a collection with basic data about each Product.

  • If the user calls for a specific Product, he gets very detailed data about the requested Product.

Schematic responses from the API when user calls _/products_ endpoint
Schematic responses from the API when user calls /products endpoint

But it’s not all.

  • If the user calls for the Orders collection, he gets a collection with a basic data about each Order. Each order has a collection containing basic data about each Product in the order.

  • If the user calls for specific Order, he gets very detailed data about the requested Order. Each order has a collection containing basic data about each Product in the order.

Schematic responses from the API when user calls _/orders_ endpoint
Schematic responses from the API when user calls /orders endpoint

Product and Order are only examples. Imagine many different cases where you need to attach some extra data to a resource, but you don’t want to connect the whole object. Sometimes you don’t need to show the related items at all. Another time, in the case of other endpoints, you need to include very detailed data.

Two groups for entities are not enough

Our first solution consisted of two groups: basic and extend.

We classified each property for one of them, and they were mutually exclusive – a single field should be classified either as basic or extend. Unfortunately, this setup didn’t let us configure the properties of nested resources properly. Whenever we wanted to retrieve a full set of properties from the primary resource and some basic data from related entities, we always wind up with an extend version on each entity.

Two groups for each entity individually

To have more control over the serialization, we decided to use an entity name as a prefix for each serialization group. For Order and Product entities, it looks as follows:

  • order_basic
  • order_extend
  • product_basic
  • product_extend

Using this approach, we can serialize, e.g., the full set of properties from Order and only basic properties from its assigned Products.

Separate group for relationship

So, we split properties in each entity into basic and extend groups, but where does the collection of products belong?

Basically, it depends on the use case. However, we came up with another convention for all types of relations between entities for the sake of flexibility. Each association has assigned a dedicated group where the name consists of both owner and related entity name, e.g., order_with_products.

class Order
{
    /**
     * @Serializer\Groups({"order_basic"})
     */
    private $id;

    /**
     * @Serializer\Groups({"order_extend"})
     */
    private $createdAt;

    /**
     * @Serializer\Groups({"order_with_products"})
     */
    private $products;

    // rest method omitted
}

How we use it

Thanks to this separation of groups, we could compose responses using specific subset of properties from entities and related objects.

So, come back to the scenarios I use in the Context section. Let’s compose proper responses for each of them. I won’t describe all entities – details aren’t needed here. The @Rest/View annotation comes from the FOSRestBundle.

First, ProductController:

class ProductController extends AbstractController
{
    /**
     * @Rest/View(serializationGroups={"product_basic"})
     **/
    public function showAllProducts()
    {
        return $this->repository->findAll(); 
    }
  
    /**
     * @Rest/View(serializationGroups={"product_basic", "product_extends"})
     **/
    public function showProduct(Product $product)
    {
        return $product; 
    }
}

Next, OrderController:

class OrderController extends AbstractController
{
    /**
     * @Rest/View(serializationGroups={
     *     "order_basic", 
     *     "order_with_products", 
     *     "product_basic"
     * })
     **/
    public function showAllOrders()
    {
        return $this->repository->findAll(); 
    }

    /**
     * @Rest/View(serializationGroups={
     *     "order_basic", 
     *     "order_extend", 
     *     "order_with_products", 
     *     "product_basic"
     * })
     **/
    public function showOrder(Order $order)
    {
        return $order; 
    }
}

Summary

The serializationGroup option is powerful, not only when it comes to generating responses like this but also when dealing with different responses that depend on the user’s role or status. There’re a lot of cases when we can use it.

I don’t know how it’ll work in a bigger project with many more entities. Still, in my opinion, as long as the relations between entities are carefully designed, everything should be manageable. In our case, it does the job.

If you have more experience with it, or if you follow another approach, let me know. I’m curious how people use it in their projects.

Featured photo by Tamara Gak on Unsplash.


  1. Or any other API that relies on the Serializer component. ↩︎

  2. It’s not a pure REST API – we broke some REST assumptions for convenience. ↩︎

  3. API request with data could be mapped as a standard form, so why not? ↩︎