Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Taking Control of the Serializer

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

The serializer is... mostly working. But we have some test failures:

Error reading property "tagLine" from available keys (id, nickname
avatarNumber, powerLevel)

Huh. Yea, if you look at the response, tagLine is mysteriously absent! Where did you go dear tagLine???

So the serializer works like this: you give it an object, and it serializes every property on it. Yep, you can control that - just hang on a few minutes. But, if any of these properties is null, instead of returning that key with null, it omits it entirely.

Fortunately, that's easy to change. Go into BaseController. In serialize() create a new variable called $context and set that to a new SerializationContext(). Call setSerializeNull() on this and pass it true. To finish this off, pass that $context as the third argument to serialize():

... lines 1 - 123
protected function serialize($data, $format = 'json')
{
$context = new SerializationContext();
$context->setSerializeNull(true);
return $this->container->get('jms_serializer')
->serialize($data, $format, $context);
}
... lines 132 - 133

Think of the SerializationContext as serialization configuration. It doesn't do a lot of useful stuff - but it does let us tell the serializer to actually return null fields.

So run the whole test suite again and wait impatiently:

phpunit -c app

ZOMG! They're passing!

Serialization Annotations

But something extra snuck into our Response - let me show you. In testGETProgrammer(), at the end, add $this->debugResponse(). Copy that method name and run just it:

phpunit -c app --filter testGETProgrammer

Ah, the id field snuck into the JSON. Before, we only serialized the other four fields. So what if we didn't want id or some property to be serialized?

The solution is so nice. Go back to the homepage of the bundle's docs. There's one documentation gotcha: the bundle is a small wrapper around the JMS Serializer library, and most of the documentation lives there. Click the documentation link to check it out.

This has a great page called Annotations: it's a reference of all of the ways that you can control serialization.

@VirtualProperty and @SerializedName

One useful annotation is @VirtualProperty. This lets you create a method and have its return value serialized. If you use that with @SerializedName, you can control the serialized property name for this or anything.

For controlling which fields are returned, we'll use @ExclusionPolicy. Scroll down to the @AccessType code block and copy that use statement. Open the Programmer entity, paste this on top, but remove the last part and add as Serializer:

... lines 1 - 5
use JMS\Serializer\Annotation as Serializer;
... lines 7 - 189

This will let us say things like @Serializer\ExclusionPolicy. Add that on top of the class, with "all".

... lines 1 - 5
use JMS\Serializer\Annotation as Serializer;
... line 7
/**
... lines 9 - 12
* @Serializer\ExclusionPolicy("all")
*/
class Programmer
... lines 16 - 189

This says: "Hey serializer, don't serialize any properties by default, just hang out in your pajamas". Now we'll use @Serializer\Expose() to whitelist the stuff we do want. We don't want id - so leave that. Above the $name property, add @Serializer\Expose(). Do this same thing above $avatarNumber, $tagLine and $powerLevel:

... lines 1 - 14
class Programmer
{
... lines 17 - 25
/**
... lines 27 - 29
* @Serializer\Expose
*/
private $nickname;
... line 33
/**
... lines 35 - 37
* @Serializer\Expose
*/
private $avatarNumber;
... line 41
/**
... lines 43 - 45
* @Serializer\Expose
*/
private $tagLine;
... line 49
/**
... lines 51 - 53
* @Serializer\Expose
*/
private $powerLevel = 0;
... lines 57 - 186
}

And my good buddy PhpStorm is telling me I have a syntax error up top. Whoops, I doubled my use statements - get rid of the extra one.

With this, the id field should be gone from the response. Run the test!

phpunit -c app --filter testGETProgrammer

No more id! Take out the debugResponse(). Phew! Congrats! We only have one resource, but our API is kicking butt! We've built a system that let's us serialize things easily, create JSON responses and update data via forms.

Oh, and the serializer can also deserialize. That is, take JSON and turn it back into an object. I prefer to use forms instead of this, but it may be another option. Of course, if life gets complex, you can always just handle incoming data manually without forms or deserialization. Just keep that in mind.

We also have a killer test setup that let's us write tests first without any headache. We could just keep repeating what we have here to make a bigger API.

But, there's more to cover! In episode 2, we'll talk about errors: a fascinating topic for API's and something that can make or break how usable your API will be.

Ok, seeya next time!

Leave a comment!

22
Login or Register to join the conversation
Default user avatar
Default user avatar Mike Ritter | posted 5 years ago

Did she just say, "and wait impatiently?"

Ha!

Reply
Default user avatar
Default user avatar Christophe Lablancherie | posted 5 years ago

