Skip to content

How to Use the Serializer

Edit this page

Symfony provides a serializer to transform data structures from one format to PHP objects and the other way around.

This is most commonly used when building an API or communicating with third party APIs. The serializer can transform an incoming JSON request payload to a PHP object that is consumed by your application. Then, when generating the response, you can use the serializer to transform the PHP objects back to a JSON response.

It can also be used to for instance load CSV configuration data as PHP objects, or even to transform between formats (e.g. YAML to XML).

Installation

In applications using Symfony Flex, run this command to install the serializer Symfony pack before using it:

1
$ composer require symfony/serializer-pack

Note

The serializer pack also installs some commonly used optional dependencies of the Serializer component. When using this component outside the Symfony framework, you might want to start with the symfony/serializer package and install optional dependencies if you need them.

See also

A popular alternative to the Symfony Serializer component is the third-party library, JMS serializer.

Serializing an Object

For this example, assume the following class exists in your project:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/Model/Person.php
namespace App\Model;

class Person
{
    public function __construct(
        private int $age,
        private string $name,
        private bool $sportsperson
    ) {
    }

    public function getAge(): int
    {
        return $this->age;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function isSportsperson(): bool
    {
        return $this->sportsperson;
    }
}

If you want to transform objects of this type into a JSON structure (e.g. to send them via an API response), get the serializer service by using the SerializerInterface parameter type:

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

use App\Model\Person;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\SerializerInterface;

class PersonController extends AbstractController
{
    public function index(SerializerInterface $serializer): Response
    {
        $person = new Person('Jane Doe', 39, false);

        $jsonContent = $serializer->serialize($person, 'json');
        // $jsonContent contains {"name":"Jane Doe","age":39,"sportsperson":false}

        return JsonResponse::fromJsonString($jsonContent);
    }
}

The first parameter of the serialize() is the object to be serialized and the second is used to choose the proper encoder (i.e. format), in this case the JsonEncoder.

Tip

When your controller class extends AbstractController (like in the example above), you can simplify your controller by using the json() method to create a JSON response from an object using the Serializer:

1
2
3
4
5
6
7
8
9
10
class PersonController extends AbstractController
{
    public function index(): Response
    {
        $person = new Person('Jane Doe', 39, false);

        // when the Serializer is not available, this will use json_encode()
        return $this->json($person);
    }
}

Using the Serializer in Twig Templates

You can also serialize objects in any Twig template using the serialize filter:

1
{{ person|serialize(format = 'json') }}

See the twig reference for more information.

Deserializing an Object

APIs often also need to convert a formatted request body (e.g. JSON) to a PHP object. This process is called deserialization (also known as "hydration"):

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

// ...
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request;

class PersonController extends AbstractController
{
    // ...

    public function create(Request $request, SerializerInterface $serializer): Response
    {
        if ('json' !== $request->getContentTypeFormat()) {
            throw new BadRequestException('Unsupported content format');
        }

        $jsonData = $request->getContent();
        $person = $serializer->deserialize($jsonData, Person::class, 'json');

        // ... do something with $person and return a response
    }
}

In this case, deserialize() needs three parameters:

