Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

@SerializedName & Constructor Args

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 $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

When we read a CheeseListing resource, we get a description field. But when we send data, it's called textDescription. And... that's technically fine: our input fields don't need to match our output fields. But... if we could make these the same, that might make life easier for anyone using our API.

It's pretty easy to guess how these properties are created: the keys inside the JSON literally match the names of the properties inside our class. And in the case of a fake property like textDescription, API Platform strips off the "set" part and makes it lower camel case. By the way, like everything in API Platform, the way fields are transformed into keys is something you can control at a global level: it's called a "name converter".

Controlling Field Names: @SerializedName

Anyways, it would be kinda nice if the input field were just called description. We'd have input description, output description. Sure, internally, we would know setTextDescription() was called on input and getDescription() on output, but the user wouldn't need to care or worry about this.

And... yes! You can totally control this with a super useful annotation. Above setTextDescription(), add @SerializedName() with description.

... lines 1 - 23
class CheeseListing
... lines 25 - 96
/**
... lines 98 - 100
* @SerializedName("description")
*/
public function setTextDescription(string $description): self
... lines 104 - 147
}

Refresh the docs! If we try the GET operation... that hasn't changed: still description. But for the POST operation... yes! The field is now called description, but the serializer will call setTextDescription() internally.

What about Constructor Arguments

Ok, so we know that the serializer likes to work by calling getter and setter methods... or by using public properties or a few other things like hasser or isser methods. But what if I want to give my class a constructor? Well, right now we do have a constructor, but it doesn't have any required arguments. That means that the serializer has no problems instantiating this class when we POST a new CheeseListing.

But... you know what? Because every CheeseListing needs a title, I'd like to give this a new required argument called $title. You definitely don't need to do this, but for a lot of people, it makes sense: if a class has required properties: force them to be passed in via the constructor!

And now that we have this, you might also decide that you don't want to have a setTitle() method anymore! From an object-oriented perspective, this makes the title property immutable: you can only set it once when creating the CheeseListing. It's kind of a silly example. In the real world, we probably would want the title to be changeable. But, from an object-oriented perspective, there are situations when you want to do exactly this.

Oh, and don't forget to say $this->title = $title in the constructor.

... lines 1 - 23
class CheeseListing
{
... lines 26 - 62
public function __construct(string $title)
{
$this->title = $title;
... line 66
}
... lines 68 - 141
}

The question now is... will the serializer be able to work with this? Is it going to be super angry that we removed setTitle()? And when we POST to add a new one, will it be able to instantiate the CheeseListing even though it has a required arg?

Whelp! Let's try it! How about crumbs of some blue cheese... for $5. Execute and... it worked! The title is correct!

Um... how the heck did that work? Because the only way to set the title is via the constructor, it apparently knew to pass the title key there? How?

The answer is... magic! I'm kidding! The answer is... by complete luck! No, I'm still totally lying. The answer is because of the argument's name.

Check this out: change the argument to $name, and update the code below. From an object-oriented perspective, that shouldn't change anything. But hit execute again.

... lines 1 - 62
public function __construct(string $name)
{
$this->title = $name;
... line 66
}
... lines 68 - 143

Huge error! A 400 status code:

Cannot create an instance of CheeseListing from serialized data because its constructor requires parameter "name" to be present.

My compliments to the creator of that error message - it's awesome! When the serializer sees a constructor argument named... $name, it looks for a name key in the JSON that we're sending. If that doesn't exist, boom! Error!

So as long as we call the argument $title, it all works nicely.

Constructor Argument can Change Validation Errors

But there is one edge case. Pretend that we're creating a new CheeseListing and we forget to send the title field entirely - like, we have a bug in our JavaScript code. Hit Execute.

We do get back a 400 error... which is perfect: it means that the person making the request has something wrong with their request. But, the hydra:title isn't very clear:

An error occurred

Fascinating! The hydra:description is way more descriptive... actually a bit too descriptive - it shows off some internal things about our API... that I maybe don't want to make public. At least the trace won't show up on production.

