diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
index 3f05ad7a59030..d63b0172335d1 100644
--- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
@@ -6,6 +6,7 @@ CHANGELOG
* Add support for assets pre-compression
* Rename `TranslationUpdateCommand` to `TranslationExtractCommand`
+ * Add JsonEncoder services and configuration
7.2
---
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php
index ae2523e515d0c..45d08a975bd83 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php
@@ -53,6 +53,9 @@ class UnusedTagsPass implements CompilerPassInterface
'form.type_guesser',
'html_sanitizer',
'http_client.client',
+ 'json_encoder.denormalizer',
+ 'json_encoder.encodable',
+ 'json_encoder.normalizer',
'kernel.cache_clearer',
'kernel.cache_warmer',
'kernel.event_listener',
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index 372da4a5ee8e5..99592fe4989c9 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -31,6 +31,7 @@
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\IpUtils;
+use Symfony\Component\JsonEncoder\EncoderInterface;
use Symfony\Component\Lock\Lock;
use Symfony\Component\Lock\Store\SemaphoreStore;
use Symfony\Component\Mailer\Mailer;
@@ -181,6 +182,7 @@ public function getConfigTreeBuilder(): TreeBuilder
$this->addHtmlSanitizerSection($rootNode, $enableIfStandalone);
$this->addWebhookSection($rootNode, $enableIfStandalone);
$this->addRemoteEventSection($rootNode, $enableIfStandalone);
+ $this->addJsonEncoderSection($rootNode, $enableIfStandalone);
return $treeBuilder;
}
@@ -2570,4 +2572,26 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable
->end()
;
}
+
+ private function addJsonEncoderSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone): void
+ {
+ $rootNode
+ ->children()
+ ->arrayNode('json_encoder')
+ ->info('JSON encoder configuration')
+ ->{$enableIfStandalone('symfony/json-encoder', EncoderInterface::class)}()
+ ->fixXmlConfig('path')
+ ->children()
+ ->arrayNode('paths')
+ ->info('Namespaces and paths of encodable/decodable classes.')
+ ->normalizeKeys(false)
+ ->useAttributeAsKey('namespace')
+ ->scalarPrototype()->end()
+ ->defaultValue([])
+ ->end()
+ ->end()
+ ->end()
+ ->end()
+ ;
+ }
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 3f51575820ae2..862abe3ca5942 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -99,6 +99,11 @@
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\HttpKernel\Log\DebugLoggerConfigurator;
+use Symfony\Component\JsonEncoder\Decode\Denormalizer\DenormalizerInterface as JsonEncoderDenormalizerInterface;
+use Symfony\Component\JsonEncoder\DecoderInterface as JsonEncoderDecoderInterface;
+use Symfony\Component\JsonEncoder\Encode\Normalizer\NormalizerInterface as JsonEncoderNormalizerInterface;
+use Symfony\Component\JsonEncoder\EncoderInterface as JsonEncoderEncoderInterface;
+use Symfony\Component\JsonEncoder\JsonEncoder;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Lock\PersistingStoreInterface;
@@ -176,6 +181,7 @@
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\TypeResolver\PhpDocAwareReflectionTypeResolver;
use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver;
+use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface;
use Symfony\Component\Uid\Factory\UuidFactory;
use Symfony\Component\Uid\UuidV4;
use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider;
@@ -414,7 +420,7 @@ public function load(array $configs, ContainerBuilder $container): void
$container->removeDefinition('console.command.serializer_debug');
}
- if ($this->readConfigEnabled('type_info', $container, $config['type_info'])) {
+ if ($typeInfoEnabled = $this->readConfigEnabled('type_info', $container, $config['type_info'])) {
$this->registerTypeInfoConfiguration($container, $loader);
}
@@ -422,6 +428,14 @@ public function load(array $configs, ContainerBuilder $container): void
$this->registerPropertyInfoConfiguration($container, $loader);
}
+ if ($this->readConfigEnabled('json_encoder', $container, $config['json_encoder'])) {
+ if (!$typeInfoEnabled) {
+ throw new LogicException('JsonEncoder support cannot be enabled as the TypeInfo component is not '.(interface_exists(TypeResolverInterface::class) ? 'enabled.' : 'installed. Try running "composer require symfony/type-info".'));
+ }
+
+ $this->registerJsonEncoderConfiguration($config['json_encoder'], $container, $loader);
+ }
+
if ($this->readConfigEnabled('lock', $container, $config['lock'])) {
$this->registerLockConfiguration($config['lock'], $container, $loader);
}
@@ -1990,6 +2004,36 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
$container->setParameter('.serializer.named_serializers', $config['named_serializers'] ?? []);
}
+ private function registerJsonEncoderConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void
+ {
+ if (!class_exists(JsonEncoder::class)) {
+ throw new LogicException('JsonEncoder support cannot be enabled as the JsonEncoder component is not installed. Try running "composer require symfony/json-encoder".');
+ }
+
+ $container->registerForAutoconfiguration(JsonEncoderNormalizerInterface::class)
+ ->addTag('json_encoder.normalizer');
+ $container->registerForAutoconfiguration(JsonEncoderDenormalizerInterface::class)
+ ->addTag('json_encoder.denormalizer');
+
+ $loader->load('json_encoder.php');
+
+ $container->registerAliasForArgument('json_encoder.encoder', JsonEncoderEncoderInterface::class, 'json.encoder');
+ $container->registerAliasForArgument('json_encoder.decoder', JsonEncoderDecoderInterface::class, 'json.decoder');
+
+ $container->setParameter('.json_encoder.encoders_dir', '%kernel.cache_dir%/json_encoder/encoder');
+ $container->setParameter('.json_encoder.decoders_dir', '%kernel.cache_dir%/json_encoder/decoder');
+ $container->setParameter('.json_encoder.lazy_ghosts_dir', '%kernel.cache_dir%/json_encoder/lazy_ghost');
+
+ $encodableDefinition = (new Definition())
+ ->setAbstract(true)
+ ->addTag('container.excluded')
+ ->addTag('json_encoder.encodable');
+
+ foreach ($config['paths'] as $namespace => $path) {
+ $loader->registerClasses($encodableDefinition, $namespace, $path);
+ }
+ }
+
private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void
{
if (!interface_exists(PropertyInfoExtractorInterface::class)) {
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/json_encoder.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/json_encoder.php
new file mode 100644
index 0000000000000..b864ed0f9a893
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/json_encoder.php
@@ -0,0 +1,126 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\DependencyInjection\Loader\Configurator;
+
+use Symfony\Component\JsonEncoder\CacheWarmer\EncoderDecoderCacheWarmer;
+use Symfony\Component\JsonEncoder\CacheWarmer\LazyGhostCacheWarmer;
+use Symfony\Component\JsonEncoder\Decode\Denormalizer\DateTimeDenormalizer;
+use Symfony\Component\JsonEncoder\Encode\Normalizer\DateTimeNormalizer;
+use Symfony\Component\JsonEncoder\JsonDecoder;
+use Symfony\Component\JsonEncoder\JsonEncoder;
+use Symfony\Component\JsonEncoder\Mapping\Decode\AttributePropertyMetadataLoader as DecodeAttributePropertyMetadataLoader;
+use Symfony\Component\JsonEncoder\Mapping\Decode\DateTimeTypePropertyMetadataLoader as DecodeDateTimeTypePropertyMetadataLoader;
+use Symfony\Component\JsonEncoder\Mapping\Encode\AttributePropertyMetadataLoader as EncodeAttributePropertyMetadataLoader;
+use Symfony\Component\JsonEncoder\Mapping\Encode\DateTimeTypePropertyMetadataLoader as EncodeDateTimeTypePropertyMetadataLoader;
+use Symfony\Component\JsonEncoder\Mapping\GenericTypePropertyMetadataLoader;
+use Symfony\Component\JsonEncoder\Mapping\PropertyMetadataLoader;
+
+return static function (ContainerConfigurator $container) {
+ $container->services()
+ // encoder/decoder
+ ->set('json_encoder.encoder', JsonEncoder::class)
+ ->args([
+ tagged_locator('json_encoder.normalizer'),
+ service('json_encoder.encode.property_metadata_loader'),
+ param('.json_encoder.encoders_dir'),
+ false,
+ ])
+ ->set('json_encoder.decoder', JsonDecoder::class)
+ ->args([
+ tagged_locator('json_encoder.denormalizer'),
+ service('json_encoder.decode.property_metadata_loader'),
+ param('.json_encoder.decoders_dir'),
+ param('.json_encoder.lazy_ghosts_dir'),
+ ])
+ ->alias(JsonEncoder::class, 'json_encoder.encoder')
+ ->alias(JsonDecoder::class, 'json_encoder.decoder')
+
+ // metadata
+ ->stack('json_encoder.encode.property_metadata_loader', [
+ inline_service(EncodeAttributePropertyMetadataLoader::class)
+ ->args([
+ service('.inner'),
+ tagged_locator('json_encoder.normalizer'),
+ service('type_info.resolver'),
+ ]),
+ inline_service(EncodeDateTimeTypePropertyMetadataLoader::class)
+ ->args([
+ service('.inner'),
+ ]),
+ inline_service(GenericTypePropertyMetadataLoader::class)
+ ->args([
+ service('.inner'),
+ service('type_info.type_context_factory'),
+ ]),
+ inline_service(PropertyMetadataLoader::class)
+ ->args([
+ service('type_info.resolver'),
+ ]),
+ ])
+
+ ->stack('json_encoder.decode.property_metadata_loader', [
+ inline_service(DecodeAttributePropertyMetadataLoader::class)
+ ->args([
+ service('.inner'),
+ tagged_locator('json_encoder.denormalizer'),
+ service('type_info.resolver'),
+ ]),
+ inline_service(DecodeDateTimeTypePropertyMetadataLoader::class)
+ ->args([
+ service('.inner'),
+ ]),
+ inline_service(GenericTypePropertyMetadataLoader::class)
+ ->args([
+ service('.inner'),
+ service('type_info.type_context_factory'),
+ ]),
+ inline_service(PropertyMetadataLoader::class)
+ ->args([
+ service('type_info.resolver'),
+ ]),
+ ])
+
+ // normalizers/denormalizers
+ ->set('json_encoder.normalizer.date_time', DateTimeNormalizer::class)
+ ->tag('json_encoder.normalizer')
+ ->set('json_encoder.denormalizer.date_time', DateTimeDenormalizer::class)
+ ->args([
+ false,
+ ])
+ ->tag('json_encoder.denormalizer')
+ ->set('json_encoder.denormalizer.date_time_immutable', DateTimeDenormalizer::class)
+ ->args([
+ true,
+ ])
+ ->tag('json_encoder.denormalizer')
+
+ // cache
+ ->set('.json_encoder.cache_warmer.encoder_decoder', EncoderDecoderCacheWarmer::class)
+ ->args([
+ tagged_iterator('json_encoder.encodable'),
+ service('json_encoder.encode.property_metadata_loader'),
+ service('json_encoder.decode.property_metadata_loader'),
+ param('.json_encoder.encoders_dir'),
+ param('.json_encoder.decoders_dir'),
+ false,
+ service('logger')->ignoreOnInvalid(),
+ ])
+ ->tag('kernel.cache_warmer')
+
+ ->set('.json_encoder.cache_warmer.lazy_ghost', LazyGhostCacheWarmer::class)
+ ->args([
+ tagged_iterator('json_encoder.encodable'),
+ param('.json_encoder.lazy_ghosts_dir'),
+ ])
+ ->tag('kernel.cache_warmer')
+ ;
+};
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
index 91528b60f3de6..9cb89207ddade 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
@@ -46,6 +46,7 @@
+
@@ -1003,4 +1004,13 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
index c7113cfb47d45..963cac6386c57 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
@@ -970,6 +970,10 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
'remote-event' => [
'enabled' => !class_exists(FullStack::class) && class_exists(RemoteEvent::class),
],
+ 'json_encoder' => [
+ 'enabled' => false,
+ 'paths' => [],
+ ],
];
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/json_encoder.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/json_encoder.php
new file mode 100644
index 0000000000000..42204b2cbb1dd
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/json_encoder.php
@@ -0,0 +1,14 @@
+loadFromExtension('framework', [
+ 'annotations' => false,
+ 'http_method_override' => false,
+ 'handle_all_throwables' => true,
+ 'php_errors' => ['log' => true],
+ 'type_info' => [
+ 'enabled' => true,
+ ],
+ 'json_encoder' => [
+ 'enabled' => true,
+ ],
+]);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml
index c01e857838bc3..a3e5cfd88b5ff 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml
@@ -46,5 +46,6 @@
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/json_encoder.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/json_encoder.xml
new file mode 100644
index 0000000000000..a20f98567581a
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/json_encoder.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml
index 7550749eb1a1e..8e272d11bfb47 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml
@@ -70,3 +70,4 @@ framework:
formats:
csv: ['text/csv', 'text/plain']
pdf: 'application/pdf'
+ json_encoder: ~
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/json_encoder.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/json_encoder.yml
new file mode 100644
index 0000000000000..e09f7c7d368b0
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/json_encoder.yml
@@ -0,0 +1,10 @@
+framework:
+ annotations: false
+ http_method_override: false
+ handle_all_throwables: true
+ php_errors:
+ log: true
+ type_info:
+ enabled: true
+ json_encoder:
+ enabled: true
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
index 798217191e7c0..0446eb5d2e7c6 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
@@ -2497,6 +2497,12 @@ public function testSemaphoreWithService()
self::assertEquals(new Reference('my_service'), $storeDef->getArgument(0));
}
+ public function testJsonEncoderEnabled()
+ {
+ $container = $this->createContainerFromFile('json_encoder');
+ $this->assertTrue($container->has('json_encoder.encoder'));
+ }
+
protected function createContainer(array $data = [])
{
return new ContainerBuilder(new EnvPlaceholderParameterBag(array_merge([
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/JsonEncoderTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/JsonEncoderTest.php
new file mode 100644
index 0000000000000..0ab66e6c1830f
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/JsonEncoderTest.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\FrameworkBundle\Tests\Functional;
+
+use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\Dto\Dummy;
+use Symfony\Component\JsonEncoder\DecoderInterface;
+use Symfony\Component\JsonEncoder\EncoderInterface;
+use Symfony\Component\TypeInfo\Type;
+
+/**
+ * @author Mathias Arlaud
+ */
+class JsonEncoderTest extends AbstractWebTestCase
+{
+ public function testEncode()
+ {
+ static::bootKernel(['test_case' => 'JsonEncoder']);
+
+ /** @var EncoderInterface $encoder */
+ $encoder = static::getContainer()->get('json_encoder.encoder.alias');
+
+ $this->assertSame('{"@name":"DUMMY","range":"10..20"}', (string) $encoder->encode(new Dummy(), Type::object(Dummy::class)));
+ }
+
+ public function testDecode()
+ {
+ static::bootKernel(['test_case' => 'JsonEncoder']);
+
+ /** @var DecoderInterface $decoder */
+ $decoder = static::getContainer()->get('json_encoder.decoder.alias');
+
+ $expected = new Dummy();
+ $expected->name = 'dummy';
+ $expected->range = [0, 1];
+
+ $this->assertEquals($expected, $decoder->decode('{"@name": "DUMMY", "range": "0..1"}', Type::object(Dummy::class)));
+ }
+}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/JsonEncoder/Dto/Dummy.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/JsonEncoder/Dto/Dummy.php
new file mode 100644
index 0000000000000..344b9d11cba03
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/JsonEncoder/Dto/Dummy.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\Dto;
+
+use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\RangeNormalizer;
+use Symfony\Component\JsonEncoder\Attribute\Denormalizer;
+use Symfony\Component\JsonEncoder\Attribute\EncodedName;
+use Symfony\Component\JsonEncoder\Attribute\Normalizer;
+
+/**
+ * @author Mathias Arlaud
+ */
+class Dummy
+{
+ #[EncodedName('@name')]
+ #[Normalizer('strtoupper')]
+ #[Denormalizer('strtolower')]
+ public string $name = 'dummy';
+
+ #[Normalizer(RangeNormalizer::class)]
+ #[Denormalizer(RangeNormalizer::class)]
+ public array $range = [10, 20];
+}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/JsonEncoder/RangeNormalizer.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/JsonEncoder/RangeNormalizer.php
new file mode 100644
index 0000000000000..beb9e81888ce4
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/JsonEncoder/RangeNormalizer.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder;
+
+use Symfony\Component\JsonEncoder\Decode\Denormalizer\DenormalizerInterface;
+use Symfony\Component\JsonEncoder\Encode\Normalizer\NormalizerInterface;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+
+/**
+ * @author Mathias Arlaud
+ */
+class RangeNormalizer implements NormalizerInterface, DenormalizerInterface
+{
+ public function normalize(mixed $denormalized, array $options = []): string
+ {
+ return $denormalized[0].'..'.$denormalized[1];
+ }
+
+ public function denormalize(mixed $normalized, array $options = []): array
+ {
+ return array_map(static fn (string $v): int => (int) $v, explode('..', $normalized));
+ }
+
+ public static function getNormalizedType(): BuiltinType
+ {
+ return Type::string();
+ }
+}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/JsonEncoder/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/JsonEncoder/bundles.php
new file mode 100644
index 0000000000000..15ff182c6fed5
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/JsonEncoder/bundles.php
@@ -0,0 +1,18 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
+use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle;
+
+return [
+ new FrameworkBundle(),
+ new TestBundle(),
+];
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/JsonEncoder/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/JsonEncoder/config.yml
new file mode 100644
index 0000000000000..55fdf53f5c2fd
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/JsonEncoder/config.yml
@@ -0,0 +1,24 @@
+imports:
+ - { resource: ../config/default.yml }
+
+framework:
+ http_method_override: false
+ type_info: ~
+ json_encoder:
+ enabled: true
+ paths:
+ Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\Dto\: '../../Tests/Functional/app/JsonEncoder/Dto/*'
+
+services:
+ _defaults:
+ autoconfigure: true
+
+ json_encoder.encoder.alias:
+ alias: json_encoder.encoder
+ public: true
+
+ json_encoder.decoder.alias:
+ alias: json_encoder.decoder
+ public: true
+
+ Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\RangeNormalizer: ~