-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Add JSON_THROW_ON_ERROR option for json_decode/encode() #2662
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fe44c50
bf3df52
242d2f8
d1e5e98
c2bf7c3
3b20763
6a4d3b6
64a684d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,7 @@ static PHP_FUNCTION(json_last_error); | |
static PHP_FUNCTION(json_last_error_msg); | ||
|
||
PHP_JSON_API zend_class_entry *php_json_serializable_ce; | ||
PHP_JSON_API zend_class_entry *php_json_exception_ce; | ||
|
||
PHP_JSON_API ZEND_DECLARE_MODULE_GLOBALS(json) | ||
|
||
|
@@ -95,6 +96,9 @@ static PHP_MINIT_FUNCTION(json) | |
INIT_CLASS_ENTRY(ce, "JsonSerializable", json_serializable_interface); | ||
php_json_serializable_ce = zend_register_internal_interface(&ce); | ||
|
||
INIT_CLASS_ENTRY(ce, "JsonException", NULL); | ||
php_json_exception_ce = zend_register_internal_class_ex(&ce, zend_ce_exception); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like this to extend There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Further investigation reveals it's because There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please don't extend runtime exception. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, I'm happy not to! I'd be curious to hear your reasoning, though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's simply not a reason to extend another exception. It doesn't add any benefit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair enough. It's not worth the effort in any case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because SPL might not be loaded is a good reason. :D There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's probably any easy workaround. I also cba. |
||
|
||
/* options for json_encode */ | ||
PHP_JSON_REGISTER_CONSTANT("JSON_HEX_TAG", PHP_JSON_HEX_TAG); | ||
PHP_JSON_REGISTER_CONSTANT("JSON_HEX_AMP", PHP_JSON_HEX_AMP); | ||
|
@@ -116,6 +120,7 @@ static PHP_MINIT_FUNCTION(json) | |
/* common options for json_decode and json_encode */ | ||
PHP_JSON_REGISTER_CONSTANT("JSON_INVALID_UTF8_IGNORE", PHP_JSON_INVALID_UTF8_IGNORE); | ||
PHP_JSON_REGISTER_CONSTANT("JSON_INVALID_UTF8_SUBSTITUTE", PHP_JSON_INVALID_UTF8_SUBSTITUTE); | ||
PHP_JSON_REGISTER_CONSTANT("JSON_THROW_ON_ERROR", PHP_JSON_THROW_ON_ERROR); | ||
|
||
/* json error constants */ | ||
PHP_JSON_REGISTER_CONSTANT("JSON_ERROR_NONE", PHP_JSON_ERROR_NONE); | ||
|
@@ -207,14 +212,50 @@ PHP_JSON_API int php_json_encode(smart_str *buf, zval *val, int options) /* {{{ | |
} | ||
/* }}} */ | ||
|
||
static const char *php_json_get_error_msg(php_json_error_code error_code) /* {{{ */ | ||
{ | ||
switch(error_code) { | ||
case PHP_JSON_ERROR_NONE: | ||
return "No error"; | ||
case PHP_JSON_ERROR_DEPTH: | ||
return "Maximum stack depth exceeded"; | ||
case PHP_JSON_ERROR_STATE_MISMATCH: | ||
return "State mismatch (invalid or malformed JSON)"; | ||
case PHP_JSON_ERROR_CTRL_CHAR: | ||
return "Control character error, possibly incorrectly encoded"; | ||
case PHP_JSON_ERROR_SYNTAX: | ||
return "Syntax error"; | ||
case PHP_JSON_ERROR_UTF8: | ||
return "Malformed UTF-8 characters, possibly incorrectly encoded"; | ||
case PHP_JSON_ERROR_RECURSION: | ||
return "Recursion detected"; | ||
case PHP_JSON_ERROR_INF_OR_NAN: | ||
return "Inf and NaN cannot be JSON encoded"; | ||
case PHP_JSON_ERROR_UNSUPPORTED_TYPE: | ||
return "Type is not supported"; | ||
case PHP_JSON_ERROR_INVALID_PROPERTY_NAME: | ||
return "The decoded property name is invalid"; | ||
case PHP_JSON_ERROR_UTF16: | ||
return "Single unpaired UTF-16 surrogate in unicode escape"; | ||
default: | ||
return "Unknown error"; | ||
} | ||
} | ||
/* }}} */ | ||
|
||
PHP_JSON_API int php_json_decode_ex(zval *return_value, char *str, size_t str_len, zend_long options, zend_long depth) /* {{{ */ | ||
{ | ||
php_json_parser parser; | ||
|
||
php_json_parser_init(&parser, return_value, str, str_len, (int)options, (int)depth); | ||
|
||
if (php_json_yyparse(&parser)) { | ||
JSON_G(error_code) = php_json_parser_error_code(&parser); | ||
php_json_error_code error_code = php_json_parser_error_code(&parser); | ||
if (!(options & PHP_JSON_THROW_ON_ERROR)) { | ||
JSON_G(error_code) = error_code; | ||
} else { | ||
zend_throw_exception(php_json_exception_ce, php_json_get_error_msg(error_code), error_code); | ||
} | ||
RETVAL_NULL(); | ||
return FAILURE; | ||
} | ||
|
@@ -243,11 +284,19 @@ static PHP_FUNCTION(json_encode) | |
php_json_encode_init(&encoder); | ||
encoder.max_depth = (int)depth; | ||
php_json_encode_zval(&buf, parameter, (int)options, &encoder); | ||
JSON_G(error_code) = encoder.error_code; | ||
|
||
if (encoder.error_code != PHP_JSON_ERROR_NONE && !(options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR)) { | ||
smart_str_free(&buf); | ||
RETURN_FALSE; | ||
if (!(options & PHP_JSON_THROW_ON_ERROR) || (options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR)) { | ||
JSON_G(error_code) = encoder.error_code; | ||
if (encoder.error_code != PHP_JSON_ERROR_NONE && !(options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR)) { | ||
smart_str_free(&buf); | ||
RETURN_FALSE; | ||
} | ||
} else { | ||
if (encoder.error_code != PHP_JSON_ERROR_NONE) { | ||
smart_str_free(&buf); | ||
zend_throw_exception(php_json_exception_ce, php_json_get_error_msg(encoder.error_code), encoder.error_code); | ||
RETURN_FALSE; | ||
} | ||
} | ||
|
||
smart_str_0(&buf); /* copy? */ | ||
|
@@ -277,10 +326,16 @@ static PHP_FUNCTION(json_decode) | |
Z_PARAM_LONG(options) | ||
ZEND_PARSE_PARAMETERS_END(); | ||
|
||
JSON_G(error_code) = PHP_JSON_ERROR_NONE; | ||
if (!(options & PHP_JSON_THROW_ON_ERROR)) { | ||
JSON_G(error_code) = PHP_JSON_ERROR_NONE; | ||
} | ||
|
||
if (!str_len) { | ||
JSON_G(error_code) = PHP_JSON_ERROR_SYNTAX; | ||
if (!(options & PHP_JSON_THROW_ON_ERROR)) { | ||
JSON_G(error_code) = PHP_JSON_ERROR_SYNTAX; | ||
} else { | ||
zend_throw_exception(php_json_exception_ce, php_json_get_error_msg(PHP_JSON_ERROR_SYNTAX), PHP_JSON_ERROR_SYNTAX); | ||
} | ||
RETURN_NULL(); | ||
} | ||
|
||
|
@@ -327,33 +382,7 @@ static PHP_FUNCTION(json_last_error_msg) | |
return; | ||
} | ||
|
||
switch(JSON_G(error_code)) { | ||
case PHP_JSON_ERROR_NONE: | ||
RETURN_STRING("No error"); | ||
case PHP_JSON_ERROR_DEPTH: | ||
RETURN_STRING("Maximum stack depth exceeded"); | ||
case PHP_JSON_ERROR_STATE_MISMATCH: | ||
RETURN_STRING("State mismatch (invalid or malformed JSON)"); | ||
case PHP_JSON_ERROR_CTRL_CHAR: | ||
RETURN_STRING("Control character error, possibly incorrectly encoded"); | ||
case PHP_JSON_ERROR_SYNTAX: | ||
RETURN_STRING("Syntax error"); | ||
case PHP_JSON_ERROR_UTF8: | ||
RETURN_STRING("Malformed UTF-8 characters, possibly incorrectly encoded"); | ||
case PHP_JSON_ERROR_RECURSION: | ||
RETURN_STRING("Recursion detected"); | ||
case PHP_JSON_ERROR_INF_OR_NAN: | ||
RETURN_STRING("Inf and NaN cannot be JSON encoded"); | ||
case PHP_JSON_ERROR_UNSUPPORTED_TYPE: | ||
RETURN_STRING("Type is not supported"); | ||
case PHP_JSON_ERROR_INVALID_PROPERTY_NAME: | ||
RETURN_STRING("The decoded property name is invalid"); | ||
case PHP_JSON_ERROR_UTF16: | ||
RETURN_STRING("Single unpaired UTF-16 surrogate in unicode escape"); | ||
default: | ||
RETURN_STRING("Unknown error"); | ||
} | ||
|
||
RETURN_STRING(php_json_get_error_msg(JSON_G(error_code))); | ||
} | ||
/* }}} */ | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
--TEST-- | ||
Test json_decode() function : JSON_THROW_ON_ERROR flag | ||
--FILE-- | ||
<?php | ||
|
||
try { | ||
var_dump(json_decode("{", false, 512, JSON_THROW_ON_ERROR)); | ||
} catch (JsonException $e) { | ||
var_dump($e); | ||
} | ||
|
||
?> | ||
--EXPECTF-- | ||
object(JsonException)#1 (7) { | ||
["message":protected]=> | ||
string(12) "Syntax error" | ||
["string":"Exception":private]=> | ||
string(0) "" | ||
["code":protected]=> | ||
int(4) | ||
["file":protected]=> | ||
string(%d) "%s" | ||
["line":protected]=> | ||
int(%d) | ||
["trace":"Exception":private]=> | ||
array(1) { | ||
[0]=> | ||
array(4) { | ||
["file"]=> | ||
string(%d) "%s" | ||
["line"]=> | ||
int(%d) | ||
["function"]=> | ||
string(11) "json_decode" | ||
["args"]=> | ||
array(4) { | ||
[0]=> | ||
string(1) "{" | ||
[1]=> | ||
bool(false) | ||
[2]=> | ||
int(512) | ||
[3]=> | ||
int(4194304) | ||
} | ||
} | ||
} | ||
["previous":"Exception":private]=> | ||
NULL | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
--TEST-- | ||
Test json_encode() function : JSON_THROW_ON_ERROR flag | ||
--FILE-- | ||
<?php | ||
|
||
try { | ||
var_dump(json_encode("\x80", JSON_THROW_ON_ERROR)); | ||
} catch (JsonException $e) { | ||
var_dump($e); | ||
} | ||
|
||
// JSON_PARTIAL_OUTPUT_ON_ERROR is incompatible with exceptions | ||
// So it overrides it for the sake of working with wrappers that add the | ||
// JSON_THROW_ON_ERROR flag | ||
var_dump(json_encode("\x80", JSON_THROW_ON_ERROR | JSON_PARTIAL_OUTPUT_ON_ERROR)); | ||
var_dump(json_last_error()); | ||
var_dump(json_last_error_msg()); | ||
|
||
?> | ||
--EXPECTF-- | ||
object(JsonException)#1 (7) { | ||
["message":protected]=> | ||
string(56) "Malformed UTF-8 characters, possibly incorrectly encoded" | ||
["string":"Exception":private]=> | ||
string(0) "" | ||
["code":protected]=> | ||
int(5) | ||
["file":protected]=> | ||
string(%d) "%s" | ||
["line":protected]=> | ||
int(%d) | ||
["trace":"Exception":private]=> | ||
array(1) { | ||
[0]=> | ||
array(4) { | ||
["file"]=> | ||
string(%d) "%s" | ||
["line"]=> | ||
int(%d) | ||
["function"]=> | ||
string(11) "json_encode" | ||
["args"]=> | ||
array(2) { | ||
[0]=> | ||
string(1) "%s" | ||
[1]=> | ||
int(4194304) | ||
} | ||
} | ||
} | ||
["previous":"Exception":private]=> | ||
NULL | ||
} | ||
string(4) "null" | ||
int(5) | ||
string(56) "Malformed UTF-8 characters, possibly incorrectly encoded" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
--TEST-- | ||
JSON_THROW_ON_ERROR: global error flag untouched | ||
--FILE-- | ||
<?php | ||
|
||
var_dump(json_last_error()); | ||
|
||
// here we cause a different kind of error to the following errors, so that | ||
// we can be sure the global error state looking unchanged isn't coincidence | ||
json_decode("\xFF"); | ||
|
||
var_dump(json_last_error()); | ||
|
||
try { | ||
json_decode("", false, 512, JSON_THROW_ON_ERROR); | ||
} catch (JsonException $e) { | ||
echo "Caught JSON exception: ", $e->getCode(), PHP_EOL; | ||
} | ||
|
||
var_dump(json_last_error()); | ||
|
||
try { | ||
json_decode("{", false, 512, JSON_THROW_ON_ERROR); | ||
} catch (JsonException $e) { | ||
echo "Caught JSON exception: ", $e->getCode(), PHP_EOL; | ||
} | ||
|
||
var_dump(json_last_error()); | ||
|
||
|
||
try { | ||
json_encode(NAN, JSON_THROW_ON_ERROR); | ||
} catch (JsonException $e) { | ||
echo "Caught JSON exception: ", $e->getCode(), PHP_EOL; | ||
} | ||
|
||
var_dump(json_last_error()); | ||
|
||
?> | ||
--EXPECT-- | ||
int(0) | ||
int(5) | ||
Caught JSON exception: 4 | ||
int(5) | ||
Caught JSON exception: 4 | ||
int(5) | ||
Caught JSON exception: 7 | ||
int(5) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't seem to comply with this newly-accepted RFC: https://wiki.php.net/rfc/class-naming
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It complies with being consistent with existing JSON-related class names, such as the one two lines above you...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO
JsonParseException
suites better forjson_decode
, because it's parsing related exception, and forjson_encode
it would be for eg.JsonEncodeException
orJsonSerializeException
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nikic I know ... I don't care that much, just noting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is one of those scenarios where PHP's case-insensitivity is actually useful. We could change the case of the existing class with no* BC break.
*except for autoloaded polyfills
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hikari-no-yume I don't see any value in renaming that. The RFC was just for the future scope so it certainly doesn't apply for
JsonSerializable
. It's already documented and people are used to that name so I wouldn't change it. I think that such change needs a separate RFC.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically I'm not sure if we should introduce
JsonException
without RFC. It really goes against the linked class-naming RFC and if it'sJSONException
then it goes against consistency of this extension. I think we need an RFC in any case.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RFC just to resolve some inconsistency that's irrelevant due to case-insensitivity? I'm sorry I brought this up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PHP sadness. The naming RFC just shouldn't be a thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Proclaiming anarchy "shouldn't be a thing", either. The respective RFC has been accepted, and so we have to comply – whether we like it or not.