Showing these details inside hydra:description might be ok with you... But if you want to avoid this, you need to rely on validation, which is a topic that we'll talk about in a few minutes. But, what you need to know now is that validation can't happen unless the serializer is able to successfully create the CheeseListing object. In other words, you need to help the serializer out by making this argument optional.

... lines 1 - 62
public function __construct(string $title = null)
{
... lines 65 - 66
}
... lines 68 - 143

If you try this again... ha! A 500 error! It does create the CheeseListing object successfully... then explodes when it tries to add a null title in the database. But, that's exactly what we want - because it will allow validation to do its work... once we add that in a few minutes.

Tip

Actually, the auto-validation was not enabled by default in Symfony 4.3, but may be in Symfony 4.4.

Oh, and if you're using Symfony 4.3, you may already see a validation error! That's because of a new feature that can automatically convert your database rules - the fact that we've told Doctrine that title is required in the database - into validation rules. Fun fact, this feature was contributed to Symfony by Kèvin Dunglas - the lead developer of API Platform. Sheesh Kèvin! Take a break once in awhile!

Next: let's explore filters: a powerful system for allowing your API clients to search and filter through our CheeseListing resources.

Leave a comment!

15
Login or Register to join the conversation
Wondrous Avatar
Wondrous Avatar Wondrous | posted 6 months ago

I would like to set the logged in user in the constructor. The problem is I can't and don't want to simply inject services into the entity. And in the DataPersister the object was already initialized. Any ideas?


#[Groups(['read'])]
public readonly User $creator;

// this should be the logged in User
public function __construct(User $creator)
{
$this->creator = $creator;
}

Reply

Hey Wondrous!

Sorry for the slow reply! We're usually much faster - that's my bad - prepping for a conference next week!

This is an interesting question/problem. First, when using Doctrine (ignore API Platform for a moment), the constructor is only called ONE time: when you originally CREATE the object. Once it's been persisted to Doctrine, on future requests, when you query for that object from the database, the constructor is NOT called. That's by design: Doctrine wants it to feel like your object is created just once... then is kind of "put to sleep" in the database and woken up later.

You may have already known the above stuff :). I mention it because it narrows the scope. The question now is: how can we pass a custom argument to the constructor when our object is originally created - e.g. when a POST request is made to /api/cheeses? That's a question for the serializer - and though I haven't done this before, I think the answer is here: https://symfony.com/doc/cur...

You should be able to accomplish this by creating a custom context builder - https://symfonycasts.com/sc... - and if the "resource class" is currently the target class, you could add that AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS key to the context, set to the currently-logged-in user.

Let me know if you end up trying this and if it works out - it's an interesting problem!

Cheers!

Reply
Fedale Avatar

Hi, I don't understand why we use "SeriallizedName" annotation. Doesn't "Serialize" mean that we are *reading* data (i.e. from Object to Format)? So why we use "Serialzed" and not "Deserialized" in setting property?

Reply

Hey @Danilo Di Moia!

That's an excellent question and perspective! It had not occurred to me before!

