Commit cd80e3e807c for php.net

commit cd80e3e807ce8ba90a9335a94f90a39dea0c24cd
Author: Tim Düsterhus <tim@tideways-gmbh.com>
Date:   Thu Jun 25 21:26:21 2026 +0200

    json: Store errors in a structured fashion (#22421)

    This will make it both easier to consume the error information and reduce the
    chance for mistakes when settings errors, because not every field will need to
    be set manually.

    The functions that have been removed as part of this change were added in
    master only, thus removing those is not an API change between the latest stable
    (PHP 8.5) and this PR.

    Fixes php/php-src#22420.

diff --git a/ext/json/json.c b/ext/json/json.c
index 3ba53e959c5..ac033c057ac 100644
--- a/ext/json/json.c
+++ b/ext/json/json.c
@@ -60,18 +60,14 @@ static PHP_GINIT_FUNCTION(json)
 	ZEND_TSRMLS_CACHE_UPDATE();
 #endif
 	json_globals->encoder_depth = 0;
-	json_globals->error_code = 0;
-	json_globals->error_line = 0;
-	json_globals->error_column = 0;
+	php_json_error_details_clear(&json_globals->error_details);
 	json_globals->encode_max_depth = PHP_JSON_PARSER_DEFAULT_DEPTH;
 }
 /* }}} */

 static PHP_RINIT_FUNCTION(json)
 {
-	JSON_G(error_code) = 0;
-	JSON_G(error_line) = 0;
-	JSON_G(error_column) = 0;
+	php_json_error_details_clear(&JSON_G(error_details));
 	return SUCCESS;
 }

@@ -134,7 +130,11 @@ PHP_JSON_API zend_result php_json_encode_ex(smart_str *buf, zval *val, int optio
 	encoder.max_depth = depth;

 	return_code = php_json_encode_zval(buf, val, options, &encoder);
-	JSON_G(error_code) = encoder.error_code;
+	JSON_G(error_details) = (php_json_error_details){
+		.code = encoder.error_code,
+		.line = 0,
+		.column = 0,
+	};

 	return return_code;
 }
@@ -179,12 +179,12 @@ static const char *php_json_get_error_msg(php_json_error_code error_code) /* {{{
 }
 /* }}} */

