Skip to content

Bug: ObjectMapperProvider prioritizes output over input DTO in PATCH operations, breaking data merge #7745

@vincent-philippe

Description

@vincent-philippe

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;
}
  1. Send a PATCH request: PATCH /contact/1 with body {"firstname": "John"}.
  2. Inspect the ContactUpdate object in the ValidateProvider (or subsequent providers).
  3. Actual result: $name is null (even if it has a value in the database).
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions