5
version
symfony 7.4
symfony/object-mapper 7.4
api-platform/symfony 4.2
php 8.4

Describe the problem

So I've been reading about the new recommandation for API Platform which is to use and map DTO instead of writing API Platform ressources metadata directly through the Entity.

See also https://api-platform.com/docs/core/dto/

So I've been trying to implement this with no luck yet.

Illustrate the problem with my code

Here's my simple entity :

# src/Entity
namespace App\Entity;
#[ORM\Entity(repositoryClass: CompanyRepository::class)]
#[ORM\Table(name: 'company')]
class Company
{
 use TimestampableEntity;
 #[ORM\Id]
 #[ORM\GeneratedValue]
 #[ORM\Column(type: Types::BIGINT)]
 #[Groups(['read:company'])]
 private ?int $id = null;
 #[ORM\Column(type: Types::STRING, length: 255)]
 #[Groups(['read:company'])]
 private string $name = '';
...
}

Here's my input Resource :

# src/Domain/Company/ApiResource
namespace App\Domain\Company\ApiResource;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use App\Entity\Company;
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
 operations: [
 new Post(
 uriTemplate: '/company',
 /** @todo security: 'is_granted("ROLE_ADMIN")', */
 normalizationContext: ['groups' => ['read:company']],
 output: CompanyCreateOutput::class,
 name: 'create_company',
 ),
 ],
 stateOptions: new Options(entityClass: Company::class),
)]
#[Map(target: Company::class)]
class CompanyCreateInput
{
 public string $name = '';
}

Now here's my output :

# src/Domain/Company/ApiResource
namespace App\Domain\Company\ApiResource;
use App\Entity\Company;
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\Serializer\Attribute\Groups;
#[Map(source: Company::class)]
class CompanyCreateOutput
{
 #[Groups(['read:company'])]
 public int $id;
 #[Groups(['read:company'])]
 public string $name;
 #[Groups(['read:company'])]
 #[Map(source: 'created_at')]
 public string $createdAt;
}

But well now when I call the route such as

POST {{url}}/company
{
 "name": "This is a test"
}

I got the following response :

{
 "@context": {
 "@vocab": "https://api.project.local/api/docs.jsonld#",
 "hydra": "http://www.w3.org/ns/hydra/core#",
 "name": "CompanyCreateInput/name"
 },
 "@type": "CompanyCreateInput",
 "@id": "/api/.well-known/genid/28ed6cfb3faf268d111c"
}

Which is to me absolutely insane response.

What I'm trying to get

I want that when I create a resource (with a POST) that it return the created resource such as :

{
 "id": 12345,
 "name": This is a test",
 "created_at": "29/12/2025 00:00:00"
}

What I've been trying

  • I've been reading how it's been proceed to normalize the object the standard way using the ItemNormalizer::normalize which do include the output argument but it does not use it... It look like an error to me...

  • I've been trying to create a custom provider CompanyCreateRepresentationProvider but I didn't know how to use it and it does not looked like to be actually used ...

asked Dec 29, 2025 at 22:27

1 Answer 1

4

yeah so api platform maintainer soyuka actually admitted. the recommended approach now is Input DTO as resource, output for docs, and do your thing in processor.
your problem is that ObjectMapperProvider and ObjectMapperProcessor handle output differently. provider does it right
$class = $operation->getOutput()['class'] ?? $operation->getClass();
but processor just ignores output entirely
return $this->objectMapper->map($persisted, $operation->getClass());
so it maps your entity back to Input DTO instead of Output. fun stuff.
anyway, use map: false to skip that whole mess and return Output DTO yourself:

#[ApiResource(
 operations: [
 new Post(
 uriTemplate: '/company',
 output: CompanyCreateOutput::class,
 processor: CompanyCreateProcessor::class,
 map: false,
 ),
 ],
)]
class CompanyCreateInput
{
 #[Assert\NotBlank]
 public string $name = '';
}
final readonly class CompanyCreateProcessor implements ProcessorInterface
{
 public function __construct(
 private EntityManagerInterface $entityManager,
 ) {}
 public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): CompanyCreateOutput
 {
 $company = new Company();
 $company->setName($data->name);
 $this->entityManager->persist($company);
 $this->entityManager->flush();
 $output = new CompanyCreateOutput();
 $output->id = $company->getId();
 $output->name = $company->getName();
 $output->createdAt = $company->getCreatedAt()?->format(\DateTimeInterface::ATOM);
 return $output;
 }
}

map is legit btw. when false, processor skips its logic.
see also issues: 1, 2

answered Dec 30, 2025 at 6:58
Sign up to request clarification or add additional context in comments.

5 Comments

I'll give this "map" option a try and came back to you. Also I wanted to thank you for all the link and your detailed answer, it really helps
It work verry well, I came with a little custom processor and made it work. So indeed the output is not really used right ?
I'm trying to make this work in my code: github.com/arthurGrinjo/ApiPlatformDtoPostPutFailure But I still get an unable to generate IRI error.. What do I miss? Because it is returning the ResponseDto now in the processor.
After the my custom processor it still continues to the writeProcessor which, in my opinion, should be skipped after the custom processor.
So removing the itemUriTemplate fixed it! Thanks for rubberducking!

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.