Serializing data in PHP: A simple primer on the JMS Serializer and FoS Rest


Do you want to create a REST API in PHP? Are you struggling to find information about data serialization? Check out this super simple guide

By Merlin Carter

Co-authored with Zoltan Kincses, Senior Developer at Project A

Are you creating REST APIs in Symfony and trying to decide what bundle to use? Are you wondering how you’ll serialize and deserialize data? Welp, we created this article to help you.

What you’ll need to start

If you want to try out this tutorial, you might need to install a few things. It shouldn’t take any more than 20 minutes, even if you have to install every item in this list:

  • PHP 7.2.5+
  • Composer for installing bundles.
  • The latest Symfony CLI for bootstrapping our project.
  • The PHP extensions that Symfony requires.
    Most of these extensions are already included in later versions of PHP, so you could first run the terminal command ‘symfony check:requirements’ to see if you need to install anything extra.
  • JetBrains PhpStorm for generating code.
  • The Postman API Client for making POST requests.

We’ll also be using the following bundles: Maker, FOSRest, JMS Serializer, and FrameworkExtra, but we’ll show you how to install those in the tutorial.

And, of course, you’ll need a basic knowledge of object-oriented programming in PHP.

Check out part two of this tutorial.

What we’ll be doing

We’re going to create a REST endpoint that accepts a JSON payload. For the payload, we’ll send the basic properties of a postal address. Then, we are going to serialize the address details into an object. Finally, we’ll create a response that deserializes the object back into another format.

We’ll be using extremely simple examples, so it won’t take too long.

Why are you using <Technology X> and not <Technology Y>?

Here at Project A, we like to use Symfony. As is the case with a lot of our projects, we tend to use older battle-tested technologies rather than the hottest, up-to-the-minute tech. There tends to be more “community documentation” (my euphemism for answers on Stack Overflow) for stuff that’s been around a while.

That’s why we’re going to show you how to create a REST endpoint with the FoS Rest Bundle and serialize some data with the JMS Serializer.

These two bundles have been partners for many years. They’re like Siegfried and Roy…or Rick and Morty. They’re very often used together (although you can also use FOS Rest with Symfony’s built-in serializer). Mostly because JMS is a more sophisticated serializer and works well with FoS REST — which is natural since Johannes Schmitt, one of the early contributors to FoS Rest (FoS is “Friends of Symfony”), created the JMS Serializer.

There is also a newer, more comprehensive option for creating APIs, but I’ll talk about that at the end of the tutorial.

Here are the major steps:

  1. Create an empty skeleton project.
    We’ll use the Symfony CLI to bootstrap the folder structure and required files, then install a couple of extra bundles.
  2. Create a basic REST endpoint and configure it to accept POST requests.
    We’ll use the Maker bundle to bootstrap a controller, map it to an endpoint address, start a server, then send an empty request to the endpoint just to make sure it works.
  3. Set up the serialization.
    We’ll create an “Address” class, import the JMS Serializer bundle and annotate each address property with serialization instructions.
  4. Make a POST request with a JSON payload.
    We’ll send a POST request containing our address JSON payload and try to get a response back that contains the address as XML.
  5. Make our code more concise.
    We’ll add the FOSRest and FrameworkExtra bundles so that we can use the ParamConverter — which simplifies the serialization process.

So without further ado, let’s get started.


Build a Basic REST Endpoint that Serializes Address Data

Remember, make sure that you’ve installed the Symfony CLI before you proceed.

1. Create an empty PHP Skeleton Project

After you’ve installed all the required dependencies, use Symfony CLI to create a Symfony skeleton project called “jms-tutorial” (or whatever else you want to call it).

Enter the following command:

$ symfony new jms-tutorial
Code language: JavaScript (javascript)

It’s going to create the folder structure and initial files that you need for this tutorial. When it’s done, we need to install the JMS Serializer bundle. Open a terminal, change into the project directory and install it like so:

$ composer require jms/serializer-bundle
Code language: JavaScript (javascript)

We’ll also install a development tool called the “Maker Bundle”. It’s going to save us a bit of time when creating classes. Install it like so:

$ composer require --dev symfony/maker-bundle
Code language: JavaScript (javascript)

Check the composer.json file in the root of your project directory — it should now look something like this:

