Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

UUID Quirk with "id" Name

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

There is one tiny little quirk with you UUID's. What is it? If you want to be able to send it in JSON, the field can't be called... id!

Naming the Identifier "id"

I know, that's sounds kinda strange, so let me show you. Find the uuid property and pretend that we want to call this id in the API:

... lines 1 - 44
class User implements UserInterface
{
... lines 47 - 54
/**
* @ORM\Column(type="uuid", unique=true)
* @ApiProperty(identifier=true)
* @Groups({"user:write"})
*/
private $uuid;
... lines 61 - 305
}

Literally, instead of sending a field called uuid, I want to send one called id.

The easiest way to make that happen is to add @SerializedName("id"):

... lines 1 - 44
class User implements UserInterface
{
... lines 47 - 54
/**
... lines 56 - 57
* @SerializedName("id")
... line 59
*/
private $uuid;
... lines 62 - 306
}

Then over in the test, it's simple: change uuid to id:

... lines 1 - 9
class UserResourceTest extends CustomApiTestCase
{
... lines 12 - 33
public function testCreateUserWithUuid()
{
... lines 36 - 38
$client->request('POST', '/api/users', [
'json' => [
'id' => $uuid,
... lines 42 - 44
]
]);
... lines 47 - 50
}
... lines 52 - 108
}

We've done this type of thing before. And until now, it's been working fine. If you're getting the feeling that it won't work this time... yea... you're right. Run that test again:

symfony php bin/phpunit --filter=testCreateUserWithUuid

It fails! It says:

Update is not allowed for this operation.

This... is sort of a bug in API Platform. Well... it's not that simple - it's related to the idea that the id field is, sort of special, in the jsonapi format.

Obviously, the easiest fix is to not call your field id if you want to allow it to be sent on create. But, if you really do want to call it id, then we have two work-arounds.

Sending the Data as json-ld

Whenever you send data, API Platform tries to figure out what format that data is. For example, if we sent XML instead of JSON, API Platform would somehow need to know the data is XML so that it could use the correct deserializer.

And the same thing happens with the data we get back from our API. Normally, our API return JSON. Well, more specifically, it returns in the JSON-LD format - we can see that if we go to /api/cheeses.jsonld.

But we can also get things back as normal json! We're controlling this here by adding .json to the URL, but the normal way you should control this is by sending an Accept header to tell the API which format you want.

We haven't talked about it yet, but you can do the same thing for the format of your input data. Yep, you can say:

Hey API! The data I'm providing you is JSON... or XML... or gibberish.

You can even tell API Platform that you are sending JSON-LD instead of plain JSON. Now.... in reality, that makes very little difference - the JSON would still look the same. But internally, this activates a different denormalizer that avoids the error.

How do we tell API Platform what format the data we're sending is in? With the Content-Type header. In the test, add headers and set that to an array with Content-Type equals application/ld+json, which is the official "media type" or "mime type" for JSON-LD:

... lines 1 - 9
class UserResourceTest extends CustomApiTestCase
{
... lines 12 - 33
public function testCreateUserWithUuid()
{
... lines 36 - 38
$client->request('POST', '/api/users', [
'json' => [
... lines 41 - 44
],
'headers' => ['Content-Type' => 'application/ld+json'],
]);
... lines 48 - 51
}
... lines 53 - 109
}

Let's try it!

symfony php bin/phpunit --filter=testCreateUserWithUuid

Now having an id field is ok.

Removing the JSON format Entirely

If you're the only person using your API, then this is probably ok: you just need to know to include this specific header. But if third-parties will use your API, then it's kind of ugly to force them to pass this exact header or get a weird error.

Oh, and by the way, your API clients do need to include a Content-Type header when sending data, otherwise API Platform won't understand it or will think that you're sending form data instead of JSON. Basically, you'll get a 400 error. But the Content-Type header is usually sent automatically by most clients and set to application/json... because the client realizes you're sending JSON.

So... if you want to prevent your API users from needing to override that and send application/ld+json, there is another - kind of more extreme - option: disable the JSON format and always force JSON-LD.

Let's try this. Remove the header:

... lines 1 - 9
class UserResourceTest extends CustomApiTestCase
{
... lines 12 - 33
public function testCreateUserWithUuid()
{
... lines 36 - 38
$client->request('POST', '/api/users', [
... lines 40 - 45
'headers' => ['Content-Type' => 'application/ld+json'],
]);
... lines 48 - 51
}
... lines 53 - 109
}

And then go into config/packages/api_platform.yaml. Down here, comment-out the json format completely:

api_platform:
... lines 2 - 8
formats:
... lines 10 - 13
# json:
# mime_types:
# - application/json
... lines 17 - 23

This means that this is no longer a format that our API can read or output.

There's one other spot I need to change to avoid an error. Open CheeseListing. In a previous tutorial, we set specific formats for this resource to allow a csv format. Remove json from this list:

... lines 1 - 18
/**
* @ApiResource(
... lines 21 - 42
* attributes={
... line 44
* "formats"={"jsonld", "html", "jsonhal", "csv"={"text/csv"}}
* }
* )
... lines 48 - 60
*/
class CheeseListing
{
... lines 64 - 171
}