Hi, how could we implement the ExclusionPolicy with FOSUserBundle ? I can't "hide" in the serialization, the password, salt etc... :'(

Reply

Great question actually! Try this out and let me know what you find: https://github.com/schmittj...

Cheers!

Reply
Default user avatar
Default user avatar Christophe Lablancherie | weaverryan | posted 5 years ago

Hi :) Thanks for the answer. Well i've just made this after my question and that's correct, it's works :)
Thank's you for the screencast !

Reply
Default user avatar
Default user avatar Mrugendra Bhure | posted 5 years ago

Hi,

Having trouble getting the User entity serialized with exclusion policy. We extend it from FOSUserBundle, desopite saying ExclusionPolicy(all), all fields are serialized. Its as if JMS serializer is ignoring annotations. I searched the web and found config changes to be done for FOSUserBundle entity, I implemented those, but no change. Only thing that seems to be working is by configuring groups and setting them in the context. JMS even exposed a FK collection I defined under a different group. So, if you do not set a group, all other annotations are simply ignored! I am using Symfony 3.4 and JMS bundle: 2.3.

I changed things as per this: http://bit.ly/2mEZwBw And http://bit.ly/2DwcOrp

/**
* @Serializer\ExclusionPolicy("all")
*/
class Therapist extends User
{
/**
* @Serializer\Expose()
* @Serializer\Groups("{Deep}")
*/
private $clinics;
}

Despite configuring "deep" group above, if I do not set group, JMS also exposes this property. Am I doing something wrong ?

Reply

Hey Mrugendra Bhure!

Oh boy, this is a mess! JMSSerializer does not play well when you extend a base class: it seems that, under normal situations, *you* can only control the serialization rules for the fields in *your* class, not the parent class.

It's clear to me from looking around that, for some reason, the fix for this will involve you using YAML serialization rules instead of annotations: Check out this thread for details: https://github.com/schmittj...

And honestly, due to all this craziness, you could also decide to NOT use the serializer for just this one object, and instead turn it into JSON manually (or, turn it into an array and the put that through the serializer, which should allow embedded objects [if you want them in your JSON] to still be serialized through JMS).

I hope that helps! Cheers!

Reply
Vladimir Z. Avatar
Vladimir Z. Avatar Vladimir Z. | posted 5 years ago

Hi Ryan,

When you make updates to courses, how do I know what's been updated?

I've finished some of the courses, but periodically see "Updated 3 days ago", or so, don't see any new videos, so don't know what's been updated.

Thank you!

Reply

Hey Vlad!

This is something we're still working on - right now the "Updated" status (unless a course is being actively released still) usually is not significant (we probably tweaked something insignificant). We plan to publish an actual "changelog" whenever we make significant changes in the future. It's on the list!

Cheers!

Reply
Vladimir Z. Avatar

Thanks, Ryan!

Reply
Default user avatar
Default user avatar Mike Ritter | posted 5 years ago | edited

I'm guessing there didn't make the "Hey Serializer" graphic/meme.

Reply

hahaha, I can bet Leanna has the credit on that

Cheers!

Reply
Default user avatar
Default user avatar Roberto Briones Argüelles | posted 5 years ago

I notice that you always use the Annotation way, I'm trying to avoid in order to use configuration files in yml, I think that way is more decoupled from the framework or tool, but I don't really know for sure, can you tell me why are you using Annotations?

Reply

Hi Roberto!

Yes, very fair question :). First, there is no performance difference or flexibility between the formats. So, it *does* come down to developer preference. I like annotations because I like having my configuration right next to the thing it's configuring. For serializing, you can immediately see what properties are being serialized, without needing to find another configuration file. Route & Doctrine annotations also have that same advantage.

About the coupling idea, I tend to think that idea is over-hyped. You *are* using Symfony, and it's massively unlikely that you'll need to switch suddenly to using something else. And even if you *did* need this, having your configuration in YAML instead of annotations won't make much difference - it would be pretty easy to quickly delete the annotations from your class if you decided to use a different library for serializing. Decoupling from your framework is really important if you're sharing your code (you don't want to force your serialization configuration on another use, just so they can use your class) - but purposefully *coupling* your code to your framework - along with use a nice layer of services, that's key - is a great way to be pragmatic and stay productive. That's subjective of course - but that at least shows you why I've made these choices :).

Cheers!

1 Reply

Hey Ryan
It is me again ;)

I try to use the Symfony's serializer and use the Groups for handling the response.

I tried this:

protected function serialize($data)
{
$encoders = [new XmlEncoder(), new JsonEncoder()];
$normalizer = new ObjectNormalizer();
$serializer = new Serializer([$normalizer], $encoders);

return $serializer->serialize($data, 'json', ['groups' => ['group1']]);
}