{
    "type": "project",
    "license": "proprietary",
    "require": {
        "php": ">=7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "symfony/console": "5.1.*",
        "symfony/dotenv": "5.1.*",
        "symfony/flex": "^1.3.1",
        "symfony/framework-bundle": "5.1.*",
        "symfony/yaml": "5.1.*",
        "jms/serializer-bundle": "^3.7",
    },
    "require-dev": {
        "symfony/maker-bundle": "^1.23"
    },
}
Code language: JSON / JSON with Comments (json)

2. Create a Basic Endpoint and Test It

To create an endpoint, we first need to create a controller, which is a class method that accepts requests and returns responses.

After you map it to a URL, you can do useful things with it. For example, you can have it dynamically return an HTML page when you visit a URL like http://localhost:8000/mypage (HTTP Get).

In our case, though, we want to end up with an endpoint that we can eventually POST data to.

Here, you’ll see why we installed the Maker bundle.

  • To auto-generate a boilerplate controller, run the following command
$ bin/console make:controller
Code language: JavaScript (javascript)
  • When prompted, give it the name “TestAddress”. It will generate the following file: ./src/Controller/TestAddressController.php
  • If you open the file, it should look like this:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class TestAddressController extends AbstractController
{
    /**
     * @Route("/test/address", name="Test_Address")
     */
    public function index(): Response
    {
        return $this->json([
            'message' => 'Welcome to your new controller!',
            'path' => 'src/Controller/TestAddressController.php',
        ]);
    }
}
Code language: HTML, XML (xml)

Of course, you can also manually create your own controller class, but the Maker bundle does most of the work for you.

Let’s take a close look at what it has done:

  • It has included the necessary dependencies, such as the Response and Route components
  • It has already autogenerated a URL based on the controller name and mapped it to a route with the @Route annotation.
  • It has included an AbstractController class which our new “TestAddressController” extends.
  • This AbstractController is a base controller which includes shortcuts for common tasks such as rendering templates or checking security permissions — so it’s considered a best practice to use it.

To test that it’s working, let’s start Symfony’s built-in server.

  • In your project root, run the following command:
$ symfony serve

To test that it’s working, open the following URL in your web browser:

http://localhost:8000/test/address

The response should resemble the following screenshot.


3. Add POST Action

Since we want to be able to send data to this endpoint, we need to update it, so it supports POST requests (by default, the controller only supports GET requests).

We do this by adding the methods attribute to the route.

  • First, remove the default index function that got auto-generated with the controller (that starts with “public function index()”) and replace it with the following function:
public function postAddress(): Response    
{        
       return $this->json([]);    
}
Code language: PHP (php)

With return $this->json([]), we‘re removing the default “welcome..” response so that the endpoint returns empty JSON for now.

We’re also renaming the function to postAddress so it’s clearer that we’re responding to a POST request.

Next, we actually need to enable POST requests by updating the @Route annotation above the function.

  • Edit the following line:
@Route("/test/address", name="Test_Address")
Code language: JavaScript (javascript)
  • Remove the name attribute (we don’t really need it here) and add methods={“POST”} so that it looks like this:
@Route("test/address", methods={"POST"})
Code language: JavaScript (javascript)

The controller should now resemble the following example:

<?php
...
class TestAddressController extends AbstractController
{
    /**
     * @Route("/test/address", methods={"POST"})
     */
    public function postAddress(): Response
    {
        return $this->json([]);
    }
}
Code language: HTML, XML (xml)

To test that it works, you’ll now need to fire up Postman instead of your browser — you’ll need it easily send POST requests.

  • Send an empty POST request to your endpoint — as mentioned before, you should get back an empty JSON.

OK, all good? Now we want to send it some actual data and have that data serialized.


4. Define our Expected Data and Create a Matching Entity Class

First, we need to know the shape of the data that our endpoint is going to accept. We’ve established that it’s going to accept address details, but of course, addresses can be formatted in many different ways.

Let’s say the endpoint accepts addresses formatted like so:

{
    "name": "Horst Konrader",
    "line1": "Konis Hupen GmbH",
    "line2": "Hochtirolerstr. 88",
    "line3": "3rd floor",
    "city": "Berlin",
    "postcode": "10405",
    "country": "Germany"
}
Code language: JSON / JSON with Comments (json)

To make life easy for now, let’s say any of these properties can be empty, and they’re all strings.

We need to codify these rules with an entity class. To do so quickly, follow these steps:

  1. In the “src” folder, create the folder “Entity”.
  2. In PhpStorm, right-click the folder and select New > PHP Class
  3. In the dialog that appears, enter “Address” for the class name and file name.
  4. In the “Entity” folder, open the newly created “Address.php” file

When you inspect Address.php, you’ll see an empty class like this:

class Address
{
}
Code language: JavaScript (javascript)

Note that this process would have been even faster if we’d used the make: command (like we did with the controller). BUT the make:entity command is designed to generate entities that are written to a database (and we’re not going that far yet). If we use this command now, the Symfony console will complain about missing libraries and whatnot. Keep this in mind for later, though.

First, add the following annotation above the class.

/** 
* @Serializer\ExclusionPolicy('all') 
*/
class Address
{
Code language: PHP (php)

We want to exclude all properties for serialization by default in case the client keeps adding or renaming properties in the request body. For example, imaging new requests contained a “salutation” property. We wouldn’t know how to handle that new data — so it’s better if we explicitly expose only the properties we want to serialize.

Let’s do this now and define the properties that we’re expecting in the body of incoming POST requests.

  • Add the first property, “name”, like this:
/**     
* @Serializer\Expose()     
* @Serializer\Type('string')     
* @var string|null     
*/
private $name;
Code language: PHP (php)
  • Add the “line1”, “line2”, and “line3” properties with an extra SerializedName annotation like this:
/**     
* @Serializer\Expose()     
* @Serializer\Type('string')     
* @Serializer\SerializedName('line1')
* @var string|null
*/
private $lineOne;
Code language: PHP (php)

Obviously, for the subsequent properties, change the numbers to match. I’ll explain what all those annotations mean in a second. First, make sure that your Address class resembles the following example:

<?php
...
     
use JMS\Serializer\Annotation as Serializer;
/**
 * @Serializer\ExclusionPolicy("all")
 */
class Address
{
     /**
     * @Serializer\Expose()
     * @Serializer\Type("string")
     *
     * @var string|null
     */
    private $name;
    
    /**
     * @Serializer\Expose()
     * @Serializer\Type("string")
     * @Serializer\SerializedName("line1")
     *
     * @var string|null
     */
    private $lineOne;
...
Code language: HTML, XML (xml)

OK, so what’s with all the annotations? As you can guess, the ones that begin with “@Serializer” are configuration instructions for the JMS Serializer library.

  • Expose() is the instruction “please serialize this property as an exception to my exclude all policy”.
  • Type() defines how to serialize the incoming data. For our address details, we’re just using the simple type “string”. You can find out what other types are supported in the documentation.
  • SerializedName() helps you map an internal property name to a property name that differs in the request body. Usually, this mapping is done automatically, so you don’t always need this annotation.
     — However, in our request example, the address lines are called “line1” and “line2”, and so forth. 
     — Suppose that our PHP coding standards dictate that property names can’t contain digits, so internally we’ve called them “lineOne” and “lineTwo”.
     — The SerializedName() annotation maps the external name to our internal name.
  • The @var annotations are typically used for property documentation, but they can also help us automatically generate Getters for each property (as you’ll see in a minute). The syntax “string|null” means that the property can be a string or empty.

Now comes the code generation:

  • In PhpStorm, select Code > Generate > Getters, select all the properties in the dialog that appears, and click OK.
Code Generation Dialog

Underneath your property list, you’ll now have a bunch of matching Getter functions like this:

<?php    
...
    private $country;
    public function getName(): ?string
    {
        return $this->name;
    }
    public function getLineOne(): ?string
    {
        return $this->lineOne;
    }
    public function getLineTwo(): ?string
    {
        return $this->lineTwo;
    }
    public function getLineThree(): ?string
    {
        return $this->lineThree;
    }
    public function getPostcode(): ?string
    {
        return $this->postcode;
    }
    public function getCountry(): ?string
    {
        return $this->country;
    }
}
Code language: HTML, XML (xml)

Don’t worry if your result doesn’t look exactly like the previous example — we removed all the “* @return string” annotations for readability.

You might also be wondering, “where’s the address constructor?”. For simplicity, we’re going to omit it. We’re only using Getters, so we’ll be creating immutable address objects. Behind the scenes, Symfony uses PHP’s object reflection API to create the address objects instead. But you don’t need to worry about that for now.


5. Add Serializer Constructor

Now that we’ve prepared our entity let’s add some logic to apply it to the incoming data. We do this by updating our controller and adding a constructor for the serializer interface.

Again, we’ll use a bit of code generation magic in PhpStorm. At the top of the TestAddressController class, add the following annotated property:

/**
 * @var SerializerInterface
 */
private $serializer;
Code language: PHP (php)

The class should now look like this:

<?php 
...
class TestAddressController extends AbstractController
{
    /**
     * @var SerializerInterface
     */
    private $serializer;
    
...
Code language: HTML, XML (xml)

In PhpStorm, navigate to Code > Generate > Constructor > Select “serializer”. PhpStorm will generate a constructor function underneath the serializer property like this:

<?php 
...
class TestAddressController extends AbstractController
{
    /**
     * @var SerializerInterface
     */
    private $serializer;
    public function __construct(SerializerInterface $serializer)
    {
        $this->serializer = $serializer;
    }
...
Code language: HTML, XML (xml)

We can then use this SerializerInterface for serialization and deserialization.


6. Serialize the data into XML

To test the serialization, let’s update the endpoint to return our serialized data in a different format from what we received.

Since our incoming data is JSON, let’s try and serialize it into XML and return the XML in the response.

First, we need to update the postAddress function and pass the request body into it.