-static zend_string *php_json_get_error_msg_with_location(php_json_error_code error_code, size_t line, size_t column) /* {{{ */
+static zend_string *php_json_get_error_msg_with_location(const php_json_error_details *details) /* {{{ */
 {
-	const char *base_msg = php_json_get_error_msg(error_code);
+	const char *base_msg = php_json_get_error_msg(details->code);

-	if (line > 0 && column > 0) {
-		return zend_strpprintf(0, "%s near location %zu:%zu", base_msg, line, column);
+	if (details->line > 0 && details->column > 0) {
+		return zend_strpprintf(0, "%s near location %zu:%zu", base_msg, details->line, details->column);
 	}

 	return zend_string_init(base_msg, strlen(base_msg), 0);
@@ -198,17 +198,14 @@ PHP_JSON_API zend_result php_json_decode_ex(zval *return_value, const char *str,
 	php_json_parser_init(&parser, return_value, str, str_len, (int)options, (int)depth);

 	if (php_json_yyparse(&parser)) {
-		php_json_error_code error_code = php_json_parser_error_code(&parser);
-		size_t error_line = php_json_parser_error_line(&parser);
-		size_t error_column = php_json_parser_error_column(&parser);
+		php_json_error_details details;
+		php_json_parser_error_details(&parser, &details);

 		if (!(options & PHP_JSON_THROW_ON_ERROR)) {
-			JSON_G(error_code) = error_code;
-			JSON_G(error_line) = error_line;
-			JSON_G(error_column) = error_column;
+			JSON_G(error_details) = details;
 		} else {
-			zend_string *error_msg = php_json_get_error_msg_with_location(error_code, error_line, error_column);
-			zend_throw_exception(php_json_exception_ce, ZSTR_VAL(error_msg), error_code);
+			zend_string *error_msg = php_json_get_error_msg_with_location(&details);
+			zend_throw_exception(php_json_exception_ce, ZSTR_VAL(error_msg), details.code);
 			zend_string_release(error_msg);
 		}
 		RETVAL_NULL();
@@ -228,13 +225,8 @@ PHP_JSON_API bool php_json_validate_ex(const char *str, size_t str_len, zend_lon
 	php_json_parser_init_ex(&parser, &tmp, str, str_len, (int)options, (int)depth, parser_validate_methods);

 	if (php_json_yyparse(&parser)) {
-		php_json_error_code error_code = php_json_parser_error_code(&parser);
-		size_t error_line = php_json_parser_error_line(&parser);
-		size_t error_column = php_json_parser_error_column(&parser);
+		php_json_parser_error_details(&parser, &JSON_G(error_details));

-		JSON_G(error_code) = error_code;
-		JSON_G(error_line) = error_line;
-		JSON_G(error_column) = error_column;
 		return false;
 	}

@@ -263,7 +255,12 @@ PHP_FUNCTION(json_encode)
 	php_json_encode_zval(&buf, parameter, (int)options, &encoder);

 	if (!(options & PHP_JSON_THROW_ON_ERROR) || (options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR)) {
-		JSON_G(error_code) = encoder.error_code;
+		JSON_G(error_details) = (php_json_error_details){
+			.code = encoder.error_code,
+			.line = 0,
+			.column = 0,
+		};
+
 		if (encoder.error_code != PHP_JSON_ERROR_NONE && !(options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR)) {
 			smart_str_free(&buf);
 			RETURN_FALSE;
@@ -299,16 +296,16 @@ PHP_FUNCTION(json_decode)
 	ZEND_PARSE_PARAMETERS_END();

 	if (!(options & PHP_JSON_THROW_ON_ERROR)) {
-		JSON_G(error_code) = PHP_JSON_ERROR_NONE;
-		JSON_G(error_line) = 0;
-		JSON_G(error_column) = 0;
+		php_json_error_details_clear(&JSON_G(error_details));
 	}

 	if (!str_len) {
 		if (!(options & PHP_JSON_THROW_ON_ERROR)) {
-			JSON_G(error_code) = PHP_JSON_ERROR_SYNTAX;
-			JSON_G(error_line) = 0;
-			JSON_G(error_column) = 0;
+			JSON_G(error_details) = (php_json_error_details){
+				.code = PHP_JSON_ERROR_SYNTAX,
+				.line = 0,
+				.column = 0,
+			};
 		} else {
 			zend_throw_exception(php_json_exception_ce, php_json_get_error_msg(PHP_JSON_ERROR_SYNTAX), PHP_JSON_ERROR_SYNTAX);
 		}
@@ -360,15 +357,16 @@ PHP_FUNCTION(json_validate)
 	}

 	if (!str_len) {
-		JSON_G(error_code) = PHP_JSON_ERROR_SYNTAX;
-		JSON_G(error_line) = 0;
-		JSON_G(error_column) = 0;
+		JSON_G(error_details) = (php_json_error_details){
+			.code = PHP_JSON_ERROR_SYNTAX,
+			.line = 0,
+			.column = 0,
+		};
+
 		RETURN_FALSE;
 	}

-	JSON_G(error_code) = PHP_JSON_ERROR_NONE;
-	JSON_G(error_line) = 0;
-	JSON_G(error_column) = 0;
+	php_json_error_details_clear(&JSON_G(error_details));

 	if (depth <= 0) {
 		zend_argument_value_error(2, "must be greater than 0");
@@ -389,7 +387,7 @@ PHP_FUNCTION(json_last_error)
 {
 	ZEND_PARSE_PARAMETERS_NONE();

-	RETURN_LONG(JSON_G(error_code));
+	RETURN_LONG(JSON_G(error_details).code);
 }
 /* }}} */

@@ -398,10 +396,6 @@ PHP_FUNCTION(json_last_error_msg)
 {
 	ZEND_PARSE_PARAMETERS_NONE();

-	RETVAL_STR(php_json_get_error_msg_with_location(
-		JSON_G(error_code),
-		JSON_G(error_line),
-		JSON_G(error_column)
-	));
+	RETURN_STR(php_json_get_error_msg_with_location(&JSON_G(error_details)));
 }
 /* }}} */
diff --git a/ext/json/json_parser.y b/ext/json/json_parser.y
index cf296ed9db6..0d3b90b29e1 100644
--- a/ext/json/json_parser.y
+++ b/ext/json/json_parser.y
@@ -311,14 +311,11 @@ PHP_JSON_API php_json_error_code php_json_parser_error_code(const php_json_parse
 	return parser->scanner.errcode;
 }

-PHP_JSON_API size_t php_json_parser_error_line(const php_json_parser *parser)
+PHP_JSON_API void php_json_parser_error_details(const php_json_parser *parser, php_json_error_details *out)
 {
-	return parser->scanner.errloc.first_line;
-}
-
-PHP_JSON_API size_t php_json_parser_error_column(const php_json_parser *parser)
-{
-	return parser->scanner.errloc.first_column;
+	out->code = parser->scanner.errcode;
+	out->line = parser->scanner.errloc.first_line;
+	out->column = parser->scanner.errloc.first_column;
 }

 static const php_json_parser_methods default_parser_methods =
diff --git a/ext/json/php_json.h b/ext/json/php_json.h
index e2728835ed4..f34684e149d 100644
--- a/ext/json/php_json.h
+++ b/ext/json/php_json.h
@@ -52,6 +52,18 @@ typedef enum {
 	PHP_JSON_ERROR_NON_BACKED_ENUM,
 } php_json_error_code;

+typedef struct php_json_error_details {
+	php_json_error_code code;
+	size_t line;
+	size_t column;
+} php_json_error_details;
+
+static inline void php_json_error_details_clear(php_json_error_details *out) {
+	out->code = PHP_JSON_ERROR_NONE;
+	out->line = 0;
+	out->column = 0;
+}
+
 /* json_decode() options */
 #define PHP_JSON_OBJECT_AS_ARRAY            (1<<0)
 #define PHP_JSON_BIGINT_AS_STRING           (1<<1)
@@ -83,9 +95,7 @@ typedef enum {
 ZEND_BEGIN_MODULE_GLOBALS(json)
 	int encoder_depth;
 	int encode_max_depth;
-	php_json_error_code error_code;
-	size_t error_line;
-	size_t error_column;
+	php_json_error_details error_details;
 ZEND_END_MODULE_GLOBALS(json)

 PHP_JSON_API ZEND_EXTERN_MODULE_GLOBALS(json)
diff --git a/ext/json/php_json_parser.h b/ext/json/php_json_parser.h
index 4a7d64307c4..888a0d317fe 100644
--- a/ext/json/php_json_parser.h
+++ b/ext/json/php_json_parser.h
@@ -83,9 +83,7 @@ PHP_JSON_API void php_json_parser_init(

 PHP_JSON_API php_json_error_code php_json_parser_error_code(const php_json_parser *parser);

-PHP_JSON_API size_t php_json_parser_error_line(const php_json_parser *parser);
-
-PHP_JSON_API size_t php_json_parser_error_column(const php_json_parser *parser);
+PHP_JSON_API void php_json_parser_error_details(const php_json_parser *parser, php_json_error_details *out);

 PHP_JSON_API int php_json_parse(php_json_parser *parser);

diff --git a/ext/json/tests/gh22420.phpt b/ext/json/tests/gh22420.phpt
new file mode 100644
index 00000000000..eecdc6d97a8
--- /dev/null
+++ b/ext/json/tests/gh22420.phpt
@@ -0,0 +1,16 @@
+--TEST--
+GH-22420: json_encode() errors don't clear the error position
+--FILE--
+<?php
+
+var_dump(json_decode('{132'));
+var_dump(json_last_error_msg());
+var_dump(json_encode("\xFE\xD0"));
+var_dump(json_last_error_msg());
+
+?>
+--EXPECTF--
+NULL
+string(30) "Syntax error near location 1:2"
+bool(false)
+string(56) "Malformed UTF-8 characters, possibly incorrectly encoded"