And in my model I put the @Groups({"group1"})

But as you can imagine it doesn't work ;)
If I put $normalizer->setIgnoredAttributes(['user', 'id']); it is ok I don't have the user and id but in the doc it write is better to user the annotation instead the ignoredAttributes.

Do you know what is my mistake ?

Thanks again for your work

Reply

Hi Greg!

I might know the issue :). Obviously, as you found out, there is a Serializer component in Symfony, which means you can create a Serializer object from scratch, adding in whatever normalizers, encoders, etc that you want. But, when you use the Symfony Framework, we do this for you. As soon as you uncomment this line (https://github.com/symfony/symfony-standard/blob/bba96b98623851b0ce9331c6fbe9ab2b7e57ae27/app/config/config.yml#L21), you will have a serializer service... which means you can just do:


return $this->container->get('serializer')->serialize($data, 'json' ['groups' => ['group1']]);

Now, your approach should also work, but I think you're missing the extra setup needed to tell the Serializer to read the annotations. Check out this section: http://symfony.com/doc/current/components/serializer.html#attributes-groups

So, you're totally free to keep creating the Serializer yourself - but you may not need to! But if you do continue to do this, just make sure to make the tweaks in that section so that your annotations are loaded :).

Cheers!

Reply

Hi Ryan,

Exactly I definitely forgot to initialize the ClassMetadataFactory.
So I just need to initialize it just above ?
Thanks

Reply

You got it :). The 4th code block on that link shows everything all put together - that ClassMetadataFactory is passed to your ObjectNormalizer.

Have fun!

Reply

Thanks again I'am so stupid that I didn't see it in the example :)
Btw Nice tuto about ES6.

Reply
Default user avatar
Default user avatar Tom Friedhof | posted 5 years ago

You mention at the end of this tutorial that you prefer to use forms instead of the Serializer to turn the json back into an object. Can you elaborate on why you prefer to use the form method over the serializer?

Reply

Hey Tom!

Definitely - good question :). There are a few reasons:

1) Data transformers - the idea that your user might send you an "id", but on your object, that property is itself an object. The EntityType is built to do this type of transformation

2) Similar to the above, I find that your output doesn't always match your *input*. I mean, if I literally looked at the JSON for a GET /blog/{id} endpoint and compared it with the JSON that I send to *create* a blog post, they will differ more than you might think. So, at first, it seems kind of awesome to have one model class that you can serialize for output and deserialize for input. But in reality, it's not often that simple, and the form gives you that layer to hide/show fields or add other transformations.

If I summarize these two reasons, it comes down to this: the serializer is "stupid" by design: it simply takes the JSON (when deserializing) and puts it onto an object. Unless that JSON and your object match perfectly, it gets tough. On the other hand, we going from your model to JSON is often easier: we typically design our model classes to match the JSON we want with little or no effort.

But, not everyone agrees - that's why the deserializer exists! But I like having a model class that models my output, and a form class that models my input.

Cheers!

Reply
amcastror Avatar
amcastror Avatar amcastror | posted 5 years ago

Hey! Thanks a lot for this, you guys are doing one hell of a job.. I didn't go through the hole code, but it showed me some tricks that were really useful.

Reply

Hey Matias,

There're a lot of tricks in REST ;) We are really glad you like this course and it's helpful for you!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony. The concepts of REST are still valid, but I recommend using API Platform in new Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.6.*", // v2.6.11
        "doctrine/orm": "~2.2,>=2.2.3,<2.5", // v2.4.7
        "doctrine/dbal": "<2.5", // v2.4.4
        "doctrine/doctrine-bundle": "~1.2", // v1.4.0
        "twig/extensions": "~1.0", // v1.2.0
        "symfony/assetic-bundle": "~2.3", // v2.6.1
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.8
        "symfony/monolog-bundle": "~2.4", // v2.7.1
        "sensio/distribution-bundle": "~3.0,>=3.0.12", // v3.0.21
        "sensio/framework-extra-bundle": "~3.0,>=3.0.2", // v3.0.7
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "hautelook/alice-bundle": "0.2.*", // 0.2
        "jms/serializer-bundle": "0.13.*" // 0.13.0
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3", // v2.5.3
        "behat/behat": "~3.0", // v3.0.15
        "behat/mink-extension": "~2.0.1", // v2.0.1
        "behat/mink-goutte-driver": "~1.1.0", // v1.1.0
        "behat/mink-selenium2-driver": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~4.6.0" // 4.6.4
    }
}
userVoice