-
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
Conversation
c12e72d
to
5923600
Compare
|
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like this to extend RuntimeException
, but that made PHP complain about this not implementing Throwable
for some reason and throw an Exception
instead. :/
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.
Further investigation reveals it's because ext/json
gets initialised before ext/spl
, which provides RuntimeException
. This could be worked around somehow.
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.
Please don't extend runtime exception.
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.
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 comment
The 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 comment
The 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
There's probably any easy workaround. I also cba.
ext/json/json.c
Outdated
JSON_G(error_code) = php_json_parser_error_code(&parser); | ||
php_json_error_code error_code = php_json_parser_error_code(&parser); | ||
if (0 == (options & PHP_JSON_THROW_EXCEPTIONS)) { | ||
JSON_G(error_code) = error_code; |
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.
I don't think we should prevent json_last_error()
and json_last_error_msg()
from being used just because an exception was thrown
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.
Well, you can get the code and message from the exception. Why dirty the global error state in that 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.
I just think it's a weird inconsistency to have json_last_error()
only give you the last error if certain flags were passed
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.
e.g.
try {
json_decode("{", false, 512, JSON_THROW_EXCEPTIONS);
} catch (JsonException $e) {
var_dump($e->getMessage(), $e->getCode());
}
string(12) "Syntax error"
int(4)
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.
Well, JSON_THROW_EXCEPTIONS
completely changes the error handling model. You can't get an error return value (null
/false
), why should you get an error code or message?
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.
@DaveRandom Testing reveals it sets it whether or not it throws an exception. But bear in mind PDO's errorInfo
is a strange beast which is local to the particular PDO
object and which returns different things depending on the driver. JSON is much more straightforward.
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.
Maybe I'm just being stubborn here, but it seems silly to have neatly encapsulated state in an exception object if you then also store the result in what's effectively a global variable. You have no reason to check the global value yourself, instead you write a catch
block which will be triggered as appropriate. You have no reason to fetch the global values yourself, they'll be provided to you on the exception object.
We could write the global error code, but why let you do things the messy way?
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.
Also, if we don't write the global error code, it means we can cleanly remove it in future.
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.
I would prefer to set global as well as the function resetting it anyway so you are changing that already in
Alternatively you shouldn't touch it at all. But that could make it slightly confusing if the function is called before (json_decode wihout throwing that fails in this case) so the old state will be kept and might confuse some users.
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.
PDO::errorInfo() always contains the last error. With PDO::ATTR_ERRMODE = PDO::ERRMODE_EXCEPTION it also throws an exception with the same message.
For this reason i think json_decode should behave identically.
ext/json/json.c
Outdated
@@ -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_EXCEPTIONS", PHP_JSON_THROW_EXCEPTIONS); |
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 could be shortened to JSON_THROW
.
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.
Rename it to JSON_ERRMODE_EXCEPTION like PDO::ERRMODE_EXCEPTION. This is not shorter, but naming allows something like JSON_ERRMODE_WARNING or JSON_ERRMODE_SILENT in the future.
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.
A sensible suggestion, but that makes it even longer. If we went for JSON_THROW
, the hypothetical future versions could be JSON_WARN
and JSON_SILENT
, maybe?
Though JSON_THROW
seems maybe too simple anyway. Is its meaning clear?
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.
With JSON_ERRMODE_ as a common prefix, it is easy to understand the purpose for the constants and you never will not have any naming conflicts in the future. Anyone with an IDE without an autocomplete can create "aliases" for that in their own projects like:
define('JDO', JSON_ERRMODE_EXCEPTION | JSON_BIGINT_AS_STRING);
$data = json_decode($json, JDO);
In my opinion clear naming (with reasonable length) is more important than short code.
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.
I agree that clear naming is better. It's like that for other constants in json as well.
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.
Maybe something like JSON_ERRMODE_RECOVER or JSON_ERRMODE_PARTIAL and deprecate JSON_EXCEPTION_ON_ERROR to group the error handling with the prefix JSON_ERRMODE_?
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.
I guess you meant deprecate JSON_PARTIAL_OUTPUT_ON_ERROR
? If so that's really not a good idea IMHO as we would just force some user change their code for no reason. In addition I prefer JSON_PARTIAL_OUTPUT_ON_ERROR
as it's more clear what it does. I don't see any need to have it the same as it is in PDO.
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.
Just to correct one of my previous comments. I meant that it should be named JSON_EXCEPTION_ON_ERROR
. (PHP_JSON_EXCEPTION_ON_ERROR
is just an internal macro name ofc.)
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.
Yes, my fault and yes JSON_PARTIAL_OUTPUT_ON_ERROR is a good name.
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.
For consistency we should name it JSON_EXCEPTION_ON_ERROR, but i would prefer JSON_ERRMODE_*.
ext/json/json.c
Outdated
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 (0 == (options & PHP_JSON_THROW_EXCEPTIONS)) { |
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.
Please use !(options & PHP_JSON_THROW_EXCEPTIONS)
as it's kind of a convention in this ext :)
ext/json/json.c
Outdated
} | ||
} else { | ||
JSON_G(error_code) = PHP_JSON_ERROR_NONE; | ||
if (encoder.error_code != PHP_JSON_ERROR_NONE) { |
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.
So this basically means that this has priority over PHP_JSON_PARTIAL_OUTPUT_ON_ERROR
. Personally I think it should be other way round and it should be ignored if user select PHP_JSON_PARTIAL_OUTPUT_ON_ERROR
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.
I changed this after thinking about it.
ext/json/tests/exceptions.phpt
Outdated
@@ -0,0 +1,130 @@ | |||
--TEST-- |
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.
Please could you add a separate tests for json_decode and json_encode. It's much more useful when doing changes in the extension to immediately see what parts are failing. Probably mainly useful for me though... :)
@@ -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); |
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 for json_decode
, because it's parsing related exception, and for json_encode
it would be for eg. JsonEncodeException
or JsonSerializeException
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's JSONException
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.
6ec41e6
to
337aac6
Compare
@duncan3dc would you be willing to rework your proposal around this concept? |
@hikari-no-yume No sorry, I'm only interested in making them safe/useful by default, we already have enough workarounds to make them safe. |
Okay then! Here's an RFC: https://wiki.php.net/rfc/json_throw_on_error. |
031d65f
to
dcc868a
Compare
<?php | ||
|
||
try { | ||
var_dump(json_decode("{", false, 512, JSON_THROW_ON_ERROR)); |
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.
tab is used as indent for all json tests (at least those that I'm aware of) so it would be nice to keep ti consistent.
I’m +1 on this one, I’m just not really sure if I like the name of the new constant. I think I’d like JSON_EXCEPTION_ON_ERROR or even JSON_THROW_EXCEPTION_ON_ERROR better. The rest looks good to me! 👌 |
7fb9733
to
fb42add
Compare
c80874e
to
6a4d3b6
Compare
Merged: e823770 |
RFC: https://wiki.php.net/rfc/json_throw_on_error
Does exactly what it says on the tin.
This implements my suggestion re: @duncan3dc's proposal to make
json_decode()
andjson_encode()
throw exceptions.Possibly of interest to @kelunik, @thg2k, @RyanNerd, @cubiclesoft, @nikic, @bukka, @narfbg (each replied to the two threads).