Ok, let's see what happens! When we run the test...

symfony php bin/phpunit --filter=testCreateUserWithUuid

It still fails. And the error is cool!

The content-type "application/json" is not supported

The test client - realizing that we're sending JSON data - sent this header automatically for us. But now that our API doesn't support JSON, this fails!

Back in api_platform.yaml, move that application/json line up under the jsonld mime type:

api_platform:
... lines 2 - 8
formats:
jsonld:
mime_types:
- application/ld+json
- application/json
... lines 14 - 23

This means that if the Content-Type header is application/json, use jsonld.

Try the test one more time.

symfony php bin/phpunit --filter=testCreateUserWithUuid

And... got it! This solution is a bit odd... but if you're always using JSON-LD, it's one option. But I'm going to remove all of this and be happy to call the field uuid. So I'll put that formats back:

api_platform:
... lines 2 - 8
formats:
jsonld:
mime_types:
- application/ld+json
json:
mime_types:
- application/json
... lines 16 - 22

Re-add the json format:

... lines 1 - 18
/**
* @ApiResource(
... lines 21 - 42
* attributes={
... line 44
* "formats"={"jsonld", "json", "html", "jsonhal", "csv"={"text/csv"}}
* }
* )
... lines 48 - 60
*/
class CheeseListing
{
... lines 64 - 171
}

And, over in the resource class, remove the SerializedName:

... lines 1 - 44
class User implements UserInterface
{
... lines 47 - 54
/**
... lines 56 - 57
* @SerializedName("id")
... line 59
*/
private $uuid;
... lines 62 - 306
}

Whoa! Friends! That's it! Congratulations on taking your API Platform skills up another huge level! You can now properly add custom fields in... about 10 different ways, including by creating custom API resources or DTO's. We also learned how to run custom code on a "state change", like when a cheese listing becomes published. That's a great way to keep your endpoints RESTful, but still run the business code you need in situations like this.

In the next course, we'll dive a bit deeper into one area we haven't talked about much yet: creating true, custom endpoints, both via a controller and also via API Platform's Messenger integration.

If there's something that we still haven't talked about, let us know down in the comments!

Now, go make some awesome JSON, let us know what cool stuff you're building and we'll seeya next time.

Leave a comment!

15
Login or Register to join the conversation
drutpro Avatar

HI! Wonderful course, thanks for your work!
Is there a chance that part 4 will be released at all?

Reply

Hey Drutpro!

Thank you for your feedback! Yes, we definitely want to release more courses about API Platform, this is a great tool with huge capabilities. Unfortunately, I'm not sure when it might be released, for now we're working on many other topics that have bigger priorities for us now.

P.S. But if you have some topics which you would like to see in that 4th episode of API Platform course - please, feel free to share with us in the comments below! It would definitely helpful for us when we will work on planning a new episode!

Cheers!

Reply

I can give some more specifics too. We will, soon, start releasing new courses for API Platform 3 (which was just released). When we do that, we will take a look at all the possible topics and cover what we think is most important. So if you'd like to see something, indeed, let us know!

Cheers!

1 Reply
Twiscard Avatar
Twiscard Avatar Twiscard | posted 2 years ago | edited

Hey there ,

When is Part 4 coming?

Really love your work. Thanks a bunch! Greetings from Romania!

Reply

Hey Bancuadrian,

Thank you for your interest in SymfonyCasts tutorials! Unfortunately, we don't have any plans for recording another ApiPlatform course in the nearest future - many other good things are coming soon. But we will definitely want to do it some day. For now, you can watch for upcoming tutorials on this page: https://symfonycasts.com/co...

Thank you for your patience and understanding!

Cheers!

Reply
Peter L. Avatar
Peter L. Avatar Peter L. | posted 2 years ago

I am not sure about gains of setting UUID from the frontend. But with that requirement this article may be only way to go.

Personally I would ditch the UUID setup from frontend and also autoincrement id altogether and use timebased uuid1 type generation (for MySQL). Performance drop should be very small. But using uuid4 and MySQL it's completely different story.

For MySQL 5 I would use custom converter for uuid to binary and reverting order of uuid. Just google uuid binary and mysql.

Reply

Hey Peter L.!

That seems like a fine solution :). If you're able to set the UUID from the frontend.... then for the situations when you "don't care", you definitely want the UUID to be set directly. I'm not familiar with MySQL's functionality for doing this (you certainly seem to know more about it than I do), but that is definitely a legitimate way of being able to auto-set that column.

Cheers!

Reply
Peter L. Avatar

Sorry, I read my comment again, I did not mean to sound like that. I do not have more experience, only in different area.
Also thanks for the idea of setting uuid from frontend, it is usefull in PWA when it's in "no internet" state. Finally my temporary identifier I used for this is gone.
My experience with uuids was to choose best choice for identifier if you are using mongodb, mysql and sqlite in one project.

Reply

No worries Peter L. - I didn't take it harshly, but thanks for your follow-up :).

Reply
Carlos Avatar