The truth is that SerializedName would be used for both reading data and/or writing data. In this example, we added the SerializedName above our setTextDescription() method. So naturally (since this is a setter, so it's only used for writing data), the SerializedName is actually more of a "deserialized name" as you suggested :). If we had put this same SerializedName on a getTextDescription(), then it would only be used for "reading" data.

So it really controls the named that's used for both serialization and deserialization. But if you are adding it to a getter or setter method like we did, then, of course, it really only applies to either read (for the getter) or write (for the setter). However, if you added it to a property (imagine @SerializedName("cheeseDescription") above the $description property), then the field would be called cheeseDescription for both reading and writing).

I hope that explains why it has just this one name (SerializedNamed)... even though that's imperfect in 1/2 of the situations :P.

Cheers!

Reply
Default user avatar

Just trying to add some API capability to existing Symfony 4.4 project but probably missed something in config. On simple POST operation got an error message:

"@context": "/api/contexts/Error",
"@type": "hydra:Error",
"hydra:title": "An error occurred",
"hydra:description": "Format 'jsonld' not supported, handler must be implemented",

And can't find how to implement handler and where :(

Reply

Hey @isTom!

Hmm. You *will* need to add some config for API Platform - but it's pretty basic - https://github.com/symfony/... - and it doesn't include any "formats" config... you should get several formats out-of-the-box without needing any config. Are you also using FOSRestBundle by chance? I'm asking because, as far as I can tell, THAT is the library that is throwing this error - https://github.com/FriendsO...

Cheers!

Reply
Default user avatar
Default user avatar isTom | weaverryan | posted 1 year ago | edited

Hello weaverryan !

I described the problem with more details in Stackoverflow question
https://stackoverflow.com/q....

Reply

Hey @isTom!

I replied over there. The tl;dr is: uninstall/disable FOSRestBundle to see if it fixes things or at least gives you a different error.

Cheers!

Reply
Tom H. Avatar

Hi, i have lots of properties i need to make immutable but i also need to use the SerializedName() to alter the camelcase that API Platform generates. When i remove the setter the property disappears completey from the POST even though it's still in the group. Any ideas how i can achieve this?
Thank you

Reply

Hey Tom!

Sorry for the slow reply! Hmm, by "immutable" - I think you are referring to "I want them immutable in my app/PHP" versus "I want them immutable in my API", correct? If you (for whatever reason - e.g. design reasons) want to not have setter methods, then you *can* instead have each property as a constructor argument - each argument name needs to match the field name in the POST (that's how the serializer matches each input field with the arguments in your constructor). I don't like doing it this way... because iirc, if the user fails to send a required constructor field, they will get a serialization error - which is still a 400 error, but not as clear as a validation error. Let me know if that helps... or if I've completely answered the wrong question :).

About the SerializedName() part, if you are consistently changing how your "casing" is done, you can also do this on a global level - https://symfony.com/doc/cur...

Cheers!

Reply
Mykl Avatar

Hey Guys,
We have made the title property immutable but in the documentation for the PUT operation, the key "title" is still present. So we can believe that it is possible to change the title ...
Is there a solution to this problem?
Many thanks

Reply

Hey Mykl

Nice catch! I had to do some research to figure it out what's happening. The problem is that the field "title" has the group "cheese_listing:write", so all http methods will let you pass in that field. What you have to do is to only allow the POST method to access to it by defining a collection operation.


* @ApiResource(
​* collectionOperations={
* "post"={
* "denormalization_context"={"groups"={"cheese_listing:write", "cheese_listing:collection:post"}}
* }
* },
* )

So then you can replace the group "cheese_listing:write" by "cheese_listing:collection:post" on the title property

Cheers!

Reply
Mykl Avatar

Thank you Diego !

1 Reply

Hey Guys,

This is a bit off topic, but in phpstorm, is there a way to get closing quotes and brackets in the annotations? For example, if I type { or ", then } or " this would immediately be inserted.

Reply

Hey Skylar,

Not sure about this feature out of the box like activating it with a tick... but I use PhpStorm's Live Templates for this. You can create your own template where you declare that when you write "{" and press "Tab" for example - it expands to "{$END$}" where "$END$" is a point where you will have the cursor. You can configure the action when it will trigger like pressing Tab, enter, etc. and in what file extensions it will work, like in ".php", or ".yaml" files.

I hope this helps, just play with it a bit.

Cheers!

Reply
Cat in space

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

This tutorial works great for Symfony 5 and API Platform 2.5/2.6.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.3
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.10.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.5
        "nesbot/carbon": "^2.17", // 2.19.2
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/console": "4.2.*", // v4.2.12
        "symfony/dotenv": "4.2.*", // v4.2.12
        "symfony/expression-language": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/flex": "^1.1", // v1.17.6
        "symfony/framework-bundle": "4.2.*", // v4.2.12
        "symfony/security-bundle": "4.2.*|4.3.*", // v4.3.3
        "symfony/twig-bundle": "4.2.*|4.3.*", // v4.2.12
        "symfony/validator": "4.2.*|4.3.*", // v4.3.11
        "symfony/yaml": "4.2.*" // v4.2.12
    },
    "require-dev": {
        "symfony/maker-bundle": "^1.11", // v1.11.6
        "symfony/stopwatch": "4.2.*|4.3.*", // v4.2.9
        "symfony/web-profiler-bundle": "4.2.*|4.3.*" // v4.2.9
    }
}