Skip to content

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

Closed
wants to merge 8 commits into from

Conversation

hikari-no-yume
Copy link
Contributor

@hikari-no-yume hikari-no-yume commented Jul 29, 2017

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() and json_encode() throw exceptions.

Possibly of interest to @kelunik, @thg2k, @RyanNerd, @cubiclesoft, @nikic, @bukka, @narfbg (each replied to the two threads).

@hikari-no-yume hikari-no-yume force-pushed the JSON_exceptions branch 2 times, most recently from c12e72d to 5923600 Compare July 29, 2017 15:57
@hikari-no-yume
Copy link
Contributor Author

json_decode() throws two errors when depth <= 0 and depth > INT_MAX, which this doesn't replace with exceptions currently. They both use php_error_docref… can I keep the “docref” part while also producing an exception? Should these even be exceptions, anyway? They're not the runtime errors of the kind the other errors are.

@@ -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);
Copy link
Contributor Author

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. :/

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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

Copy link
Contributor Author

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;
Copy link
Member

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

Copy link
Contributor Author

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?

Copy link
Member

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

Copy link
Contributor Author

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)

Copy link
Contributor Author

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?

Copy link
Contributor Author

@hikari-no-yume hikari-no-yume Jul 29, 2017

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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

@hikari-no-yume hikari-no-yume Jul 29, 2017

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.

Copy link
Member

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

https://github.com/hikari-no-yume/php-src/blob/5923600c99993aeb769344bfdfdf2cdea67f1bed/ext/json/json.c#L330

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.

Copy link
Contributor

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);
Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Contributor

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_?

Copy link
Member

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.

Copy link
Member

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.)

Copy link
Contributor

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.

Copy link
Contributor

@blar blar Jul 30, 2017

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)) {
Copy link
Member

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) {
Copy link
Member

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

Copy link
Contributor Author

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.

@@ -0,0 +1,130 @@
--TEST--
Copy link
Member

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);
Copy link
Contributor

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

Copy link
Member

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...

Copy link
Contributor

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

Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Member

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.

@hikari-no-yume hikari-no-yume changed the title Add JSON_THROW_EXCEPTIONS option for json_decode/encode() Add JSON_THROW_ON_ERROR option for json_decode/encode() Aug 2, 2017
@hikari-no-yume
Copy link
Contributor Author

@duncan3dc would you be willing to rework your proposal around this concept?

@duncan3dc
Copy link
Member

@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.
I have no objections to you creating an RFC for this feature though 👍

@hikari-no-yume
Copy link
Contributor Author

<?php

try {
var_dump(json_decode("{", false, 512, JSON_THROW_ON_ERROR));
Copy link
Member

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.

@nicoSWD
Copy link

nicoSWD commented Oct 5, 2017

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! 👌

@hikari-no-yume
Copy link
Contributor Author

Merged: e823770

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.