API Platform Core allows to choose which attributes of the resource are exposed during the normalization (read) and denormalization (write) process. It relies on the serialization (and deserialization) groups feature of the Symfony Serializer component.
The Symfony Serializer component allows to specify the definition of serialization using XML, YAML, or annotations. As annotations are really easy to understand, we will use them in this documentation.
However, if you don't use the official distribution of API Platform, don't forget to enable annotation support in the serializer configuration:
# app/config/config.yml
framework:
serializer: { enable_annotations: true }
Specifying to the API system the groups to use is really simple:
<?php
// src/AppBundle/Entity/Book.php
namespace AppBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @ApiResource(attributes={
* "normalization_context"={"groups"={"read"}},
* "denormalization_context"={"groups"={"write"}}
* })
*/
class Book
{
/**
* @Groups({"read", "write"})
*/
private $name;
/**
* @Groups({"write"})
*/
private $author;
// ...
}
With the config of the previous example, the name
property will be accessible in read and write, but the author
property
will be write only, therefore the author
property will never be included in documents returned by the API.
The value of the normalization_context
is passed to the Symfony Serializer during the normalization process. In the same
way, denormalization_context
is used for denormalization.
You can configure groups as well as any Symfony Serializer option configurable through the context argument (e.g. the enable_max_depth
key when using the @MaxDepth
annotation).
Built-in actions and the Hydra documentation generator will leverage the specified serialization and deserialization groups to give access only to exposed properties and to guess if they are readable and/or writable.
It is possible to specify normalization and denormalization contexts (as well as any other attribute) on a per operation basis. API Platform Core will always use the most specific definition. For instance if normalization groups are set both at the resource level and at the operation level, the configuration set at the operation level will be used and the resource level ignored.
In the following example we use different serialization groups for the GET
and PUT
operations:
<?php
// src/AppBundle/Entity/Book.php
namespace AppBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @ApiResource(
* attributes={"normalization_context"={"groups"={"get"}}},
* itemOperations={
* "get"={"method"="GET"},
* "put"={"method"="PUT", "normalization_context"={"groups"={"put"}}}
* }
* )
*/
class Book
{
/**
* @Groups({"get", "put"})
*/
private $name;
/**
* @Groups({"get"})
*/
private $author;
// ...
}
name
and author
properties will be included in the document generated during a GET
operation because the configuration
defined at the resource level is inherited. However the document generated when a PUT
request will be received will only
include the name
property because of the specific configuration for this operation.
Refer to the documentation of operations to learn more about the concept of operations.
By default, the serializer provided with API Platform Core represents relations between objects by dereferenceables IRIs. They allow to retrieve details of related objects by issuing an extra HTTP request.
In the following JSON document, the relation from a book to an author is represented by an URI:
{
"@context": "/contexts/Book",
"@id": "/books/62",
"@type": "Book",
"name": "My awesome book",
"author": "/people/59"
}
To improve the application's performance, it is sometimes necessary to avoid issuing extra HTTP requests. It is possible
to embed related objects (or only some of their properties) directly in the parent response trough serialization groups.
By using the following serialization groups annotations (@Groups
), a JSON representation of the author is embedded in
the book response:
<?php
// src/AppBundle/Entity/Book.php
namespace AppBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @ApiResource(attributes={"normalization_context"={"groups"={"book"}}})
*/
class Book
{
/**
* @Groups({"book"})
*/
private $name;
/**
* @Groups({"book"})
*/
private $author;
// ...
}
<?php
// src/AppBundle/Entity/Person.php
namespace AppBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @ApiResource
*/
class Person
{
/**
* ...
* @Groups({"book"})
*/
public $name;
// ...
}
The generated JSON with previous settings will be like the following:
{
"@context": "/contexts/Book",
"@id": "/books/62",
"@type": "Book",
"name": "My awesome book",
"author": {
"@id": "/people/59",
"@type": "Person",
"name": "Kévin Dunglas"
}
}
In order to optimize such embedded relations, the default Doctrine data provider will automatically join entities on relations
marked as EAGER
avoiding extra queries to be executed when serializing the sub-objects.
It is also possible to embed a relation in PUT
and POST
requests. To enable that feature, the serialization groups must be
set the same way as normalization and the configuration should be like this:
<?php
// src/AppBundle/Entity/Book.php
namespace AppBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
/**
* @ApiResource(attributes={"denormalization_context"={"groups"={"book"}}})
*/
class Book
{
// ...
}
The following rules apply when denormalizating embedded relations:
- If a
@id
key is present in the embedded resource, the object corresponding to the given URI will be retrieved trough the data provider and any changes in the embedded relation will be applied to that object. - If no
@id
key exists, a new object will be created containing data provided in the embedded JSON document.
You can create as relation embedding levels as you want.
Let's imagine a resource where most fields can be managed by any user, but some can be managed by admin users only:
<?php
// src/AppBundle/Entity/Book.php
namespace AppBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @ApiResource(attributes={
* "normalization_context"={"groups"={"book_output"}},
* "denormalization_context"={"groups"={"book_input"}}
* })
*/
class Book
{
// ...
/**
* This field can be managed by an admin only
*
* @var bool
*
* @Groups({"book_output", "admin_input"})
*/
private $active = false;
/**
* This field can be managed by any user
*
* @var string
*
* @Groups({"book_output", "book_input"})
*/
private $name;
}
All entry points are the same for all users, so we should find a way to detect if authenticated user is admin, and if so
dynamically add admin_input
to deserialization groups.
API Platform implements a ContextBuilder
, which prepares the context for serialization & deserialization. Let's
decorate this service to override the
createFromRequest
method:
# app/config/services.yml
services:
app.serializer.builder.book:
decorates: api_platform.serializer.context_builder
class: AppBundle\Serializer\BookContextBuilder
arguments: ['@app.serializer.builder.book.inner', '@security.authorization_checker']
<?php
// src/AppBundle/Serializer/BookContextBuilder.php
namespace AppBundle\Serializer;
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use AppBundle\Entity\Book;
final class BookContextBuilder implements SerializerContextBuilderInterface
{
private $decorated;
private $authorizationChecker;
public function __construct(SerializerContextBuilderInterface $decorated, AuthorizationCheckerInterface $authorizationChecker)
{
$this->decorated = $decorated;
$this->authorizationChecker = $authorizationChecker;
}
public function createFromRequest(Request $request, bool $normalization, array $extractedAttributes = null) : array
{
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
$subject = $request->attributes->get('data');
if ($subject instanceof Book && $this->authorizationChecker->isGranted('ROLE_ADMIN') && false === $normalization) {
$context['groups'][] = 'admin_input';
}
return $context;
}
}
If the user has ROLE_ADMIN
permission and the subject is an instance of Book, admin_input
group will be dynamically added to the denormalization context.
The variable $normalization
lets you check whether the context is for normalization (if true) or denormalization.
The Serializer Component provides a handy way to map PHP field names to serialized names. See the related Symfony documentation.
To use this feature, declare a new service with id app.name_converter
. For example, you can convert CamelCase
to
snake_case
with the following configuration:
# app/config/services.yml
services:
app.name_converter:
class: Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter
public: false
# app/config/config.yml
api_platform:
name_converter: app.name_converter
API Platform is able to guess the entity identifier using Doctrine metadata. It also supports composite identifiers.
If Doctrine ORM is not used, the identifier must be marked explicitly using the identifier
attribute of the ApiPlatform\Core\Annotation\ApiProperty
annotation.
Most of the time, the property identifying the entity is not included in the returned document but is included as a part
of the URI contained in the @id
field. So in the /apidoc
endpoint the identifier will not appear in the properties list.
However, when using composite identifier, properties composing the identifier are included in the API response and in the documentation.
In some cases, you will want to set the identifier of a resource from the client (like a slug for example).
In this case the identifier property must become a writable class property in the /apidoc
endpoint.
To do this you simply have to:
- Create a setter for identifier in the entity.
- Add the denormalization group to the property if you use a specific denormalization group.
By default, the generated JSON-LD context (@context
) is only reference by
an IRI. A client supporting JSON-LD must send a second HTTP request to retrieve it:
{
"@context": "/contexts/Book",
"@id": "/books/62",
"@type": "Book",
"name": "My awesome book",
"author": "/people/59"
}
You can configure API Platform Core to embed the JSON-LD context in the root document like the following:
{
"@context": {
"@vocab": "http://localhost:8000/apidoc#",
"hydra": "http://www.w3.org/ns/hydra/core#",
"name": "http://schema.org/name",
"author": "http://schema.org/author"
},
"@id": "/books/62",
"@type": "Book",
"name": "My awesome book",
"author": "/people/59"
}
To do so, use the following configuration:
<?php
// src/AppBundle/Entity/Book.php
namespace AppBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
/**
* @ApiResource(attributes={"jsonld_embed_context"=true})
*/
class Book
{
// ...
}
Previous chapter: Filters
Next chapter: Validation