Symfony 7.3: ObjectMapper, JsonPath, and the Features That Actually Change How You Work
Symfony 7.3 ships two powerful new components — ObjectMapper and JsonPath — plus console upgrades and security wins worth knowing about.
Symfony 7.3 landed in May 2025, and while a minor release number can make it easy to dismiss, this version is unusually dense with features that change day-to-day development. Two brand-new components — ObjectMapper and JsonPath — are the headliners, but there are improvements throughout the Console component, the security layer, and the asset pipeline worth knowing about as well. Here is a practical tour of what is new and when you should actually reach for it.
ObjectMapper: No More Repetitive Mapping Code
If you have ever built a transformation layer between a Doctrine entity and a DTO, or mapped API response data to an internal model, you know the pain. The code is mechanical, verbose, and it breaks silently when properties are renamed on one side. Symfony 7.3’s new ObjectMapper component addresses this directly.
The component uses PHP attributes on your target class to describe how properties should be mapped from a source object. Here is a simple example:
use Symfony\Component\ObjectMapper\Attribute\Map;
class UserDto
{
#[Map(source: 'firstName')]
public string $first_name;
#[Map(source: 'lastName')]
public string $last_name;
#[Map(if: 'isPublic', transform: 'formatEmail')]
public string $email;
}
The source option remaps a property from a differently-named source field. The if option accepts a method name or callable that controls whether the mapping runs at all. The transform option lets you pass the value through a callable before assignment. Wire it up in your code:
use Symfony\Component\ObjectMapper\ObjectMapper;
$mapper = new ObjectMapper();
$dto = $mapper->map($userEntity, UserDto::class);
That is it. No hand-written loops, no $dto->first_name = $entity->firstName, no silent gaps when you add a field to one side and forget the other.
For Symfony service injection, the mapper is available as a service out of the box after installing the component:
composer require symfony/object-mapper
The component is marked experimental in 7.3, with stabilization targeted for 7.4 and 8.0. That means the API may shift slightly, but it is already useful for internal tooling and greenfield projects. For anything customer-facing, keep an eye on the 7.4 notes before treating it as stable.
One important use case: read models in CQRS-style architectures. Rather than passing fat entities to your query handlers, you define lightweight read DTOs and use ObjectMapper to hydrate them from your Doctrine results. The attribute-based declaration keeps the mapping logic close to the DTO definition rather than scattered across service classes.
JsonPath: Querying JSON Like a First-Class Citizen
PHP has always had json_decode(), but working with deeply nested structures still ends up as a mess of null checks and array access. Symfony 7.3 ships a JsonPath component that implements RFC 9535, the official JSONPath standard, giving you a proper query language for JSON data.
use Symfony\Component\JsonPath\JsonPath;
use Symfony\Component\JsonPath\JsonCrawler;
$json = '{"store":{"books":[{"title":"PHP 8 in Action","price":29.99},{"title":"Symfony Up & Running","price":39.99}]}}';
$crawler = new JsonCrawler($json);
// Get all book titles
$titles = $crawler->find('$.store.books[*].title');
// Result: ["PHP 8 in Action", "Symfony Up & Running"]
// Get books over $30
$expensive = $crawler->find('$.store.books[?(@.price > 30)].title');
// Result: ["Symfony Up & Running"]
The query syntax is concise and powerful. $ refers to the root, .property navigates to a key, [*] selects all array elements, and [?(@.condition)] applies a filter expression. It also supports recursive descent with ..:
// Get all prices anywhere in the document
$allPrices = $crawler->find('$..price');
Install the component with:
composer require symfony/json-path
The practical applications are immediate. If your application consumes third-party API responses, GitHub webhooks, Stripe events, or any deeply nested JSON payload, JsonPath replaces the chain of $data['store']['books'][0]['title'] ?? null with a declarative query that is easier to read and test. You can also use it to validate that an expected key path exists in a response before processing — cleaner than checking each nesting level manually.
Console Commands via PHP Attributes
Defining console commands in Symfony has always required either extending Command and calling $this->setName(), $this->addArgument(), $this->addOption() in a configure() method, or using the #[AsCommand] attribute for the name. In Symfony 7.3, the entire definition can live in attributes:
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\Option;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'app:import-users', description: 'Import users from a CSV file')]
class ImportUsersCommand extends Command
{
public function __construct(
#[Argument(description: 'Path to the CSV file')]
private string $file,
#[Option(description: 'Skip the first N rows')]
private int $skip = 0,
#[Option(shortcut: 'f', description: 'Force import without confirmation')]
private bool $force = false,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
// $this->file, $this->skip, $this->force are already populated
// from the console input by the framework
$output->writeln("Importing from: {$this->file}");
return Command::SUCCESS;
}
}
The framework reads the #[Argument] and #[Option] attributes from the constructor and wires up the input binding automatically. No more boilerplate configure() method, no more $input->getArgument('file') calls scattered through execute(). The definition and the usage are co-located, and your IDE knows the types.
The Console Tree Helper
A smaller but genuinely useful addition: a new Tree helper in the Console component for rendering tree structures in terminal output. If you have ever written a command that lists file trees, dependency graphs, or hierarchical configuration, you know how much code it takes to get the indentation and branch characters right.
use Symfony\Component\Console\Helper\TreeHelper;
use Symfony\Component\Console\Helper\TreeNode;
$tree = TreeHelper::createTree($output, new TreeNode('project/'), [
new TreeNode('src/', [
new TreeNode('Controller/'),
new TreeNode('Entity/'),
new TreeNode('Repository/'),
]),
new TreeNode('templates/'),
new TreeNode('tests/'),
]);
$tree->render();
Output:
project/
├── src/
│ ├── Controller/
│ ├── Entity/
│ └── Repository/
├── templates/
└── tests/
For diagnostic commands and dev tooling this is a real quality-of-life improvement. The helper handles the branch/corner character logic so you do not have to.
Security: OAuth2 Token Introspection and OIDC Discovery
On the security side, Symfony 7.3 adds support for OAuth2 token introspection (RFC 7662) and OpenID Connect discovery. If your application acts as an OAuth2 resource server — accepting tokens issued by an external authorization server — you can now configure Symfony to validate those tokens against the introspection endpoint directly, without managing public keys yourself.
OIDC discovery support means you can point Symfony at a provider’s /.well-known/openid-configuration endpoint and let it resolve the JWKS URI, issuer, and supported scopes automatically, rather than hardcoding them in your security config. This matters if your authorization server rotates keys or if you are building multi-tenant systems where the provider configuration varies by tenant.
Pre-Compressed Web Assets
A quieter but impactful addition: Symfony 7.3 lets the Asset Mapper pre-compress CSS and JavaScript files during the build step and serve them directly, bypassing runtime Gzip or Brotli compression. The server just hands over the already-compressed file. For high-traffic applications, removing per-request compression CPU usage is a meaningful win, and for servers that do not handle compression at the web server layer (some containerized setups), this fills the gap cleanly.
Upgrading
If you are on Symfony 7.2, the upgrade path to 7.3 is straightforward — minor releases maintain backward compatibility. Run composer require symfony/framework-bundle:^7.3 and follow any deprecation notices surfaced by PHPStan or the Symfony Deprecations Detector.
For new projects, symfony new my-project --version=7.3 gives you the full stack from the start.