main Upgrade Guide

# API Platform 4.4 to 5.0

5.0 removes long-deprecated APIs. Components ship with a @beta stability flag (for example "api-platform/state": "^5.0@beta") instead of @alpha. If your 4.4 install runs without deprecation notices, most of this upgrade is a no-op; the sections below cover the changes that are not announced by a 4.4 deprecation.

# API Platform 5.0 Breaking Changes

# JSON:API use_iri_as_id Now Defaults to false

The default announced by the 4.4 deprecation now applies: not setting api_platform.jsonapi.use_iri_as_id explicitly resolves to false instead of true. The JSON:API data.id member carries the resource identifier and the IRI moves to data.links.self.

Before (4.x default, use_iri_as_id: true):

{
    "data": {
        "id": "/dummies/10",
        "type": "Dummy",
        "attributes": {
            "name": "Dummy #10"
        }
    }
}

After (5.0 default, use_iri_as_id: false):

{
    "data": {
        "id": "10",
        "type": "Dummy",
        "links": {
            "self": "/dummies/10"
        },
        "attributes": {
            "name": "Dummy #10"
        }
    }
}

To keep the previous payload, set the option explicitly back to true.

Symfony:

# api/config/packages/api_platform.yaml
api_platform:
    jsonapi:
        use_iri_as_id: true

Laravel:

// config/api-platform.php
return [
    'jsonapi' => [
        'use_iri_as_id' => true,
    ],
];

See JSON:API for the full behavior, including composite identifiers and resources without a standalone item endpoint.

# DeserializeProvider No Longer Accepts a Translator

ApiPlatform\State\Provider\DeserializeProvider drops the Symfony\Contracts\Translation\TranslatorInterface fourth constructor argument that was deprecated in 4.4. DenormalizationViolationFactoryInterface, previously the fifth argument, moves to the fourth position:

public function __construct(
    ?ProviderInterface $decorated,
    SerializerInterface $serializer,
    SerializerContextBuilderInterface $serializerContextBuilder,
    ?DenormalizationViolationFactoryInterface $violationFactory = null,
)

Who is affected: anyone constructing DeserializeProvider by hand, or overriding the api_platform.state_provider.deserialize service definition with a TranslatorInterface argument. Translation of denormalization violations is entirely handled by DenormalizationViolationFactoryInterface since 4.4; drop the translator argument and shift any positional $violationFactory argument one position to the left.

api-platform/state no longer requires symfony/translation-contracts.

# Deprecated APIs Removed

The following long-deprecated APIs are removed:

  • Configuration keys — these Symfony bundle options no longer exist; remove them from config/packages/api_platform.yaml. The configuration reference lists the current options:

    Removed keyReplacement
    validator.query_parameter_validationnone — always on
    enable_link_securitynone — sub-resource link security is always enabled
    resource_class_directoriesnone — #[ApiResource] classes are autoconfigured
    graphql.graphql_playgroundnone — was already ignored
    http_cache.invalidation.varnish_urlshttp_cache.invalidation.urls or scoped_clients
    http_cache.invalidation.xkeya custom purger, see HTTP cache invalidation
  • ObjectMapperProcessorApiPlatform\State\Processor\ObjectMapperProcessor is removed. Use ApiPlatform\State\Processor\ObjectMapperInputProcessor and ApiPlatform\State\Processor\ObjectMapperOutputProcessor instead; see DTOs for the split responsibilities.

  • The $distinctFormats constructor argument — it is removed from both ApiPlatform\JsonSchema\DefinitionNameFactory and ApiPlatform\JsonSchema\SchemaFactory. JSON Schema definition names for formats other than json and merge-patch+json are now always suffixed with the format. If you instantiate SchemaFactory positionally, its $definitionNameFactory argument moves from the seventh to the sixth position.

  • ValidationException’s string-message constructor — the first constructor argument only accepts a Symfony\Component\Validator\ConstraintViolationListInterface now; the string|ConstraintViolationListInterface union and the plain-string code path are removed.

  • ApiTestCase::$alwaysBootKernel defaulting to null — the property now defaults to false (kernel not rebooted between requests if already booted) instead of triggering a deprecation and always booting the kernel. Set it to true in your test class if you relied on the implicit always-boot behavior.

  • Automatic short-name deduplication — the defaults.extra_properties.deduplicate_resource_short_names opt-in flag is removed. Resources sharing a shortName are now always deduplicated with a numeric suffix (AttributeResource2, Employee3, …), unconditionally, instead of raising a deprecation when two #[ApiResource] attributes shared the same shortName without opting in.

  • Explicit api_assign_object_to_populate contextDeserializeProvider no longer falls back to computing SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE (api_assign_object_to_populate) from the HTTP method itself; it only assigns the loaded object to the denormalization context’s object_to_populate when that flag is already set. Symfony’s MainController and DeserializeListener, and Laravel’s ApiPlatformController, already set it for POST, PATCH, and non-standard PUT before calling the provider, so this only matters if you call DeserializeProvider::provide() directly from a custom controller or pipeline.

  • Serializer-aware state providersApiPlatform\State\SerializerAwareProviderInterface and ApiPlatform\State\SerializerAwareProviderTrait are removed. Inject Symfony\Component\Serializer\SerializerInterface through your provider’s constructor instead of relying on setSerializerLocator(). The internal DataProviderPass that performed setter injection for these providers is removed as a consequence.

See core#8367 for the full diff.

# Legacy PropertyInfo Type System Removed

The legacy symfony/property-info Type system is replaced by symfony/type-info throughout:

  • ApiProperty::$builtinTypes, ApiProperty::getBuiltinTypes(), and ApiProperty::withBuiltinTypes() are removed. Use ApiProperty::getNativeType() / withNativeType(), which return a Symfony\Component\TypeInfo\Type.
  • GraphQL’s TypeConverterInterface::convertType() is removed. Implement convertPhpType() instead; it takes a Symfony\Component\TypeInfo\Type rather than a legacy Symfony\Component\PropertyInfo\Type.
  • ContextAwareTypeBuilderInterface::isCollection(LegacyType $type) is removed entirely; collection detection is handled internally from the Type object.

Who is affected: custom TypeConverterInterface or ContextAwareTypeBuilderInterface implementations, and any code reading ApiProperty::getBuiltinTypes().

# PropertyAwareFilterInterface::getProperties() Is Now a Real Interface Method

getProperties(): ?array was previously only documented via an @method docblock annotation on ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface (with the real method commented out). It is now declared on the interface. Custom filters implementing PropertyAwareFilterInterface without a getProperties() method now fail with a fatal “must implement” error; add the method (or use PropertyAwareFilterTrait).

# JSON:API Error status Is Now a String

ApiPlatform\JsonApi\Serializer\ErrorNormalizer now always casts the status member of a JSON:API error object to a string, matching the JSON:API error object spec. Update any client or custom normalizer that expects status to be an integer.

# JSON-LD /contexts/Error and /contexts/ConstraintViolationList Are No Longer Special-Cased

ApiPlatform\JsonLd\Action\ContextAction::RESERVED_SHORT_NAMES and the hardcoded base-context fallback for the Error and ConstraintViolationList short names are removed. Since exceptions and validation errors have been resources since 3.2, their @context is now built through the normal per-resource context loop like any other resource. The /contexts/ConstraintViolationList route no longer has a producer: responses reference /contexts/ConstraintViolation (singular) instead.

Who is affected: code or tests hardcoding /contexts/ConstraintViolationList URLs.

# SerializerContextBuilder No Longer Injects uri_variables

SerializerContextBuilder::createFromRequest() no longer populates $context['uri_variables'] from the request attributes. The key is still set — but only later in the state pipeline, by SerializeProcessor and DeserializeProvider, which already had the correctly parsed values.

Who is affected: custom normalizers reading $context['uri_variables'] in a context built directly from SerializerContextBuilder::createFromRequest() outside the standard state pipeline (for example, a custom controller that calls it directly). Normalizers invoked through the regular provider/processor flow are unaffected — the key is still present by the time normalization runs.

# UniqueConstraintViolationException Maps to 422 by Default

Doctrine\DBAL\Exception\UniqueConstraintViolationException is added to the Symfony bundle’s default exception_to_status map, resolving to 422 Unprocessable Entity (alongside the pre-existing OptimisticLockException => 409 Conflict). If you previously mapped this exception yourself, or relied on it falling through to 500, review your exception_to_status configuration. See Exception to status.