  1. The data to be decoded
  2. The name of the class this information will be decoded to
  3. The name of the encoder used to convert the data to an array (i.e. the input format)

When sending a request to this controller (e.g. {"first_name":"John Doe","age":54,"sportsperson":true}), the serializer will create a new instance of Person and sets the properties to the values from the given JSON.

Note

By default, additional attributes that are not mapped to the denormalized object will be ignored by the Serializer component. For instance, if a request to the above controller contains {..., "city": "Paris"}, the city field will be ignored. You can also throw an exception in these cases using the serializer context you'll learn about later.

See also

You can also deserialize data into an existing object instance (e.g. when updating data). See Deserializing in an Existing Object.

The Serialization Process: Normalizers and Encoders

The serializer uses a two-step process when (de)serializing objects:

In both directions, data is always first converted to an array. This splits the process in two seperate responsibilities:

Normalizers
These classes convert objects into arrays and vice versa. They do the heavy lifting of finding out which class properties to serialize, what value they hold and what name they should have.
Encoders
Encoders convert arrays into a specific format and the other way around. Each encoder knows exactly how to parse and generate a specific format, for instance JSON or XML.

Internally, the Serializer class uses a sorted list of normalizers and one encoder for the specific format when (de)serializing an object.

There are several normalizers configured in the default serializer service. The most important normalizer is the ObjectNormalizer. This normalizer uses reflection and the PropertyAccess component to transform between any object and an array. You'll learn more about this and other normalizers later.

The default serializer is also configured with some encoders, covering the common formats used by HTTP applications:

Read more about these encoders and their configuration in Serializer Encoders.

Tip

The API Platform project provides encoders for more advanced formats:

Serializer Context

The serializer, and its normalizers and encoders, are configured through the serializer context. This context can be configured in multiple places:

You can use all three options at the same time. When the same setting is configured in multiple places, the latter in the list above will override the previous one (e.g. the setting on a specific property overrides the one configured globally).

Configure a Default Context

You can configure a default context in the framework configuration, for instance to disallow extra fields while deserializing:

1
2
3
4
5
# config/packages/serializer.yaml
framework:
    serializer:
        default_context:
            allow_extra_attributes: false

Pass Context while Serializing/Deserializing

You can also configure the context for a single call to serialize()/deserialize(). For instance, you can skip properties with a null value only for one serialize call:

1
2
3
4
5
6
7
8
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;

// ...
$serializer->serialize($person, 'json', [
    AbstractObjectNormalizer::SKIP_NULL_VALUES => true
]);

// next calls to serialize() will NOT skip null values
Using Context Builders

6.1

Context builders were introduced in Symfony 6.1.

You can use "context builders" to help define the (de)serialization context. Context builders are PHP objects that provide autocompletion, validation, and documentation of context options:

1
2
3
4
5
use Symfony\Component\Serializer\Context\Normalizer\DateTimeNormalizerContextBuilder;

$contextBuilder = (new DateTimeNormalizerContextBuilder())
    ->withFormat('Y-m-d H:i:s');
$serializer->serialize($something, 'json', $contextBuilder->toArray());

Each normalizer/encoder has its related context builder. To create a more complex (de)serialization context, you can chain them using the withContext() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\Serializer\Context\Encoder\CsvEncoderContextBuilder;
use Symfony\Component\Serializer\Context\Normalizer\ObjectNormalizerContextBuilder;

$initialContext = [
    'custom_key' => 'custom_value',
];

$contextBuilder = (new ObjectNormalizerContextBuilder())
    ->withContext($initialContext)
    ->withGroups(['group1', 'group2']);

$contextBuilder = (new CsvEncoderContextBuilder())
    ->withContext($contextBuilder)
    ->withDelimiter(';');

$serializer->serialize($something, 'csv', $contextBuilder->toArray());

See also

You can also create your context builders to have autocompletion, validation, and documentation for your custom context values.

Configure Context on a Specific Property

At last, you can also configure context values on a specific object property. For instance, to configure the datetime format:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Model/Person.php

// ...
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

class Person
{
    #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
    public \DateTimeImmutable $createdAt;

    // ...
}

Note

When using YAML or XML, the mapping files must be placed in one of these locations:

  • All *.yaml and *.xml files in the config/serializer/ directory.
  • The serialization.yaml or serialization.xml file in the Resources/config/ directory of a bundle;
  • All *.yaml and *.xml files in the Resources/config/serialization/ directory of a bundle.

You can also specify a context specific to normalization or denormalization:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/Model/Person.php

// ...
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

class Person
{
    #[Context(
        normalizationContext: [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'],
        denormalizationContext: [DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339],
    )]
    public \DateTimeImmutable $createdAt;

    // ...
}

You can also restrict the usage of a context to some groups:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Model/Person.php

// ...
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

class Person
{
    #[Groups(['extended'])]
    #[Context([DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339])]
    #[Context(
        context: [DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339_EXTENDED],
        groups: ['extended'],
    )]
    public \DateTimeImmutable $createdAt;

    // ...
}

The attribute can be repeated as much as needed on a single property. Context without group is always applied first. Then context for the matching groups are merged in the provided order.

If you repeat the same context in multiple properties, consider using the #[Context] attribute on your class to apply that context configuration to all the properties of the class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace App\Model;

use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

#[Context([DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339])]
#[Context(
    context: [DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339_EXTENDED],
    groups: ['extended'],
)]
class Person
{
    // ...
}

Serializing to or from PHP Arrays

The default Serializer can also be used to only perform one step of the two step serialization process by using the respective interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
// ...

class PersonController extends AbstractController
{
    public function index(DenormalizerInterface&NormalizerInterface $serializer): Response
    {
        $person = new Person('Jane Doe', 39, false);

        // use normalize() to convert a PHP object to an array
        $personArray = $serializer->normalize($person, 'json');

        // ...and denormalize() to convert an array back to a PHP object
        $personCopy = $serializer->denormalize($personArray, Person::class);

        // ...
    }

    public function json(DecoderInterface&EncoderInterface $serializer): Response
    {
        $data = ['name' => 'Jane Doe'];

        // use encode() to transform PHP arrays into another format
        $json = $serializer->encode($data, 'json');

        // ...and decode() to transform any format to just PHP arrays (instead of objects)
        $data = $serializer->decode('{"name":"Charlie Doe"}', 'json');
        // $data contains ['name' => 'Charlie Doe']
    }
}

