DEV Community

Cover image for Level Up Your DTOs: Pro Techniques for the Symfony ObjectMapper

Level Up Your DTOs: Pro Techniques for the Symfony ObjectMapper

Matt Mochalkin on October 18, 2025

We often reach for tools that solve immediate problems. When it comes to hydrating objects from raw data, many of us see the ObjectMapper component...
Collapse
 
arthurgrinjo profile image
arthurGrinjo

And another issue popped up; I'm trying to map an array of objects.

Documentation states: (documentation)

However I'm not able to make this happen, did you?

Collapse
 
mattleads profile image
Matt Mochalkin

Weekend project for me) Turning this article into a GitHub repo.

Collapse
 
arthurgrinjo profile image
arthurGrinjo • Edited

I started a new repo with some test-commands. Test 1, 2, 4 are working, 3 fails, 5 I still have to build. Test 6 probably is a test for trying to fix the name-converter without a decorator. (or maybe perhaps with an decorator)

repo

Thread Thread
 
mattleads profile image
Matt Mochalkin
Thread Thread
 
arthurgrinjo profile image
arthurGrinjo

Nice! Will have a closer look at it this weekend. But already love the nested object implementation. The posts are not converted into objects, not sure if the transformer is actually triggered.

Thread Thread
 
mattleads profile image
Matt Mochalkin

Updated. Check once more.

Thread Thread
 
arthurgrinjo profile image
arthurGrinjo

I solved everything in a single decorator now. For me it works fine. The objectMapper seems to be good in handling objects, however decoded json it seems not to understand in certain cases but this might improve in the near future.

decorator Thanks for rubberducking/pair programming this!

Collapse
 
arthurgrinjo profile image
arthurGrinjo • Edited

Hi @mattleads ,

I tried to use the snake_case to CamelCase converter in Symfony 8.0 but I'm not able to make it work. You already tried this? Only way I get to function snake_case combined with CamelCase is by using the

#[Map(source: 'snake_case')]
public string $snakeCase,

Thanks in advance! Regards Arthur

Collapse
 
mattleads profile image
Matt Mochalkin

All works fine)) Clean Symfony 8.0 installed just for you))

class ApiUserDto
{
    public function __construct(
        #[Map(source: 'user_id')]
        public int $userId,
        #[Map(source: 'first_name')]
        public string $firstName,
        #[Map(source: 'last_name')]
        public string $lastName
    ) {}
}
Enter fullscreen mode Exit fullscreen mode
        $data = [
            'user_id' => 99,
            'first_name' => 'Jane',
            'last_name' => 'Doe'
        ];

        var_dump($data);

        $apiUser = $this->objectMapper->map((object)$data, ApiUserDto::class);

        var_dump($apiUser);
Enter fullscreen mode Exit fullscreen mode
array(3) {
  ["user_id"]=>
  int(99)
  ["first_name"]=>
  string(4) "Jane"
  ["last_name"]=>
  string(3) "Doe"
}
object(App\DTO\ApiUserDto)#3171 (3) {
  ["userId"]=>
  int(99)
  ["firstName"]=>
  string(4) "Jane"
  ["lastName"]=>
  string(3) "Doe"
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
arthurgrinjo profile image
arthurGrinjo • Edited

Thanks for your effort! Sorry for the confusion.. That actually works great and that's the way I'm doing it right now. However, I'm trying to implement this name-converter:

serializer.name_converter.snake_case_to_camel_case

And I can't get it to work.. Not sure what I'm missing.. Probably it moved to another location (config/packages/serializer.yaml) but that's in my opinion about serializing and not about object mapping.. So I'm a little confused.

But when I get this name-converter to work that will save me a great amount of time :)

Thread Thread
 
mattleads profile image
Matt Mochalkin

I'd suggest to use Decorator))

namespace App\ObjectMapper;

use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;

#[AsDecorator(decorates: ObjectMapperInterface::class)]
readonly class SnakeCaseDecorator implements ObjectMapperInterface
{
    public function __construct(
        private ObjectMapperInterface $innerMapper
    ) {}

    public function map(object|array $source, object|string|null $target = null, array $context = []): object
    {
        if (is_array($source)) {
            if (!($context['skip_normalization'] ?? false)) {
                $source = $this->convertKeysToCamelCase($source);
            }
            $source = (object) $source;
        }

        return $this->innerMapper->map($source, $target, $context);
    }

    private function convertKeysToCamelCase(array $data): array
    {
        $result = [];
        foreach ($data as $key => $value) {
            if (is_array($value)) {
                $value = $this->convertKeysToCamelCase($value);
            }

            $newKey = lcfirst(str_replace('_', '', ucwords($key, '_')));

            $result[$newKey] = $value;
        }

        return $result;
    }
}

Enter fullscreen mode Exit fullscreen mode

And no more #Map needed))

Thread Thread
 
arthurgrinjo profile image
arthurGrinjo • Edited

Yeah that's also a good option! But that draws me to the conclusion that this configuration via the framework.yaml file with the name_converter isn't working as we expected. Because I would love the implementation. And I tried it in the serializer.yaml file but that also didn't work out.