API Platform version(s) affected: 4.3.15 (still present on main as of 2026-07-02)
Environment: PHP 8.3, Symfony 6.4, Doctrine ORM, CamelCaseToSnakeCaseNameConverter configured as api_platform.name_converter
Description
Follow-up to #7866 / #7877, which fixed the missing nameConverter injection for legacy AbstractFilter subclasses used inline with QueryParameter.
The modern parameter filters (ExactFilter, SortFilter, ComparisonFilter, PartialSearchFilter, ...) have a related but distinct problem: when a name converter is configured, ParameterResourceMetadataCollectionFactory::setDefaults() normalizes Parameter::property (createdAt -> created_at), and the filters then use $parameter->getProperty() as-is in the DQL. Since the Doctrine field is createdAt, every request filtering on a multi-word property fails with HTTP 500:
Doctrine\ORM\Query\QueryException: [Semantical Error] ... Class App\Entity\Book has no field or association named created_at
Single-word properties (status, name, ...) are unaffected (camelCase == snake_case for them), which makes the bug easy to miss in a test suite.
Note that the migration guide currently recommends the modern filters as the fix for the legacy filters' nameConverter issues ("If you encounter silent filter failures with camelCase properties (e.g., createdAt, firstName), upgrading to the modern filter equivalents listed above is the recommended solution"), but on this setup they fail harder than the legacy ones (500 instead of silently ignoring the filter).
How to reproduce
# config/packages/api_platform.yaml
api_platform:
name_converter: 'Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter'
#[ApiResource]
#[GetCollection(
parameters: [
'created_at' => new QueryParameter(filter: new ExactFilter(), property: 'createdAt'),
'sortBy[created_at]' => new QueryParameter(filter: new SortFilter(), property: 'createdAt'),
],
)]
class Book
{
// ...
public \DateTimeImmutable $createdAt;
}
GET /books?created_at=2026-01-01 -> 500 (has no field or association named created_at)
GET /books?sortBy[created_at]=asc -> 500
- same with
ComparisonFilter(new ExactFilter()) and ?created_at[gt]=...
GET /books?name=foo (single-word property) -> works
Possible Solution
Mirror what #7877 did for the legacy filters: denormalize Parameter::property back to the PHP field name before it reaches the query, either in ParameterExtensionTrait::configureFilter() or in the filters themselves. The metadata factory already preserves the PHP path for nested properties (nested_properties_info skips the name conversion in setDefaults()), so flat multi-word properties are the only remaining case.
Additional Context
Workaround we are shipping in the meantime — a small decorating filter that restores the camelCase property before delegating:
final class DenormalizePropertyFilter implements FilterInterface, ManagerRegistryAwareInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
{
use ManagerRegistryAwareTrait;
public function __construct(private readonly FilterInterface $inner)
{
$this->nameConverter = new CamelCaseToSnakeCaseNameConverter();
}
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
{
if ($this->inner instanceof ManagerRegistryAwareInterface && $this->hasManagerRegistry()) {
$this->inner->setManagerRegistry($this->getManagerRegistry());
}
$parameter = $context['parameter'] ?? null;
if ($parameter instanceof Parameter && null !== ($property = $parameter->getProperty())) {
$context['parameter'] = $parameter->withProperty($this->nameConverter->denormalize($property));
}
$this->inner->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context);
}
// getDescription/getSchema/getOpenApiParameters delegate to $this->inner
}
API Platform version(s) affected: 4.3.15 (still present on
mainas of 2026-07-02)Environment: PHP 8.3, Symfony 6.4, Doctrine ORM,
CamelCaseToSnakeCaseNameConverterconfigured asapi_platform.name_converterDescription
Follow-up to #7866 / #7877, which fixed the missing
nameConverterinjection for legacyAbstractFiltersubclasses used inline withQueryParameter.The modern parameter filters (
ExactFilter,SortFilter,ComparisonFilter,PartialSearchFilter, ...) have a related but distinct problem: when a name converter is configured,ParameterResourceMetadataCollectionFactory::setDefaults()normalizesParameter::property(createdAt->created_at), and the filters then use$parameter->getProperty()as-is in the DQL. Since the Doctrine field iscreatedAt, every request filtering on a multi-word property fails with HTTP 500:Single-word properties (
status,name, ...) are unaffected (camelCase == snake_case for them), which makes the bug easy to miss in a test suite.Note that the migration guide currently recommends the modern filters as the fix for the legacy filters' nameConverter issues ("If you encounter silent filter failures with camelCase properties (e.g.,
createdAt,firstName), upgrading to the modern filter equivalents listed above is the recommended solution"), but on this setup they fail harder than the legacy ones (500 instead of silently ignoring the filter).How to reproduce
#[ApiResource] #[GetCollection( parameters: [ 'created_at' => new QueryParameter(filter: new ExactFilter(), property: 'createdAt'), 'sortBy[created_at]' => new QueryParameter(filter: new SortFilter(), property: 'createdAt'), ], )] class Book { // ... public \DateTimeImmutable $createdAt; }GET /books?created_at=2026-01-01-> 500 (has no field or association named created_at)GET /books?sortBy[created_at]=asc-> 500ComparisonFilter(new ExactFilter())and?created_at[gt]=...GET /books?name=foo(single-word property) -> worksPossible Solution
Mirror what #7877 did for the legacy filters: denormalize
Parameter::propertyback to the PHP field name before it reaches the query, either inParameterExtensionTrait::configureFilter()or in the filters themselves. The metadata factory already preserves the PHP path for nested properties (nested_properties_infoskips the name conversion insetDefaults()), so flat multi-word properties are the only remaining case.Additional Context
Workaround we are shipping in the meantime — a small decorating filter that restores the camelCase property before delegating: