The problem

The order service exposes its orders through the API Platform resource OrderResource, linked to the Order entity. We add a preparation priority, an integer, to Order and OrderResource. API consumers want to filter by threshold: /orders?priority[gte]=3.

ComparisonFilter, introduced in 4.3, covers this need. It wraps an equality filter and adds the gt, gte, lt, lte and ne operators to it. Version 5.0 adds between.

// src/ApiResource/OrderResource.php (GetCollection operation)
use ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter;
use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;

new GetCollection(
    parameters: [
        'priority' => new QueryParameter(
            filter: new ComparisonFilter(new ExactFilter()),
            property: 'priority',
        ),
    ],
),

Filtering works, the documentation does not. Swagger UI and Scalar list priority[gt], priority[gte] and so on, each typed object with the five operators as properties.

Swagger UI's "Execute" button then sends gt=...&gte=..., without the priority prefix. The filter receives nothing and the response contains every order.

The cause: the filter describes itself twice. getSchema() returns an object with one property per operator, set on priority. getOpenApiParameters() returns one OpenAPI parameter per operator, without a schema.

While building the document, OpenApiFactory merges each operator with that parent parameter. Lacking a schema of its own, each one inherits the object, with style: form and explode: true. The OpenAPI specification serializes such an object as key=value pairs, without the parameter name.

We reproduced it on 4.4.2, and on 5.0.1 with six operators (between included). The code at fault dates back to 4.3.0.

The solution

Upgrade. Versions 4.4.3 and 5.0.2, released on 2 October 2026, fix the filter. Each operator gets a scalar schema and explode: false: Swagger UI shows plain text fields.

The type stays string, because ExactFilter does not describe the property's type. To document an integer, or on an earlier version (4.3 did not get the fix), we override the documentation.

Override openApi. This property of QueryParameter accepts a list of OpenAPI Parameter objects: one per operator, with a simple schema.

// src/ApiResource/OrderResource.php (GetCollection operation)
use ApiPlatform\OpenApi\Model\Parameter;

'priority' => new QueryParameter(
    filter: new ComparisonFilter(new ExactFilter()),
    property: 'priority',
    openApi: [
        new Parameter('priority[gt]', 'query', 'Priority strictly greater than', schema: ['type' => 'integer']),
        new Parameter('priority[gte]', 'query', 'Priority greater than or equal to', schema: ['type' => 'integer']),
        new Parameter('priority[lt]', 'query', 'Priority strictly less than', schema: ['type' => 'integer']),
        new Parameter('priority[lte]', 'query', 'Priority less than or equal to', schema: ['type' => 'integer']),
        new Parameter('priority[ne]', 'query', 'Priority not equal to', schema: ['type' => 'integer']),
        // 5.0 only: two inclusive bounds, separated by two dots.
        new Parameter('priority[between]', 'query', 'Priority between two bounds', schema: ['type' => 'string'], example: '2..4'),
    ],
),

Each name is complete: priority[gte], not gte. Swagger UI and Scalar then show integer fields, and the request goes out as priority[gte]=3. An operator missing from the list stays active, but disappears from the documentation.

On 5.0, priority[between]=2..4 returns priorities 2 to 4, bounds included. A malformed value (2-4) is ignored without an error: every order comes back.

The pitfall

Do not use the schema key of QueryParameter. It looks like documentation, but it acts at runtime. API Platform derives validation constraints from it and, with castToNativeType: true, a type conversion.

Yet the parameter's value is not an integer: it is the array of operators, for example ['gte' => '3']. On 4.4 as on 5.0:

  • schema: ['type' => 'integer', 'minimum' => 1, 'maximum' => 5]: every request that uses it gets a 422, "This value should be a valid number.";
  • schema: ['type' => 'integer'] with castToNativeType: true: 422 as well, "This value should be of type integer.";
  • minimum alone rejects nothing and maximum alone rejects everything: PHP considers an array greater than any number.

On its own, schema: ['type' => 'integer'] passes and even fixes the display up to 4.4.2 and 5.0.1. Since the fixes, the filter's schema wins: the key no longer changes the documentation.

To validate the values, we use constraints, applied to the whole array. constraints: [new Assert\All([new Assert\Regex('/^\d+$/')])] rejects priority[gte]=abc with a 422 (widen the pattern for between). The rule to remember: openApi describes, constraints validates.

Sources