API Platform 3.3

API Platform 3.3 is out and adds nice improvements once again! Many new options added to the Metadata classes, an increased focus on RDF at the heart of API design and a target to make API Platform more Laravel friendly.

Many thanks to every contributor that made API Platform more pleasant to use! Don’t miss our API Platform Conference, we completed the program last week, it’s huge! As the last early bird tickets were sold in a few days ❤️ we added some more to celebrate the release!

This blog post relates the important changes, for a more detail look read our changelog.

# Formats

We decided to deprecated not setting the formats option on API Platform. We are convinced that when using API Platform you should use an hypermedia format such as JSON-LD or JSON:API and we don’t recommend plain JSON.

# Metadata

Metadata attributes (ApiResource and HTTP Verbs) have a new headers option that allows setting up HTTP Response headers:

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;

    headers: ['Location' => '/foobar', 'Hello' => 'World'],
    status: 301,
    output: false,
    operations: [
        new Get(uriTemplate: '/redirect_to_foobar'),

The Link option has a security option allowing to bind values represented by the toProperty or fromProperty:

use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Link;

    uriTemplate: '/employees/{employeeId}/company',
    uriVariables: [
        'employeeId' => new Link(
            fromClass: Employee::class,
            toProperty: 'company',
            security: "is_granted(some_voter, company)"

We also improved the security context on ApiProperty by adding the property name in its context.

If our automatic linking system isn’t matching your needs, you can hook a service that handles links. Its interface is provider-specific and can be faster to set up than a provider.

Last but not least, there’s a new getOperation Expression Language function that can be used on Mercure topics:

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\UrlGeneratorInterface;

    // ...
    mercure: [
        'topics' => [
                get_operation(object, "/foos/bars/{id}{._format}")

This improves a lot IRI generation as you can choose the operation you want the IRI from.

# Parameters

A new attribute is introduced named Parameter. It has two variants: a QueryParameter and a HeaderParameter. This allows more flexibility regarding how filters are called on API Platform resources.

use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;

    uriTemplate: 'orders',
    parameters: [
        'order[:property]' => new QueryParameter(filter: 'offer.order_filter'),
class Offer {
    public string $id;
    public string $name;

Read about all the possibilities of our parameters on the documentation. Note that validation on parameters is using the Symfony Validator, you can either bind constraints or specify a documentation (openapi, hydra, json schema) and it will automatically add constraints according to your input.

# Cache

API Platform has an invalidation mechanism based on IRIs that collects errors automatically during serialization. When you have specific needs, it wasn’t easy to specify what iris you want to invalidate. To do so we introduced a new TagCollectorInterface to help collecting specific IRIs:

use ApiPlatform\Serializer\TagCollectorInterface;

final class TagCollectorDefault implements TagCollectorInterface
    public function collect(array $context = []): void
        if (isset($context['property_metadata'])) {

        $iri = $context['iri'];

        if (isset($context['resources']) && isset($iri)) {
            $context['resources'][$iri] = $iri;

We recommend to read the code implementation if you want more details.

# Documentation

# Hydra

We now read hydra:property if it exists in the jsonLdContext and use its value directly in the documentation:

use ApiPlatform\Metadata\ApiProperty;

#[ApiProperty(jsonLdContext: [
    'hydra:property' => [
        'rdfs:label' => 'change this value',
         'owl:maxCardinality' => 5

Previously, only @type was read.

# OpenAPI

A new option is available to control whether API Platform adds OpenAPI responses automatically or not, you can either use the global configuration api_platform.openapi.overrideResponses (true by default) or specify an extra property on your operations:

use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\OpenApiFactory;
use ApiPlatform\OpenApi\Model as OpenApi;

    uriTemplate: '/override_open_api_responses',
    openapi: new OpenApi\Operation(
        responses: [
            '204' => new OpenApi\Response(
                description: 'User activated',
    extraProperties: [OpenApiFactory::OVERRIDE_OPENAPI_RESPONSES => false],

No other responses will be added by API Platform, don’t forget to add error responses.

# Deprecations

These namespaces are deprecated:


Public classes are now in the Metadata namespace.

# To listen or not to listen

Following on from the work done on supporting Laravel, we increased our focus on splitting API Platform into 18 components:

You can install api-platfom/json-schema or api-platform/openapi without the whole framework if you only need that subset of functionnalities. On top of that, HTTP Kernel listeners are Symfony specific and as our Provider/Processor mechanism is quite powerful, we introduce a way to remove listeners alltogether in favor of only using providers. This is great as we will share more code between all the subsystems (Laravel, GraphQl, etc.). Event listeners are not deprecated but if you want to use them, or you need Symfony controllers, you’ll need to enable listeners using:

    use_symfony_listeners: true

This adds back the listeners. Under the hood the ReadListener only calls the ReadProvider, so decorating the provider will still work. This is valid for every events, each event has a provider using the same name.

# Fixes

Many fixes were brought to this minor version to improve the developer experience, such as not serializing the Request object in the Messenger context stamp, or updating the MissingConstructorArgumentsException message to make it more informational.