Ignoring Properties

The ObjectNormalizer normalizes all properties of an object and all methods starting with get*(), has*(), is*() and can*(). Some properties or methods should never be serialized. You can exclude them using the #[Ignore] attribute:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Model/Person.php
namespace App\Model;

use Symfony\Component\Serializer\Attribute\Ignore;

class Person
{
    // ...

    #[Ignore]
    public function isPotentiallySpamUser(): bool
    {
        // ...
    }
}

The potentiallySpamUser property will now never be serialized:

1
2
3
4
5
6
7
8
9
10
11
12
13
use App\Model\Person;

// ...
$person = new Person('Jane Doe', 32, false);
$json = $serializer->serialize($person, 'json');
// $json contains {"name":"Jane Doe","age":32,"sportsperson":false}

$person1 = $serializer->deserialize(
    '{"name":"Jane Doe","age":32,"sportsperson":false","potentiallySpamUser":false}',
    Person::class,
    'json'
);
// the "potentiallySpamUser" value is ignored

Ignoring Attributes Using the Context

You can also pass an array of attribute names to ignore at runtime using the ignored_attributes context options:

1
2
3
4
5
6
7
8
9
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;

// ...
$person = new Person('Jane Doe', 32, false);
$json = $serializer->serialize($person, 'json',
[
    AbstractNormalizer::IGNORED_ATTRIBUTES => ['age'],
]);
// $json contains {"name":"Jane Doe","sportsperson":false}

However, this can quickly become unmaintainable if used excessively. See the next section about serialization groups for a better solution.

Selecting Specific Properties

Instead of excluding a property or method in all situations, you might need to exclude some properties in one place, but serialize them in another. Groups are a handy way to achieve this.

You can add the #[Groups] attribute to your class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Model/Person.php
namespace App\Model;

use Symfony\Component\Serializer\Attribute\Groups;

class Person
{
    #[Groups(["admin-view"])]
    private int $age;

    #[Groups(["public-view"])]
    private string $name;

    #[Groups(["public-view"])]
    private bool $sportsperson;

    // ...
}

You can now choose which groups to use when serializing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$json = $serializer->serialize(
    $person,
    'json',
    ['groups' => 'public-view']
);
// $json contains {"name":"Jane Doe","sportsperson":false}

// you can also pass an array of groups
$json = $serializer->serialize(
    $person,
    'json',
    ['groups' => ['public-view', 'admin-view']]
);
// $json contains {"name":"Jane Doe","age":32,"sportsperson":false}

// or use the special "*" value to select all groups
$json = $serializer->serialize(
    $person,
    'json',
    ['groups' => '*']
);
// $json contains {"name":"Jane Doe","age":32,"sportsperson":false}

Using the Serialization Context

At last, you can also use the attributes context option to select properties at runtime:

1
2
3
4
5
6
7
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
// ...

$json = $serializer->serialize($person, 'json', [
    AbstractNormalizer::ATTRIBUTES => ['name', 'company' => ['name']]
]);
// $json contains {"name":"Dunglas","company":{"name":"Les-Tilleuls.coop"}}

Only attributes that are not ignored are available. If serialization groups are set, only attributes allowed by those groups can be used.

Handling Arrays

The serializer is capable of handling arrays of objects. Serializing arrays works just like serializing a single object:

1
2
3
4
5
6
7
8
9
10
use App\Model\Person;

// ...
$person1 = new Person('Jane Doe', 39, false);
$person2 = new Person('John Smith', 52, true);

$persons = [$person1, $person2];
$JsonContent = $serializer->serialize($persons, 'json');

// $jsonContent contains [{"name":"Jane Doe","age":39,"sportsman":false},{"name":"John Smith","age":52,"sportsman":true}]

To deserialize a list of objects, you have to append [] to the type parameter:

1
2
3
4
// ...

$jsonData = ...; // the serialized JSON data from the previous example
$persons = $serializer->deserialize($JsonData, Person::class.'[]', 'json');

For nested classes, you have to add a PHPDoc type to the property/setter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Model/UserGroup.php
namespace App\Model;

class UserGroup
{
    private array $members;

    // ...

    /**
     * @param Person[] $members
     */
    public function setMembers(array $members): void
    {
        $this->members = $members;
    }
}

Tip

The Serializer also supports array types used in static analysis, like list<Person> and array<Person>. Make sure the phpstan/phpdoc-parser and phpdocumentor/reflection-docblock packages are installed (these are part of the symfony/serializer-pack).