  • Update the function from:
public function postAddress(): Response
Code language: PHP (php)
  • To:
public function postAddress(Request $request): Response
Code language: PHP (php)

Now, add the following variable, which we are configuring to store the deserialized data (from the content of the request body JSON):

$a = $this->serializer->deserialize($request->getContent(), Address::class, 'json');
Code language: PHP (php)

Finally, let’s update the response. Instead of returning empty JSON, we’ll update it to serialize the data back into another format — namely, XML.

return new Response($this->serializer->serialize($a, 'xml'));
Code language: PHP (php)

This helps us test that it’s working in both directions. Your updated TestAddressController class should now resemble the following example:

<?php
...
class TestAddressController extends AbstractController
{
    /**
     * @Route("/test/address", methods={"POST"})
     */
    public function postAddress(Request $request): Response
    {
        $a = $this->serializer->deserialize($request->getContent(), Address::class, 'json');
        return new Response($this->serializer->serialize($a, 'xml'));
    }
   
}
Code language: HTML, XML (xml)

Alright, start the Symfony server if it’s not running already. Then let’s fire up Postman again and hit our endpoint to test if we get an XML response.

Yessss. XML.

OK, now let’s try and simplify our controller a bit by adding a couple of extra libraries that allow us to do the same task with more concise code.


7. Install Bundles That Will Allow Us to Do It More Elegantly

So we did the job and serialized the stuff. But wouldn’t it be nice if the code was a bit cleaner? take a look at this elaborate line:

$a = $this->serializer->deserialize($request->getContent(), Address::class, ‘json’);
Code language: PHP (php)

It’s basically saying, “Hey serializer → go and deserialize this data → which is in the POST request → and go get its content (it’s JSON) and create an instance of the Address class.

That’s pretty verbose — we can also just say, “here’s an address request, go make an address object”.

I’ll show you the code for that in a moment, but first, we need to install a couple of extra Bundles: FOSRest and SensioFrameworkExtra

These two bundles have some handy functions which will allow us to simplify our code somewhat.

Install FOSRest like so…

$ composer require friendsofsymfony/rest-bundle
Code language: JavaScript (javascript)

…and SensioFrameworkExtra like so:

$ composer require sensio/framework-extra-bundle
Code language: JavaScript (javascript)

Now, we want to update the imports in TestAddressController.php — add these extra imports to the top of your file:

use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use FOS\RestBundle\Controller\AbstractFOSRestController;
Code language: PHP (php)

Remember: Currently, our TestAddressController class extends Symfony’s standard base controller (AbstractController). Let’s use a different base controller with more features — the AbstractFOSRestController, which we just imported.

Update your class declaration to extend the AbstractFOSRestController (instead of the AbstractController):

class TestAddressController extends Abstract FOSRest Controller
Code language: JavaScript (javascript)

And what about that ParamConverter thing? It’s gonna help us do some magic.


8. Use Our Newly Acquired Powers to Simplify Our code

Remember the verbose instruction that we assigned to the $a variable? Now we have the tools to make it more concise.

To start, let’s update the annotation for the PostAddress, and add an extra ParamConverter annotation like so:

* @Route("/test/address", methods={"POST"})
* @ParamConverter("address", converter="fos_rest.request_body")
Code language: JavaScript (javascript)

This is going to deserialize the request body into an object — just like what we were doing before but with a lot less code.

More specifically, we’re using the “Request Body Converter Listener” (FOSRestBundle) to pass the request body to the ParamConverter (SensioFrameworkExtraBundle), which we’re going to convert into an address object — after we make one more change.

Update the postAddress function and replace “Request $request” with “Address $address” like so:

public function postAddress(Address $address): Response
Code language: PHP (php)

This injects a new instance of our Address class into the $address variable (instead of injecting the entire request body like we were doing before).

Since the ParamConverter already took care of the deserialization for us, we can now remove this line completely!

$a = $this->serializer->deserialize($request->getContent(), Address::class, ‘json’);
Code language: PHP (php)

We can also remove the constructor for the SerializerInterface that we generated earlier:

/**
 * @var SerializerInterface
 */
private $serializer;
public function __construct(SerializerInterface $serializer)     
{         
    $this->serializer = $serializer;     
}
Code language: PHP (php)

By now, your TestAddressController class should look like this:

<?php
...
class TestAddressController extends AbstractFOSRestController
{    
    /**
     * @Route("/test/address", methods={"POST"})
     * @ParamConverter("address", converter="fos_rest.request_body")
     */
    public function post(Address $address): Response
    {
        return new Response($this->serializer->serialize($a, 'xml'));
    }
}
Code language: HTML, XML (xml)

Much cleaner, no? But there’s a problem. What about our response? it’s trying to serialize $a, which we just removed. We could just replace $a with $address, but we could also handle the response more concisely.

Let’s use the “view layer” from the base controller class, AbstractFOSRestController — it will take care of the serialization for us. To do this, replace the following line:

return new Response($this->serializer->serialize($a, 'xml'));
Code language: PHP (php)

With this one:

return $this->handleView($this->view($address));
Code language: PHP (php)

A view is essentially a layer between the Controller and the serializer. The handleView action processes the view and returns a serialized response. But in what format? Well, that’s now up to whoever is making the request.

Here’s how your controller should look in its final state:

<?php
...
class TestAddressController extends AbstractFOSRestController
{
    /**
     * @Route("/test/address", methods={"POST"})
     * @ParamConverter("address", converter="fos_rest.request_body")
     */
    public function post(Address $address): Response
    {
        return $this->handleView($this->view($address));
    }
}
Code language: HTML, XML (xml)

Notice that in our view-based approach, we’re not defining a format anymore.

So how do we get back XML like in the previous example? By defining the format that we want in our request header. The View Handler will try and figure out the format to return based on that piece of info.

Handling the View

But, but, but — you also need to configure the FOSRest Bundle so that XML is in your list of allowed formats (you don’t want your endpoint returning just ANY format that requesters demand — like “.xbap” or somesuch madness).

In your project directory, open the following file:

./config/packages/fos_rest.yaml

Update the format_listener section so that it resembles the following example:

fos_rest:
    body_converter:
        enabled: true
    allowed_methods_listener:  true
    format_listener:
        rules:
            - { path: '^/', prefer_extension: true, fallback_format: json, priorities: [ 'json', 'xml'] }
Code language: CSS (css)

Specifically, you want to make sure that ‘xml’ is in the list of format priorities (priorities: [ ‘json’, ‘xml’]). If it’s not there, we’re never going to get XML back.

For more specific info on how these format rules work, see the format listener documentation.

Once we’ve checked our FOS Rest configuration, let’s open Postman again and put ‘Accept: application/xml’ in the request header to see if we indeed got what we were expecting.

Oh yes! Ask, and ye shall receive…..that sweet, sweet XML…again. And we didn’t have to specify the format in our controller. The client can ask for whatever it wants as long as it’s in our list of allowed formats. That’s the magic of the FOS Rest bundle.


Summary

That was already quite a lot, but we’ve only scratched the surface of what this serializer can do. But this work can get quite tedious once you have a lot of entities to serialize. In another tutorial, we’ll show you how to use serializer groups to make the whole process more scalable. And we’ll show you how to write to a database, update the data, do data validation, and make your API production-ready with authorization and authentication.

Why we created this tutorial

We decided the world needed this tutorial because the documentation for FOS Rest and the JMS serializer is patchy at best. This is understandable since, for open-source libraries, maintainers are overloaded with work and tend to prioritize improvements to the functionality.

What about API Platform?

As an alternative to the FOS Rest library, there is a newer, much larger project called API Platform that has better documentation. And it also works with the JMS serializer. However, it handles a much larger range of use cases, so there’s more to think about and configure (that’s why there’s so much documentation).

We’re still friends of the friends… of Symfony REST bundle

Our PHP team still uses the FoS Rest bundle because it’s pretty simple and reliable and also powerful when paired with the JMS Serializer. But some functions aren’t documented that well, which is why we decided to write this guide. Hopefully, it made your learning curve shorter than ours!