Symfony includes two components dedicated to working with JSON:
- JsonStreamer encodes PHP data into JSON and decodes JSON back into PHP
objects by streaming the contents, which provides high performance and low
memory usage even for large payloads; - JsonPath queries and extracts values from JSON documents using the standard
JSONPath syntax.
Symfony 8.1 improves both components in several ways.
Handling Value Objects
By default, JsonStreamer serializes an object by traversing its properties one
by one. However, some objects are better represented as a single scalar value
than as a nested JSON object. Think of a Money object encoded as "100 EUR"
or a temperature represented as a number.
Symfony 8.1 introduces a generic mechanism for this called value objects.
Implement the new ValueObjectTransformerInterface to define how an object
maps to and from a scalar value:
// src/Transformer/MoneyValueObjectTransformer.php
namespace AppTransformer;
use AppValueObjectMoney;
use SymfonyComponentJsonStreamerTransformerValueObjectTransformerInterface;
use SymfonyComponentTypeInfoType;
use SymfonyComponentTypeInfoTypeBuiltinType;
/**
* @implements ValueObjectTransformerInterface<Money, string>
*/
class MoneyValueObjectTransformer implements ValueObjectTransformerInterface
{
public function transform(object $object, array $options = []): int|float|string|bool|null
{
return $object->amount.' '.$object->currency;
}
public function reverseTransform(int|float|string|bool|null $scalar, array $options = []): object
{
[$amount, $currency] = explode(' ', $scalar);
return new Money((int) $amount, $currency);
}
public static function getStreamValueType(): BuiltinType
{
return Type::string();
}
public static function getValueObjectClassName(): string
{
return Money::class;
}
}
Symfony auto-registers these transformers, and the generated code then calls
the transformer instead of traversing the object’s properties:
// write: "100 EUR"
$json = $jsonStreamWriter->write(new Money(100, 'EUR'), Type::object(Money::class));
// read: "100 EUR" -> Money(100, 'EUR')
$money = $jsonStreamReader->read('"100 EUR"', Type::object(Money::class));
Built-in DateInterval and DateTimeZone Value Objects
Building on the value object mechanism, Symfony 8.1 now handles DateInterval
and DateTimeZone objects as value objects out of the box, following the same
approach already used for DateTimeInterface.
DateInterval objects are serialized to ISO 8601 duration strings (e.g.
P2Y6M1DT12H30M5S), and DateTimeZone objects use their name (e.g.
"Europe/Paris" or "+02:00"). You can customize the duration format with
the date_interval_format option:
use SymfonyComponentTypeInfoType;
$json = $jsonStreamWriter->write($task, Type::object(Task::class), [
'date_interval_format' => 'P%yY%mM%dDT%hH%iM%sS',
]);
Configuring the DateTime Timezone
When encoding or decoding DateTimeInterface objects, you can now convert the
timezone by passing the date_time_timezone option as a string or a
DateTimeZone instance:
use SymfonyComponentTypeInfoType;
$json = $jsonStreamWriter->write($event, Type::object(Event::class), [
'date_time_timezone' => 'Asia/Tokyo',
]);
$event = $jsonStreamReader->read($json, Type::object(Event::class), [
'date_time_timezone' => new DateTimeZone('America/Mexico_City'),
]);
Defining Default Options
Repeating the same options on every write() and read() call is tedious.
Symfony 8.1 lets you define default options for the entire application, so they
are applied automatically to every operation:
# config/packages/framework.yaml
framework:
json_streamer:
default_options:
# encode properties whose value is null
include_null_properties: true
You can also define your own custom options here. Symfony passes them to your
transformers, so you can read them inside transform() and reverseTransform():
# config/packages/framework.yaml
framework:
json_streamer:
default_options:
my_custom_option: 'my_custom_value'
Custom JsonPath Functions
The JsonPath component already supports the standard functions defined by
RFC 9535 (length(), count(), match(), etc.). Symfony 8.1 lets
you register your own functions and use them inside filter expressions.
Create an invokable class and apply the #[AsJsonPathFunction] attribute to
it. Symfony registers the function automatically and makes it available in all
JsonPath queries:
use SymfonyComponentJsonPathAttributeAsJsonPathFunction;
#[AsJsonPathFunction('upper')]
final class UppercaseFunction
{
public function __invoke(mixed $value): ?string
{
return is_string($value) ? strtoupper($value) : null;
}
}
Inject the JsonPathCrawlerInterface to get a crawler pre-configured with
all your custom functions, then use them in any expression:
$crawler = $crawlerFactory->crawl($json);
$result = $crawler->find('$.items[?upper(@.title) == "HELLO"]');
The number of arguments accepted by the function is inferred automatically from
the __invoke() method signature. The optional returnType argument
controls where the function can be used: a FunctionReturnType::Value function
(the default) works in comparisons like the example above, while a
FunctionReturnType::Logical function can be used as a standalone filter test
such as $.items[?is_positive(@.value)].