Deserializing Nested Structures

6.2

The option to configure a SerializedPath was introduced in Symfony 6.2.

Some APIs might provide verbose nested structures that you want to flatten in the PHP object. For instance, imagine a JSON response like this:

1
2
3
4
5
6
7
8
9
{
    "id": "123",
    "profile": {
        "username": "jdoe",
        "personal_information": {
            "full_name": "Jane Doe"
        }
    }
}

You may wish to serialize this information to a single PHP object like:

1
2
3
4
5
6
class Person
{
    private int $id;
    private string $username;
    private string $fullName;
}

Use the #[SerializedPath] to specify the path of the nested property using valid PropertyAccess syntax:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace App\Model;

use Symfony\Component\Serializer\Attribute\SerializedPath;

class Person
{
    private int $id;

    #[SerializedPath('[profile][username]')]
    private string $username;

    #[SerializedPath('[profile][personal_information][full_name]')]
    private string $fullName;
}

Caution

The SerializedPath cannot be used in combination with a SerializedName for the same property.

The #[SerializedPath] attribute also applies to the serialization of a PHP object:

1
2
3
4
5
6
use App\Model\Person;
// ...

$person = new Person(123, 'jdoe', 'Jane Doe');
$jsonContent = $serializer->serialize($person, 'json');
// $jsonContent contains {"id":123,"profile":{"username":"jdoe","personal_information":{"full_name":"Jane Doe"}}}

Converting Property Names when Serializing and Deserializing

Sometimes serialized attributes must be named differently than properties or getter/setter methods of PHP classes. This can be achieved using name converters.

The serializer service uses the MetadataAwareNameConverter. With this name converter, you can change the name of an attribute using the #[SerializedName] attribute:

1
2
3
4
5
6
7
8
9
10
11
12
// src/Model/Person.php
namespace App\Model;

use Symfony\Component\Serializer\Attribute\SerializedName;

class Person
{
    #[SerializedName('customer_name')]
    private string $name;

    // ...
}

This custom mapping is used to convert property names when serializing and deserializing objects:

1
2
3
4
// ...

$json = $serializer->serialize($person, 'json');
// $json contains {"customer_name":"Jane Doe", ...}

See also

You can also create a custom name converter class. Read more about this in How to Create your Custom Name Converter.

CamelCase to snake_case

In many formats, it's common to use underscores to separate words (also known as snake_case). However, in Symfony applications is common to use camelCase to name properties.

Symfony provides a built-in name converter designed to transform between snake_case and CamelCased styles during serialization and deserialization processes. You can use it instead of the metadata aware name converter by setting the name_converter setting to serializer.name_converter.camel_case_to_snake_case:

1
2
3
4
# config/packages/serializer.yaml
framework:
    serializer:
        name_converter: 'serializer.name_converter.camel_case_to_snake_case'

Serializer Normalizers

By default, the serializer service is configured with the following normalizers (in order of priority):

UnwrappingDenormalizer
Can be used to only denormalize a part of the input, read more about this later in this article.
ProblemNormalizer
Normalizes FlattenException errors according to the API Problem spec RFC 7807.
UidNormalizer

Normalizes objects that extend AbstractUid.

The default normalization format for objects that implement Uuid is the RFC 4122 format (example: d9e7a184-5d5b-11ea-a62a-3499710062d0). The default normalization format for objects that implement Ulid is the Base 32 format (example: 01E439TP9XJZ9RPFH3T1PYBCR8). You can change the string format by setting the serializer context option UidNormalizer::NORMALIZATION_FORMAT_KEY to UidNormalizer::NORMALIZATION_FORMAT_BASE_58, UidNormalizer::NORMALIZATION_FORMAT_BASE_32 or UidNormalizer::NORMALIZATION_FORMAT_RFC_4122.

Also it can denormalize uuid or ulid strings to Uuid or Ulid. The format does not matter.

DateTimeNormalizer

This normalizes between DateTimeInterface objects (e.g. DateTime and DateTimeImmutable) and strings.

By default, the RFC 3339 format is used when normalizing the value. Use DateTimeNormalizer::FORMAT_KEY and DateTimeNormalizer::TIMEZONE_KEY to change the format.

ConstraintViolationListNormalizer
This normalizer converts objects that implement ConstraintViolationListInterface into a list of errors according to the RFC 7807 standard.
DateTimeZoneNormalizer
This normalizer converts between DateTimeZone objects and strings that represent the name of the timezone according to the list of PHP timezones.
DateIntervalNormalizer
This normalizes between DateInterval objects and strings. By default, the P%yY%mM%dDT%hH%iM%sS format is used. Use the DateIntervalNormalizer::FORMAT_KEY option to change this.
FormErrorNormalizer