Hey Ryan, can you give us some spoilers about how we could add some custom endpoints to our APIs?
I'm working on a project that is already underway and I will definitely need this. There is a lot of commands that I'll need to trigger, consume some queues, query some process status, etc... Some spoilers would help me a lot.
And also.. when do you think that fourth part of this course will be release? Thanks again

Reply

Hey Carlos!

> can you give us some spoilers about how we could add some custom endpoints to our APIs?

Spoilers! :) The tl;dr for custom operations would be that (A) I try to avoid them when possible by making RESTful endpoints where I can trigger custom code - https://symfonycasts.com/sc... - which would still allow you to trigger custom code (but won't work for all situations). Next, (B) I would try to use the Messenger integration with API Platform. This allows you to return values from your handlers, so it's pretty powerful And (C), finally, if none of that works or feels right, create a custom controller/action - https://api-platform.com/do... - and don't feel bad about it. The worst part of this is that it doesn't support GraphQL (you probably don't care) and if perfect documentation is important to you, then you may need to do some more work with your annotations to help out. If you're creating a custom operation... then API Platform isn't doing "all that much" for you anyways, except for helping integrate with the documentation.

> And also.. when do you think that fourth part of this course will be release? Thanks again

It likely will wait a few months, unfortunately, because we have some other super high priority things to cover like Symfony UX/Stimulus/Turbo &
Symfony 5.2 to name the 2 most important things at the moment. But if you have any other problems or questions, let me know - it can help me make sure I've got the right examples for the tutorial.

Cheers!

1 Reply
Miky Avatar

Do you think, that symfony 5.2 will solve the issues with UUID's that will part of symfony core ?
Do you will later update the course lecture to this new feature ?

https://symfony.com/blog/ne...
Symfony 5.2 includes a Uid normalizer to serialize/deserialize UUIDs and
ULIDs. It also introduces a new validation constraint to validate ULIDs.

https://symfony.com/blog/ne...
Symfony 5.2 provides new Doctrine types to help you work with UUID/ULID values in your entity properties.

Reply

Hey Miky!

Excellent questions!

I haven't used the UUID stuff from 5.2 yet, but I believe that it would not affect too much. What I mean is, it would ultimately work the same, you would just be installing a Symfony component, instead of something from ramsey. In other words, the UUID install chapter - https://symfonycasts.com/sc... - would look different, but I'm pretty sure the rest of the process will continue on more-or-less the same. Let's look at some specifics:

A) The Symfony5.2 UUID component basically accomplishes the same as ramsey/uuid
B) "Symfony 5.2 includes a Uid normalizer to serialize/deserialize UUIDs" - this is nice, but API Platform also comes with a normalizer for ramsey/uuid. So the result should be the same :).
C) "Symfony 5.2 provides new Doctrine types to help you work with UUID/ULID values in your entity properties" - this would mean that you wouldn't need to install ramsey/uuid-doctrine, but both add a "Doctrine type" so it should work the same.

The quirk that we "work around" in this chapter doesn't come from the UUID system, it really comes from the fact that API Platform does not want to allow you send a property called "id" when you are creating an object. That would be true no matter what that id field is (e.g. a UUID from ramsey or Symfony's UUID). This is ultimately a problem that would need to be addressed in API Platform :).

Let me know if that helps clarify things!

Cheers!

Reply
Cat in space

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

This tutorial also works great with API Platform 2.6.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.5.10
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.12.1
        "doctrine/doctrine-bundle": "^2.0", // 2.1.2
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.4.5", // 2.8.2
        "nelmio/cors-bundle": "^2.1", // 2.1.0
        "nesbot/carbon": "^2.17", // 2.39.1
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0 || ^5.0", // 5.2.2
        "ramsey/uuid-doctrine": "^1.6", // 1.6.0
        "symfony/asset": "5.1.*", // v5.1.5
        "symfony/console": "5.1.*", // v5.1.5
        "symfony/debug-bundle": "5.1.*", // v5.1.5
        "symfony/dotenv": "5.1.*", // v5.1.5
        "symfony/expression-language": "5.1.*", // v5.1.5
        "symfony/flex": "^1.1", // v1.18.7
        "symfony/framework-bundle": "5.1.*", // v5.1.5
        "symfony/http-client": "5.1.*", // v5.1.5
        "symfony/monolog-bundle": "^3.4", // v3.5.0
        "symfony/security-bundle": "5.1.*", // v5.1.5
        "symfony/twig-bundle": "5.1.*", // v5.1.5
        "symfony/validator": "5.1.*", // v5.1.5
        "symfony/webpack-encore-bundle": "^1.6", // v1.8.0
        "symfony/yaml": "5.1.*" // v5.1.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.3.2
        "symfony/browser-kit": "5.1.*", // v5.1.5
        "symfony/css-selector": "5.1.*", // v5.1.5
        "symfony/maker-bundle": "^1.11", // v1.23.0
        "symfony/phpunit-bridge": "5.1.*", // v5.1.5
        "symfony/stopwatch": "5.1.*", // v5.1.5
        "symfony/twig-bundle": "5.1.*", // v5.1.5
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.5
        "zenstruck/foundry": "^1.1" // v1.8.0
    }
}
userVoice