# Doctrine Filters: RangeFilter Deprecated, ComparisonFilter Gains [between]

ComparisonFilter now natively supports ?price[between]=10..100, covering the full range syntax ([gt]/[gte]/[lt]/[lte]/[between]). RangeFilter is deprecated in favor of it and is removed in 6.0; DateFilter and ExistsFilter become standalone classes (they no longer extend AbstractFilter) with no change to their URL syntax. See Doctrine Filters for the full migration path.

# API Platform 5.0 Deprecations

# FilterInterface::getDescription() Stays Deprecated

ApiPlatform\Metadata\FilterInterface::getDescription() was deprecated in 4.2 for removal in 6.0. That removal is deferred to 6.0 and does not happen in 5.0 — the method, #[ApiFilter], Operation::$filters, and the AbstractFilter base class all still work in 5.0. Only migrate off them when you are ready to keep pace with the 6.0 timeline; see Doctrine Filters for the modern filter interfaces.

# API Platform 5.0 Package Changes

The testing utilities have moved from api-platform/symfony to the new api-platform/test package. Install it as a development dependency:

composer require --dev api-platform/test:^5.0@beta

Update imports to use the new namespace:

use ApiPlatform\Test\ApiTestCase;

ApiPlatform\Symfony\Bundle\Test\ApiTestCase remains as a deprecated compatibility shim when api-platform/test is installed, but it emits a deprecation notice. The other testing utilities have moved under the same ApiPlatform\Test namespace.

The removal of the internal Request::getContentType() fallbacks and the Symfony 6 value-resolver compatibility interface requires no application changes.

# API Platform 4.3 to 4.4

4.4 is the last 4.x minor. It ships a single backwards-incompatible change (below); everything else is a deprecation that keeps working until it is removed in a later major (5.0 or 6.0). Fixing the deprecations now makes the upgrade to the next major a no-op.

# Backwards-Incompatible Changes

# Denormalization Type Errors on Unconstrained BackedEnum Properties Revert to HTTP 400

Prior to 4.4, BackedEnum-typed properties received special treatment: any serializer type mismatch during denormalization was unconditionally promoted to HTTP 422. Starting with 4.4, that implicit promotion is replaced by a constraint-aware check.

Who is affected: code that relied on enum-typed properties producing 422 without any Symfony Validator constraint (or Laravel rule) on the property.

What to do (Symfony): add an explicit constraint on the enum property:

use Symfony\Component\Validator\Constraints as Assert;

#[Assert\Type(Status::class)]
public Status $status;

Alternatively, enable Symfony Validator’s auto-mapping on the resource class. Auto-mapping generates an implicit Type constraint from the PHP type declaration, which is sufficient for the 422 promotion to apply.

What to do (Laravel): add a rule for the property in rules:

#[ApiResource(
    rules: ['status' => 'required']
)]

Properties that already carry any constraint or rule are unaffected — they continue to produce 422.

For the full rule tables and additional details, see Constraint-Aware 422 for Denormalization Errors (Symfony) and the equivalent section in the Laravel validation guide.

# Deprecations

# Legacy Doctrine Filters

The legacy Doctrine filter API is deprecated in favor of parameter-based filters declared with the #[QueryParameter] attribute. The #[ApiFilter] attribute, the Operation::$filters property, and the AbstractFilter base class (Doctrine ORM and MongoDB ODM) are all deprecated and removed in 6.0.

There are two kinds of migration:

  • Replaced filters — removed in 6.0, swap the class:

    Legacy filterReplacement
    SearchFilterExactFilter / PartialSearchFilter / IriFilter (depending on the strategy)
    BooleanFilter, NumericFilter, BackedEnumFilterExactFilter
    OrderFilterSortFilter
  • Kept filtersDateFilter, RangeFilter and ExistsFilter survive. Only the way you declare them is deprecated: move the declaration from #[ApiFilter] to #[QueryParameter]. The class name and the URL syntax stay the same (drop-in).

ComparisonFilter and OrFilter are now stable (no longer experimental) and are the recommended building blocks for comparison and disjunction filtering.

