diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml
index 23da8b07bcb04..d6379b71666b7 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml
@@ -31,6 +31,11 @@
+
+
+
+
+
diff --git a/src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php
new file mode 100644
index 0000000000000..cd80e32a2c2b9
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php
@@ -0,0 +1,121 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Serializer\Normalizer;
+
+use Symfony\Component\Debug\Exception\FlattenException;
+use Symfony\Component\Serializer\Exception\InvalidArgumentException;
+use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
+
+/**
+ * Normalizes FlattenException instances.
+ *
+ * @author Pascal Luna
+ *
+ * @experimental
+ */
+class FlattenExceptionNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
+{
+ /**
+ * {@inheritdoc}
+ *
+ * @throws InvalidArgumentException
+ */
+ public function normalize($object, $format = null, array $context = [])
+ {
+ if (!$object instanceof FlattenException) {
+ throw new InvalidArgumentException(sprintf('The object must be an instance of %s.', FlattenException::class));
+ }
+ /* @var FlattenException $object */
+
+ $normalized = [
+ 'detail' => $object->getMessage(),
+ 'code' => $object->getCode(),
+ 'headers' => $object->getHeaders(),
+ 'class' => $object->getClass(),
+ 'file' => $object->getFile(),
+ 'line' => $object->getLine(),
+ 'previous' => null === $object->getPrevious() ? null : $this->normalize($object->getPrevious(), $format, $context),
+ 'trace' => $object->getTrace(),
+ 'trace_as_string' => $object->getTraceAsString(),
+ ];
+ if (null !== $status = $object->getStatusCode()) {
+ $normalized['status'] = $status;
+ }
+
+ return $normalized;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsNormalization($data, $format = null)
+ {
+ return $data instanceof FlattenException;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function denormalize($data, $type, $format = null, array $context = [])
+ {
+ if (!\is_array($data)) {
+ throw new NotNormalizableValueException(sprintf(
+ 'The normalized data must be an array, got %s.',
+ \is_object($data) ? \get_class($data) : \gettype($data)
+ ));
+ }
+
+ $object = new FlattenException();
+
+ $object->setMessage($data['detail'] ?? null);
+ $object->setCode($data['code'] ?? null);
+ $object->setStatusCode($data['status'] ?? null);
+ $object->setClass($data['class'] ?? null);
+ $object->setFile($data['file'] ?? null);
+ $object->setLine($data['line'] ?? null);
+
+ if (isset($data['headers'])) {
+ $object->setHeaders((array) $data['headers']);
+ }
+ if (isset($data['previous'])) {
+ $object->setPrevious($this->denormalize($data['previous'], $type, $format, $context));
+ }
+ if (isset($data['trace'])) {
+ $property = new \ReflectionProperty(FlattenException::class, 'trace');
+ $property->setAccessible(true);
+ $property->setValue($object, (array) $data['trace']);
+ }
+ if (isset($data['trace_as_string'])) {
+ $property = new \ReflectionProperty(FlattenException::class, 'traceAsString');
+ $property->setAccessible(true);
+ $property->setValue($object, $data['trace_as_string']);
+ }
+
+ return $object;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsDenormalization($data, $type, $format = null, array $context = [])
+ {
+ return FlattenException::class === $type;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasCacheableSupportsMethod(): bool
+ {
+ return __CLASS__ === \get_class($this);
+ }
+}
diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/FlattenExceptionNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/FlattenExceptionNormalizerTest.php
new file mode 100644
index 0000000000000..a59117504b736
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Tests/Normalizer/FlattenExceptionNormalizerTest.php
@@ -0,0 +1,159 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Serializer\Tests\Normalizer;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Debug\Exception\FlattenException;
+use Symfony\Component\Serializer\Exception\InvalidArgumentException;
+use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
+use Symfony\Component\Serializer\Normalizer\FlattenExceptionNormalizer;
+
+/**
+ * @author Pascal Luna
+ */
+class FlattenExceptionNormalizerTest extends TestCase
+{
+ /**
+ * @var FlattenExceptionNormalizer
+ */
+ private $normalizer;
+
+ protected function setUp(): void
+ {
+ $this->normalizer = new FlattenExceptionNormalizer();
+ }
+
+ public function testSupportsNormalization(): void
+ {
+ $this->assertTrue($this->normalizer->supportsNormalization(new FlattenException()));
+ $this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
+ }
+
+ /**
+ * @dataProvider provideFlattenException
+ */
+ public function testNormalize(FlattenException $exception): void
+ {
+ $normalized = $this->normalizer->normalize($exception);
+ $previous = null === $exception->getPrevious() ? null : $this->normalizer->normalize($exception->getPrevious());
+
+ $this->assertSame($exception->getMessage(), $normalized['detail']);
+ $this->assertSame($exception->getCode(), $normalized['code']);
+ if (null !== $exception->getStatusCode()) {
+ $this->assertSame($exception->getStatusCode(), $normalized['status']);
+ } else {
+ $this->assertArrayNotHasKey('status', $normalized);
+ }
+ $this->assertSame($exception->getHeaders(), $normalized['headers']);
+ $this->assertSame($exception->getClass(), $normalized['class']);
+ $this->assertSame($exception->getFile(), $normalized['file']);
+ $this->assertSame($exception->getLine(), $normalized['line']);
+ $this->assertSame($previous, $normalized['previous']);
+ $this->assertSame($exception->getTrace(), $normalized['trace']);
+ $this->assertSame($exception->getTraceAsString(), $normalized['trace_as_string']);
+ }
+
+ public function provideFlattenException(): array
+ {
+ return [
+ 'instance from constructor' => [new FlattenException()],
+ 'instance from exception' => [FlattenException::createFromThrowable(new \RuntimeException('foo', 42))],
+ 'instance with previous exception' => [FlattenException::createFromThrowable(new \RuntimeException('foo', 42, new \Exception()))],
+ 'instance with headers' => [FlattenException::createFromThrowable(new \RuntimeException('foo', 42), 404, ['Foo' => 'Bar'])],
+ ];
+ }
+
+ public function testNormalizeBadObjectTypeThrowsException(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->normalizer->normalize(new \stdClass());
+ }
+
+ public function testSupportsDenormalization(): void
+ {
+ $this->assertTrue($this->normalizer->supportsDenormalization(null, FlattenException::class));
+ $this->assertFalse($this->normalizer->supportsDenormalization(null, \stdClass::class));
+ }
+
+ public function testDenormalizeValidData(): void
+ {
+ $normalized = [];
+ $exception = $this->normalizer->denormalize($normalized, FlattenException::class);
+
+ $this->assertInstanceOf(FlattenException::class, $exception);
+ $this->assertNull($exception->getMessage());
+ $this->assertNull($exception->getCode());
+ $this->assertNull($exception->getStatusCode());
+ $this->assertNull($exception->getHeaders());
+ $this->assertNull($exception->getClass());
+ $this->assertNull($exception->getFile());
+ $this->assertNull($exception->getLine());
+ $this->assertNull($exception->getPrevious());
+ $this->assertNull($exception->getTrace());
+ $this->assertNull($exception->getTraceAsString());
+
+ $normalized = [
+ 'detail' => 'Something went foobar.',
+ 'code' => 42,
+ 'status' => 404,
+ 'headers' => ['Content-Type' => 'application/json'],
+ 'class' => \get_class($this),
+ 'file' => 'foo.php',
+ 'line' => 123,
+ 'previous' => [
+ 'detail' => 'Previous exception',
+ 'code' => 0,
+ ],
+ 'trace' => [
+ [
+ 'namespace' => '', 'short_class' => '', 'class' => '', 'type' => '', 'function' => '', 'file' => 'foo.php', 'line' => 123, 'args' => [],
+ ],
+ ],
+ 'trace_as_string' => '#0 foo.php(123): foo()'.PHP_EOL.'#1 bar.php(456): bar()',
+ ];
+ $exception = $this->normalizer->denormalize($normalized, FlattenException::class);
+
+ $this->assertInstanceOf(FlattenException::class, $exception);
+ $this->assertSame($normalized['detail'], $exception->getMessage());
+ $this->assertSame($normalized['code'], $exception->getCode());
+ $this->assertSame($normalized['status'], $exception->getStatusCode());
+ $this->assertSame($normalized['headers'], $exception->getHeaders());
+ $this->assertSame($normalized['class'], $exception->getClass());
+ $this->assertSame($normalized['file'], $exception->getFile());
+ $this->assertSame($normalized['line'], $exception->getLine());
+ $this->assertSame($normalized['trace'], $exception->getTrace());
+ $this->assertSame($normalized['trace_as_string'], $exception->getTraceAsString());
+
+ $this->assertInstanceOf(FlattenException::class, $previous = $exception->getPrevious());
+ $this->assertSame($normalized['previous']['detail'], $previous->getMessage());
+ $this->assertSame($normalized['previous']['code'], $previous->getCode());
+ }
+
+ /**
+ * @dataProvider provideInvalidNormalizedData
+ */
+ public function testDenormalizeInvalidDataThrowsException($normalized): void
+ {
+ $this->expectException(NotNormalizableValueException::class);
+ $this->normalizer->denormalize($normalized, FlattenException::class);
+ }
+
+ public function provideInvalidNormalizedData(): array
+ {
+ return [
+ 'null' => [null],
+ 'string' => ['foo'],
+ 'integer' => [42],
+ 'object' => [new \stdClass()],
+ ];
+ }
+}
diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json
index 087289fd7ffc1..28c2eac9126db 100644
--- a/src/Symfony/Component/Serializer/composer.json
+++ b/src/Symfony/Component/Serializer/composer.json
@@ -25,6 +25,7 @@
"symfony/property-access": "~3.4|~4.0",
"symfony/http-foundation": "~3.4|~4.0",
"symfony/cache": "~3.4|~4.0",
+ "symfony/debug": "~3.4|~4.0",
"symfony/property-info": "^3.4.13|~4.0",
"symfony/validator": "~3.4|~4.0",
"doctrine/annotations": "~1.0",