This normalizer works with classes that implement FormInterface.

It will get errors from the form and normalize them according to the API Problem spec RFC 7807.

TranslatableNormalizer

This normalizer converts objects implementing TranslatableInterface to a translated string using the translator.

You can define the locale to use to translate the object by setting the TranslatableNormalizer::NORMALIZATION_LOCALE_KEY context option.

6.4

The UidNormalizer normalization formats were introduced in Symfony 5.3. The TranslatableNormalizer was introduced in Symfony 6.4.

BackedEnumNormalizer

This normalizer converts between BackedEnum enums and strings or integers.

By default, an exception is thrown when data is not a valid backed enumeration. If you want null instead, you can set the BackedEnumNormalizer::ALLOW_INVALID_VALUES option.

6.3

The BackedEnumNormalizer::ALLOW_INVALID_VALUES context option was introduced in Symfony 6.3.

DataUriNormalizer
This normalizer converts between SplFileInfo objects and a data URI string (data:...) such that files can be embedded into serialized data.
JsonSerializableNormalizer

This normalizer works with classes that implement JsonSerializable.

It will call the JsonSerializable::jsonSerialize() method and then further normalize the result. This means that nested JsonSerializable classes will also be normalized.

This normalizer is particularly helpful when you want to gradually migrate from an existing codebase using simple json_encode to the Symfony Serializer by allowing you to mix which normalizers are used for which classes.

Unlike with json_encode circular references can be handled.

ArrayDenormalizer
This denormalizer converts an array of arrays to an array of objects (with the given type). See Handling Arrays.
ObjectNormalizer

This is the most powerful default normalizer and used for any object that could not be normalized by the other normalizers.

It leverages the PropertyAccess Component to read and write in the object. This allows it to access properties directly or using getters, setters, hassers, issers, canners, adders and removers. Names are generated by removing the get, set, has, is, add or remove prefix from the method name and transforming the first letter to lowercase (e.g. getFirstName() -> firstName).

During denormalization, it supports using the constructor as well as the discovered methods.

serializer.encoder

Danger

Always make sure the DateTimeNormalizer is registered when serializing the DateTime or DateTimeImmutable classes to avoid excessive memory usage and exposing internal details.

Built-in Normalizers

Besides the normalizers registered by default (see previous section), the serializer component also provides some extra normalizers.You can register these by defining a service and tag it with serializer.normalizer. For instance, to use the CustomNormalizer you have to define a service like:

1
2
3
4
5
6
7
8
9
# config/services.yaml
services:
    # ...

    # if you're using autoconfigure, the tag will be automatically applied
    Symfony\Component\Serializer\Normalizer\CustomNormalizer:
        tags:
            # register the normalizer with a high priority (called earlier)
            - { name: 'serializer.normalizer', priority: 500 }
CustomNormalizer
This normalizer calls a method on the PHP object when normalizing. The PHP object must implement NormalizableInterface and/or DenormalizableInterface.
GetSetMethodNormalizer

This normalizer is an alternative to the default ObjectNormalizer. It reads the content of the class by calling the "getters" (public methods starting with get, has, is or can). It will denormalize data by calling the constructor and the "setters" (public methods starting with set).

Objects are normalized to a map of property names and values (names are generated by removing the get prefix from the method name and transforming the first letter to lowercase; e.g. getFirstName() -> firstName).

PropertyNormalizer

This is yet another alternative to the ObjectNormalizer. This normalizer directly reads and writes public properties as well as private and protected properties (from both the class and all of its parent classes) by using PHP reflection. It supports calling the constructor during the denormalization process.

Objects are normalized to a map of property names to property values.

You can also limit the normalizer to only use properties with a specific visibility (e.g. only public properties) using the PropertyNormalizer::NORMALIZE_VISIBILITY context option. You can set it to any combination of the PropertyNormalizer::NORMALIZE_PUBLIC, PropertyNormalizer::NORMALIZE_PROTECTED and PropertyNormalizer::NORMALIZE_PRIVATE constants:

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
// ...

$json = $serializer->serialize($person, 'json', [
    // only serialize public properties
    PropertyNormalizer::NORMALIZE_VISIBILITY => PropertyNormalizer::NORMALIZE_PUBLIC,

    // serialize public and protected properties
    PropertyNormalizer::NORMALIZE_VISIBILITY => PropertyNormalizer::NORMALIZE_PUBLIC | PropertyNormalizer::NORMALIZE_PROTECTED,
]);

6.2

The PropertyNormalizer::NORMALIZE_VISIBILITY context option and its values were introduced in Symfony 6.2.

Debugging the Serializer

6.3

