From c61a43d654b544d35bc999a1279a557a874d6406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Ostroluck=C3=BD?= Date: Sun, 30 Jul 2023 22:33:19 +0200 Subject: [PATCH] [Serializer] Add support for seld/jsonlint in order to enhance error messages --- composer.json | 1 + src/Symfony/Component/Serializer/CHANGELOG.md | 1 + .../Serializer/Encoder/JsonDecode.php | 27 ++++++++++++++++--- .../Tests/Encoder/JsonDecodeTest.php | 11 +++++--- .../Component/Serializer/composer.json | 1 + 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 18ca8b8195f6e..09909a3b703e1 100644 --- a/composer.json +++ b/composer.json @@ -150,6 +150,7 @@ "predis/predis": "^1.1|^2.0", "psr/http-client": "^1.0", "psr/simple-cache": "^1.0|^2.0|^3.0", + "seld/jsonlint": "^1.10", "symfony/mercure-bundle": "^0.3", "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", "symfony/runtime": "self.version", diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 0fb810b06bcb9..17221fe61c039 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Deprecate Doctrine annotations support in favor of native attributes * Deprecate passing an annotation reader to the constructor of `AnnotationLoader` * Allow the `Groups` attribute/annotation on classes + * JsonDecode: Add `json_decode_detailed_errors` option 6.3 --- diff --git a/src/Symfony/Component/Serializer/Encoder/JsonDecode.php b/src/Symfony/Component/Serializer/Encoder/JsonDecode.php index a9ca028451826..6875ff81c05dd 100644 --- a/src/Symfony/Component/Serializer/Encoder/JsonDecode.php +++ b/src/Symfony/Component/Serializer/Encoder/JsonDecode.php @@ -11,7 +11,9 @@ namespace Symfony\Component\Serializer\Encoder; +use Seld\JsonLint\JsonParser; use Symfony\Component\Serializer\Exception\NotEncodableValueException; +use Symfony\Component\Serializer\Exception\UnsupportedException; /** * Decodes JSON data. @@ -30,6 +32,11 @@ class JsonDecode implements DecoderInterface */ public const ASSOCIATIVE = 'json_decode_associative'; + /** + * True to enable seld/jsonlint as a source for more specific error messages when json_decode fails. + */ + public const DETAILED_ERROR_MESSAGES = 'json_decode_detailed_errors'; + public const OPTIONS = 'json_decode_options'; /** @@ -39,6 +46,7 @@ class JsonDecode implements DecoderInterface private array $defaultContext = [ self::ASSOCIATIVE => false, + self::DETAILED_ERROR_MESSAGES => false, self::OPTIONS => 0, self::RECURSION_DEPTH => 512, ]; @@ -69,6 +77,10 @@ public function __construct(array $defaultContext = []) * json_decode_options: integer * Specifies additional options as per documentation for json_decode * + * json_decode_detailed_errors: bool + * If true, enables seld/jsonlint as a source for more specific error messages when json_decode fails. + * If false or not specified, this method will use default error messages from PHP's json_decode + * * @throws NotEncodableValueException * * @see https://php.net/json_decode @@ -89,11 +101,20 @@ public function decode(string $data, string $format, array $context = []): mixed return $decodedData; } - if (\JSON_ERROR_NONE !== json_last_error()) { - throw new NotEncodableValueException(json_last_error_msg()); + if (\JSON_ERROR_NONE === json_last_error()) { + return $decodedData; + } + $errorMessage = json_last_error_msg(); + + if (!($context[self::DETAILED_ERROR_MESSAGES] ?? $this->defaultContext[self::DETAILED_ERROR_MESSAGES])) { + throw new NotEncodableValueException($errorMessage); + } + + if (!class_exists(JsonParser::class)) { + throw new UnsupportedException(sprintf('Enabling "%s" serializer option requires seld/jsonlint. Try running "composer require seld/jsonlint".', self::DETAILED_ERROR_MESSAGES)); } - return $decodedData; + throw new NotEncodableValueException((new JsonParser())->lint($data)?->getMessage() ?: $errorMessage); } public function supportsDecoding(string $format): bool diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/JsonDecodeTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/JsonDecodeTest.php index ffef479e21f25..66cd10114cc90 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/JsonDecodeTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/JsonDecodeTest.php @@ -58,17 +58,20 @@ public static function decodeProvider() /** * @dataProvider decodeProviderException */ - public function testDecodeWithException($value) + public function testDecodeWithException(string $value, string $expectedExceptionMessage, array $context) { $this->expectException(UnexpectedValueException::class); - $this->decode->decode($value, JsonEncoder::FORMAT); + $this->expectExceptionMessage($expectedExceptionMessage); + $this->decode->decode($value, JsonEncoder::FORMAT, $context); } public static function decodeProviderException() { return [ - ["{'foo': 'bar'}"], - ['kaboom!'], + ["{'foo': 'bar'}", 'Syntax error', []], + ["{'foo': 'bar'}", 'single quotes instead of double quotes', ['json_decode_detailed_errors' => true]], + ['kaboom!', 'Syntax error', ['json_decode_detailed_errors' => false]], + ['kaboom!', "Expected one of: 'STRING', 'NUMBER', 'NULL',", ['json_decode_detailed_errors' => true]], ]; } } diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index e0c646d292b2a..91b69a79ba287 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -22,6 +22,7 @@ }, "require-dev": { "doctrine/annotations": "^1.12|^2", + "seld/jsonlint": "^1.10", "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/config": "^5.4|^6.0|^7.0",