A codemod automates the rewrite of #[ApiFilter] declarations to #[QueryParameter]:

bin/console api:upgrade-filter

See the filter migration guide for the full table and before/after examples.

When instantiating a filter inside a QueryParameter, always use named arguments

(new DateFilter(nullManagement: ...) rather than positional). Filter constructors are refined across versions; named arguments keep your declarations forward-compatible.

# JSON:API use_iri_as_id

Not setting api_platform.jsonapi.use_iri_as_id explicitly is deprecated. The default changes from true to false in 5.0. Set it explicitly to silence the deprecation and lock in the behavior you want:

# api/config/packages/api_platform.yaml
api_platform:
    jsonapi:
        use_iri_as_id: true # keep IRIs as the "id" field; set to false to use entity identifiers

# Security AccessDeniedException

ApiPlatform\Symfony\Security\Exception\AccessDeniedException is deprecated. Use ApiPlatform\Metadata\Exception\AccessDeniedException instead.

# Denormalization Moved Out of the Item Normalizers

The item normalizers no longer denormalize. Calling denormalize() on one of them triggers a deprecation, and the denormalization code moves to a new ItemDenormalizer class in the same namespace:

Deprecated denormalization entry pointReplacement
ApiPlatform\Serializer\ItemNormalizerApiPlatform\Serializer\ItemDenormalizer
ApiPlatform\JsonLd\Serializer\ItemNormalizerApiPlatform\JsonLd\Serializer\ItemDenormalizer
ApiPlatform\JsonApi\Serializer\ItemNormalizerApiPlatform\JsonApi\Serializer\ItemDenormalizer
ApiPlatform\GraphQl\Serializer\ItemNormalizerApiPlatform\GraphQl\Serializer\ItemDenormalizer

This affects you only if you decorate or extend one of these classes to change how API Platform reads an incoming payload. If you decorate a normalizer service to alter denormalization, decorate the matching denormalizer service instead:

FormatDenormalizer service
Defaultapi_platform.serializer.denormalizer.item
JSON-LDapi_platform.jsonld.denormalizer.item
JSON:APIapi_platform.jsonapi.denormalizer.item
GraphQLapi_platform.graphql.denormalizer.item

Decoration that only changes normalization keeps working without a change.

# API Platform 4.2 to 4.3

# Breaking Changes

# Doctrine Filters Require Explicit property

Doctrine parameter-based filters (ExactFilter, IriFilter, PartialSearchFilter, UuidFilter) now throw InvalidArgumentException if the property attribute is missing. If you have filter parameters without an explicit property, you must either add one or use the :property placeholder in your parameter name.

// Before (would silently work without property):
#[ApiFilter(ExactFilter::class)]

// After (property is required):
#[ApiFilter(ExactFilter::class, property: 'name')]
// Or use the :property placeholder in the parameter name

# Readonly Doctrine Entities Lose PUT & PATCH

Entities marked as readonly via Doctrine metadata ($classMetadata->markReadOnly()) no longer expose PUT and PATCH operations. Clients sending PUT/PATCH to these resources will receive a 404. If you need write operations on readonly entities, explicitly define them in your ApiResource attribute.

# JSON-LD @type with output and itemUriTemplate

When using output with itemUriTemplate on a collection operation, the JSON-LD @type now uses the resource class name instead of the output DTO class name for semantic consistency with itemUriTemplate behavior. Update any client code that relies on the DTO class name in @type.

# Behavioral Changes

# isGranted Evaluated Before Provider

Security expressions are now evaluated before the state provider runs. Expressions that do not reference the object variable will be checked at the pre_read stage, improving security by preventing unnecessary database queries on unauthorized requests. Expressions that reference object still wait for the provider to resolve the entity. Review any security expressions that relied on provider side-effects running before authorization.

# Hydra Class @id Now Always Uses #ShortName

Hydra documentation classes now consistently use #ShortName as their @id instead of schema.org type URIs (e.g. schema:Product). Semantic types configured via types are now exposed through rdfs:subClassOf. Clients should expect class @id and property range changes in the Hydra documentation if resources had custom types configured.

# LDP-Compliant Response Headers