The debug:serializer`` command was introduced in Symfony 6.3.

Use the debug:serializer command to dump the serializer metadata of a given class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ php bin/console debug:serializer 'App\Entity\Book'

    App\Entity\Book
    ---------------

    +----------+------------------------------------------------------------+
    | Property | Options                                                    |
    +----------+------------------------------------------------------------+
    | name     | [                                                          |
    |          |   "groups" => [                                            |
    |          |       "book:read",                                         |
    |          |       "book:write",                                        |
    |          |   ]                                                        |
    |          |   "maxDepth" => 1,                                         |
    |          |   "serializedName" => "book_name"                          |
    |          |   "ignore" => false                                        |
    |          |   "normalizationContexts" => [],                           |
    |          |   "denormalizationContexts" => []                          |
    |          | ]                                                          |
    | isbn     | [                                                          |
    |          |   "groups" => [                                            |
    |          |       "book:read",                                         |
    |          |   ]                                                        |
    |          |   "maxDepth" => null,                                      |
    |          |   "serializedName" => null                                 |
    |          |   "ignore" => false                                        |
    |          |   "normalizationContexts" => [],                           |
    |          |   "denormalizationContexts" => []                          |
    |          | ]                                                          |
    +----------+------------------------------------------------------------+

Advanced Serialization

Skipping null Values

By default, the Serializer will preserve properties containing a null value. You can change this behavior by setting the AbstractObjectNormalizer::SKIP_NULL_VALUES context option to true:

1
2
3
4
5
6
7
8
9
10
class Person
{
    public string $name = 'Jane Doe';
    public ?string $gender = null;
}

$jsonContent = $serializer->serialize(new Person(), 'json', [
    AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
]);
// $jsonContent contains {"name":"Jane Doe"}

Handling Uninitialized Properties

In PHP, typed properties have an uninitialized state which is different from the default null of untyped properties. When you try to access a typed property before giving it an explicit value, you get an error.

To avoid the serializer throwing an error when serializing or normalizing an object with uninitialized properties, by default the ObjectNormalizer catches these errors and ignores such properties.

You can disable this behavior by setting the AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES context option to false:

1
2
3
4
5
6
7
8
9
10
class Person {
    public string $name = 'Jane Doe';
    public string $phoneNumber; // uninitialized
}

$jsonContent = $normalizer->serialize(new Dummy(), 'json', [
    AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES => false,
]);
// throws Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException
// as the ObjectNormalizer cannot read uninitialized properties

Note

Using PropertyNormalizer or GetSetMethodNormalizer with AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES context option set to false will throw an \Error instance if the given object has uninitialized properties as the normalizers cannot read them (directly or via getter/isser methods).

Handling Circular References

