-
-
Notifications
You must be signed in to change notification settings - Fork 962
Description
API Platform version(s) affected: 3.x
Description When using a PATCH operation with a dedicated input DTO and a different output class, the ObjectMapperProvider behaves unexpectedly.
Please as it's difficult to note the exact behaviour issue, I've provided a full reproductible in the attached PDF
Experience feedback Api Platform _ DTO-1.pdf
file
If output is defined, ObjectMapperProvider prioritizes mapping the source entity (retrieved by the ItemProvider) to the output class instead of the input DTO. If the developer tries to bypass this by setting map: false (to prevent the output from "hijacking" the read phase), the ObjectMapperProvider returns the raw entity.
As a result, the DeserializeProvider fails to perform a "merge" between the request data and the existing state because the object provided to it is not an instance of the input DTO. This leads to the DTO being populated with null for all properties not explicitly sent in the PATCH body, effectively breaking partial updates.
How to reproduce Define a resource where the input and output differ, and disable automatic mapping to avoid the output class interfering with the read state:
#[ApiResource(
operations: [
new Patch(
uriTemplate: '/contact/{id}',
input: ContactUpdate::class,
output: ContactResource::class,
map: false, // Note: forced to false due to mapping conflicts
normalizationContext: ['groups' => ['read:contact']],
),
],
stateOptions: new Options(entityClass: Contact::class),
)]
#[Map(target: Contact::class)]
class ContactUpdate
{
public ?string $name = null;
public ?string $firstname = null;
}- Send a PATCH request: PATCH /contact/1 with body {"firstname": "John"}.
- Inspect the ContactUpdate object in the ValidateProvider (or subsequent providers).
- Actual result: $name is null (even if it has a value in the database).
- Expected result: $name should contain the current value from the database (merged state).
Possible Solution
The ObjectMapperProvider should distinguish between the destination object for state data transfer (hydrating the input DTO) and the destination object for the final response (output).
The logic should ensure that if an input is present and the operation is PATCH or PUT, the priority mapping of the object retrieved by the ReadProvider is directed towards the input class, regardless of whether an output is defined for the normalization phase.
Additional Context This creates a configuration "deadlock":
-
Without map: false: The ObjectMapperProvider tries to transform the Entity into the Output class too early, before the input DTO is even populated.
-
With map: false: The DeserializeProvider receives an Entity but needs to populate a DTO; it therefore creates a fresh DTO instance from scratch, losing all original data from the database.