API responses now include Allow and Accept-Post headers per the Linked Data Platform specification. These are informational headers that help clients discover API capabilities and should not break existing integrations.

# API Platform 3.4

Remove the keep_legacy_inflector, the event_listeners_backward_compatibility_layer and the rfc_7807_compliant_errors flag:

api_platform:
        event_listeners_backward_compatibility_layer: false
        keep_legacy_inflector: false
        extra_properties:
            standard_put: true
            rfc_7807_compliant_errors: true

If you use a custom normalizer for validation exception use:

api_platform:
    validator:
        legacy_validation_exception: true

Indeed, we will throw another validation class in API Platform 4 we will throw ApiPlatform\Validator\Exception\ValidationException instead of ApiPlatform\Symfony\Validator\Exception\ValidationException

It’s really important to add the use_symfony_listeners flag, set to true if you use Symfony listeners or controllers:

api_platform:
    use_symfony_listeners: false

The keep_legacy_inflector flag will be removed from API Platform 4, you need to fix your issues first. In API Platform 3.4, the Inflector is available as a service that you can configure through:

api_platform:
    inflector: api_platform.metadata.inflector

Implement the ApiPlatform\Metadata\InflectorInterface if you need to tweak its behavior.

We added an hydra_prefix configuration as the hydra: prefix will be removed by default in API Platform 4:

api_platform:
    serializer:
        hydra_prefix: false

Standard PUT is now true by default, you can change its value using:

api_platform:
    defaults:
        extra_properties:
            standard_put: true

We recommend using the standalone API Platform packages instead of the Core monolithic repository.

Update your composer.json like that:

 {
     "require": {
        "api-platform/core": "^3",
        "api-platform/symfony": "^3 || ^4"
        // also add the extra packages you need, like "api-platform/doctrine-orm"
     }
 }

# API Platform 3.1/3.2

This is the recommended configuration for API Platform 3.2. We review each of these changes in this document.

api_platform:
    title: Hello API Platform
    version: 1.0.0
    formats:
        jsonld: ["application/ld+json"]
    docs_formats:
        jsonld: ["application/ld+json"]
        jsonopenapi: ["application/vnd.openapi+json"]
        html: ["text/html"]
    defaults:
        stateless: true
        cache_headers:
            vary: ["Content-Type", "Authorization", "Origin"]
        extra_properties:
            standard_put: true
            rfc_7807_compliant_errors: true
    event_listeners_backward_compatibility_layer: false
    keep_legacy_inflector: false

# Formats

We noticed that API Platform was enabling json by default because of our OpenAPI support. We introduced the new application/vnd.openapi+json. Therefore if you want json you need to explicitly handle it:

formats:
    json: ["application/json"]

You can also remove documentations you’re not using via the new docs_formats.

A new option error_formats is also used for content negotiation.

# Event listeners

For new users we recommend to use

event_listeners_backward_compatibility_layer: false

This allows API Platform to not use http kernel event listeners. It also allows you to force options like read: true or validate: true. This simplifies use cases like validating a delete operation Event listeners will not get removed and are not deprecated, they’ll use our providers and processors in a future version.

# Inflector

We’re switching to symfony/string inflector, to keep using doctrine/inflector use:

keep_legacy_inflector: true

We strongly recommend that you use your own inflector anyways with a PathSegmentNameGenerator.

# Errors

defaults:
    extra_properties:
        rfc_7807_compliant_errors: true

As this is an extraProperties it’s configurable per resource/operation. This is improving the compatibility of Hydra errors with JSON problem. It also enables new extension points on Errors such as Error provider and Error Resource.

# OpenApi context

You may want to convert your openApiContext to openapi, doing so is quite fastidious, @lyrixx created a rector script to help if needed:

https://github.com/lyrixx/rector-apip-openapi

You can also help us improve the documentation of this page.

Using an AI coding agent? See the documentation index for LLMs at /docs/llms.txt.

Made with love by

Les-Tilleuls.coop can help you design and develop your APIs and web projects, and train your teams in API Platform, Symfony, Next.js, Kubernetes and a wide range of other technologies.

Learn more

Copyright © 2023 Kévin Dunglas

Sponsored by Les-Tilleuls.coop