Circular references are common when dealing with associated objects:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Organization
{
    public function __construct(
        private string $name,
        private array $members = []
    ) {
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function addMember(Member $member): void
    {
        $this->members[] = $member;
    }

    public function getMembers(): array
    {
        return $this->members;
    }
}

class Member
{
    private Organization $organization;

    public function __construct(
        private string $name
    ) {
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setOrganization(Organization $organization): void
    {
        $this->organization = $organization;
    }

    public function getOrganization(): Organization
    {
        return $this->organization;
    }
}

To avoid infinite loops, the normalizers throw a CircularReferenceException when such a case is encountered:

1
2
3
4
5
6
7
8
$organization = new Organization('Les-Tilleuls.coop');
$member = new Member('Kévin');

$organization->addMember($member);
$member->setOrganization($organization);

$jsonContent = $serializer->serialize($organization, 'json');
// throws a CircularReferenceException

The key circular_reference_limit in the context sets the number of times it will serialize the same object before considering it a circular reference. The default value is 1.

Instead of throwing an exception, circular references can also be handled by custom callables. This is especially useful when serializing entities having unique identifiers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\Serializer\Exception\CircularReferenceException;

$context = [
    AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function (object $object, ?string $format, array $context): string {
        if (!$object instanceof Organization) {
            throw new CircularReferenceException('A circular reference has been detected when serializing the object of class "'.get_debug_type($object).'".');
        }

        // serialize the nested Organization with only the name (and not the members)
        return $object->getName();
    },
];

$jsonContent = $serializer->serialize($organization, 'json', $context);
// $jsonContent contains {"name":"Les-Tilleuls.coop","members":[{"name":"K\u00e9vin", organization: "Les-Tilleuls.coop"}]}

Handling Serialization Depth

The serializer can also detect nested objects of the same class and limit the serialization depth. This is useful for tree structures, where the same object is nested multiple times.

For instance, assume a data structure of a family tree:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ...
class Person
{
    // ...

    public function __construct(
        private string $name,
        private ?self $mother
    ) {
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getMother(): ?self
    {
        return $this->mother;
    }

    // ...
}

// ...
$greatGrandmother = new Person('Elizabeth', null);
$grandmother = new Person('Jane', $greatGrandmother);
$mother = new Person('Sophie', $grandmother);
$child = new Person('Joe', $mother);

You can specify the maximum depth for a given property. For instance, you can set the max depth to 1 to always only serialize someone's mother (and not their grandmother, etc.):

1
2
3
4
5
6
7
8
9
10
11
12
// src/Model/Person.php
namespace App\Model;

use Symfony\Component\Serializer\Attribute\MaxDepth;

class Person
{
    #[MaxDepth(1)]
    private ?self $mother;

    // ...
}

To limit the serialization depth, you must set the AbstractObjectNormalizer::ENABLE_MAX_DEPTH key to true in the context (or the default context specified in framework.yaml):

1
2
3
4
5
6
7
8
9
10
// ...
$greatGrandmother = new Person('Elizabeth', null);
$grandmother = new Person('Jane', $greatGrandmother);
$mother = new Person('Sophie', $grandmother);
$child = new Person('Joe', $mother);

$jsonContent = $serializer->serialize($child, null, [
    AbstractObjectNormalizer::ENABLE_MAX_DEPTH => true
]);
// $jsonContent contains {"name":"Joe","mother":{"name":"Sophie"}}

You can also configure a custom callable that is used when the maximum depth is reached. This can be used to for instance return the unique identifier of the next nested object, instead of omitting the property:

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

$greatGrandmother = new Person('Elizabeth', null);
$grandmother = new Person('Jane', $greatGrandmother);
$mother = new Person('Sophie', $grandmother);
$child = new Person('Joe', $mother);

// all callback parameters are optional (you can omit the ones you don't use)
$maxDepthHandler = function (object $innerObject, object $outerObject, string $attributeName, ?string $format = null, array $context = []): string {
    // return only the name of the next person in the tree
    return $innerObject instanceof Person ? $innerObject->getName() : null;
};

$jsonContent = $serializer->serialize($child, null, [
    AbstractObjectNormalizer::ENABLE_MAX_DEPTH => true,
    AbstractObjectNormalizer::MAX_DEPTH_HANDLER => $maxDepthHandler,
]);
// $jsonContent contains {"name":"Joe","mother":{"name":"Sophie","mother":"Jane"}}

Using Callbacks to Serialize Properties with Object Instances

When serializing, you can set a callback to format a specific object property. This can be used instead of defining the context for a group:

1
2
3
4
5
6
7
8
9
10
11
12
13
$person = new Person('cordoval', 34);
$person->setCreatedAt(new \DateTime('now'));

$context = [
    AbstractNormalizer::CALLBACKS => [
        // all callback parameters are optional (you can omit the ones you don't use)
        'createdAt' => function (object $attributeValue, object $object, string $attributeName, ?string $format = null, array $context = []) {
            return $attributeValue instanceof \DateTime ? $attributeValue->format(\DateTime::ATOM) : '';
        },
    ],
];
$jsonContent = $serializer->serialize($person, 'json');
// $jsonContent contains {"name":"cordoval","age":34,"createdAt":"2014-03-22T09:43:12-0500"}

Advanced Deserialization

Require all Properties

6.3

The AbstractNormalizer::PREVENT_NULLABLE_FALLBACK context option was introduced in Symfony 6.3.

By default, the Serializer will add null to nullable properties when the parameters for those are not provided. You can change this behavior by setting the AbstractNormalizer::REQUIRE_ALL_PROPERTIES context option to true:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person
{
    public function __construct(
        public string $firstName,
        public ?string $lastName,
    ) {
    }
}

// ...
$data = ['firstName' => 'John'];
$person = $serializer->deserialize($data, Person::class, 'json', [
    AbstractNormalizer::REQUIRE_ALL_PROPERTIES => true,
]);
// throws Symfony\Component\Serializer\Exception\MissingConstructorArgumentException

Collecting Type Errors While Denormalizing

When denormalizing a payload to an object with typed properties, you'll get an exception if the payload contains properties that don't have the same type as the object.

Use the COLLECT_DENORMALIZATION_ERRORS option to collect all exceptions at once, and to get the object partially denormalized:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
try {
    $person = $serializer->deserialize($jsonString, Person::class, 'json', [
        DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true,
    ]);
} catch (PartialDenormalizationException $e) {
    $violations = new ConstraintViolationList();

    /** @var NotNormalizableValueException $exception */
    foreach ($e->getErrors() as $exception) {
        $message = sprintf('The type must be one of "%s" ("%s" given).', implode(', ', $exception->getExpectedTypes()), $exception->getCurrentType());
        $parameters = [];
        if ($exception->canUseMessageForUser()) {
            $parameters['hint'] = $exception->getMessage();
        }
        $violations->add(new ConstraintViolation($message, '', $parameters, null, $exception->getPath(), null));
    }

    // ... return violation list to the user
}

Deserializing in an Existing Object

The serializer can also be used to update an existing object. You can do this by configuring the object_to_populate serializer context option:

1
2
3
4
5
6
7
8
9
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;

// ...
$person = new Person('Jane Doe', 59);

$serializer->deserialize($jsonData, Person::class, 'json', [
    AbstractNormalizer::OBJECT_TO_POPULATE => $person,
]);
// instead of returning a new object, $person is updated instead

Note

The AbstractNormalizer::OBJECT_TO_POPULATE option is only used for the top level object. If that object is the root of a tree structure, all child elements that exist in the normalized data will be re-created with new instances.

When the AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE context option is set to true, existing children of the root OBJECT_TO_POPULATE are updated from the normalized data, instead of the denormalizer re-creating them. This only works for single child objects, not for arrays of objects. Those will still be replaced when present in the normalized data.

Deserializing Interfaces and Abstract Classes

When working with associated objects, a property sometimes reference an interface or abstract class. When deserializing these properties, the Serializer has to know which concrete class to initialize. This is done using a discriminator class mapping.

Imagine there is an InvoiceItemInterface that is implemented by the Product and Shipping objects. When serializing an object, the serializer will add an extra "discriminator attribute". This contains either product or shipping. The discriminator class map maps these type names to the real PHP class name when deserializing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace App\Model;

use Symfony\Component\Serializer\Attribute\DiscriminatorMap;

#[DiscriminatorMap(
    typeProperty: 'type',
    mapping: [
        'product' => Product::class,
        'shipping' => Shipping::class,
    ]
)]
interface InvoiceItemInterface
{
    // ...
}

With the discriminator map configured, the serializer can now pick the correct class for properties typed as `InvoiceItemInterface`:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class InvoiceLine
{
    public function __construct(
        private InvoiceItemInterface $invoiceItem
    ) {
        $this->invoiceItem = $invoiceItem;
    }

    public function getInvoiceItem(): InvoiceItemInterface
    {
        return $this->invoiceItem;
    }

    // ...
}

// ...
$invoiceLine = new InvoiceLine(new Product());

$jsonString = $serializer->serialize($invoiceLine, 'json');
// $jsonString contains {"type":"product",...}

$invoiceLine = $serializer->deserialize($jsonString, InvoiceLine::class, 'json');
// $invoiceLine contains new InvoiceLine(new Product(...))

Deserializing Input Partially (Unwrapping)

The serializer will always deserialize the complete input string into PHP values. When connecting with third party APIs, you often only need a specific part of the returned response.

To avoid deserializing the whole response, you can use the UnwrappingDenormalizer and "unwrap" the input data:

1
2
3
4
5
$jsonData = '{"result":"success","data":{"person":{"name": "Jane Doe","age":57}}}';
$data = $serialiser->deserialize($jsonData, Object::class, [
    UnwrappingDenormalizer::UNWRAP_PATH => '[data][person]',
]);
// $data is Person(name: 'Jane Doe', age: 57)

The unwrap_path is a property path of the PropertyAccess component, applied on the denormalized array.

Handling Constructor Arguments

If the class constructor defines arguments, as usually happens with Value Objects, the serializer will match the parameter names with the deserialized attributes. If some parameters are missing, a MissingConstructorArgumentsException is thrown.

In these cases, use the default_constructor_arguments context option to define default values for the missing parameters:

1
2
3
4
5
6
7
8
9
10
11
use App\Model\Person;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
// ...

$jsonData = '{"age":39,"name":"Jane Doe"}';
$person = $serializer->deserialize($jsonData, Person::class, 'json', [
    AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS => [
        Person::class => ['sportsperson' => true],
    ],
]);
// $person is Person(name: 'Jane Doe', age: 39, sportsperson: true);

Recursive Denormalization and Type Safety

When a PropertyTypeExtractor is available, the normalizer will also check that the data to denormalize matches the type of the property (even for primitive types). For instance, if a string is provided, but the type of the property is int, an UnexpectedValueException will be thrown. The type enforcement of the properties can be disabled by setting the serializer context option ObjectNormalizer::DISABLE_TYPE_ENFORCEMENT to true.

Configuring the Metadata Cache

The metadata for the serializer is automatically cached to enhance application performance. By default, the serializer uses the cache.system cache pool which is configured using the cache.system option.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version