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!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.5.7
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.10.4
        "doctrine/doctrine-bundle": "^2.0", // 2.1.2
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.1
        "doctrine/orm": "^2.4.5", // v2.7.3
        "nelmio/cors-bundle": "^2.1", // 2.1.0
        "nesbot/carbon": "^2.17", // 2.39.1
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.4
        "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.9.10
        "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.7.3
        "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.21.1
        "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.1.2
    }
}